Vue源碼解析之數組變異

来源:https://www.cnblogs.com/karthuslorin/archive/2018/12/03/10045326.html
-Advertisement-
Play Games

力有不逮的對象 眾所周知,在 中,直接修改對象屬性的值無法觸發響應式。當你直接修改了對象屬性的值,你會發現,只有數據改了,但是頁面內容並沒有改變。 這是什麼原因? 原因在於: 的響應式系統是基於 這個方法的,該方法可以監聽對象中某個元素的獲取或修改,經過了該方法處理的數據,我們稱其為響應式數據。但是 ...


力有不逮的對象

眾所周知,在 Vue 中,直接修改對象屬性的值無法觸發響應式。當你直接修改了對象屬性的值,你會發現,只有數據改了,但是頁面內容並沒有改變。

這是什麼原因?

原因在於: Vue 的響應式系統是基於Object.defineProperty這個方法的,該方法可以監聽對象中某個元素的獲取或修改,經過了該方法處理的數據,我們稱其為響應式數據。但是,該方法有一個很大的缺點,新增屬性或者刪除屬性不會觸發監聽,舉個慄子:

var vm = new Vue({
    data () {
        return {
            obj: {
                a: 1
            }
        }
    }
})
// `vm.obj.a` 現在是響應式的

vm.obj.b = 2
// `vm.obj.b` 不是響應式的

原因在於,在 Vue 初始化的時候, Vue 內部會對 data 方法的返回值進行深度響應式處理,使其變為響應式數據,所以, vm.obj.a 是響應式的。但是,之後設置的 vm.obj.b 並沒有經過 Vue 初始化時響應式的洗禮,所以,理所應當的不是響應式。

那麼,vm.obj.b可以變成響應式嗎?當然可以,通過 vm.$set 方法就可以完美地實現要求,在此不再贅述相關原理了,之後應該會寫一篇文章講述 vm.$set 背後的原理。

更凄慘的數組

上面說了這麼多,還沒有提到本篇文章的主角——數組,現在該主角出場了。

比起對象,數組的境遇更加凄慘一些,看看官方文檔:

由於 JavaScript 的限制, Vue 不能檢測以下變動的數組:

  1. 當你利用索引直接設置一個項時,例如:vm.items[indexOfItem] = newValue
  2. 當你修改數組的長度時,例如:vm.items.length = newLength

有可能官方文檔不是很清晰,那我們繼續舉個慄子:

var vm = new Vue({
    data () {
        return {
            items: ['a', 'b', 'c']
        }
    }
})
vm.items[1] = 'x' // 不是響應性的
vm.items.length = 2 // 不是響應性的

也就是說,數組連自身元素的修改也無法監聽,原因在於, Vuedata 方法返回的對象中的元素進行響應式處理時,如果元素是數組時,僅僅對數組本身進行響應式化,而不對數組內部元素進行響應式化。

這也就導致如官方文檔所寫的後果,無法直接修改數組內部元素來觸發響應式。

那麼,有沒有破解方法呢?

當然有,官方規定了 7 個數組方法,通過這 7 個數組方法,可以很開心地觸發數組的響應式,這 7 個數組方法分別是:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

可以發現,這 7 個數組方法貌似就是原生的那些數組方法,為什麼這 7 個數組方法可以觸發應式,觸發視圖更新呢?

你是不是心裡想著:數組方法了不起呀,數組方法就可以為所欲為啊?

騷瑞啊,這 7 個數組方法是真的可以為所欲為的。

因為,它們是變異後的數組方法。

數組變異思路

什麼是變異數組方法?

變異數組方法即保持數組方法原有功能不變的前提下對其進行功能拓展,在 Vue 中這個所謂的功能拓展就是添加響應式功能。

將普通的數組變為變異數組的方法分為兩步:

  1. 功能拓展
  2. 數組劫持

功能拓展

先來個思考題:

有這樣一個需求,要求在不改變原有函數功能以及調用方式的情況下,使得每次調用該函數都能在控制臺中列印出'HelloWorld'

其實思路很簡單,分為三步:

  1. 使用新的變數緩存原函數
  2. 重新定義原函數
  3. 在新定義的函數中調用原函數

看看具體的代碼實現:

function A () {
    console.log('調用了函數A')
}

const nativeA = A
A = function () {
    console.log('HelloWorld')
    nativeA()
}

可以看到,通過這種方式,我們就保證了在不改變 A 函數行為的前提下對其進行了功能拓展。

接下來,我們使用這種方法對數組原本方法進行功能拓展:

// 變異方法名稱
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

const arrayProto = Array.prototype
// 繼承原有數組的方法
const arrayMethods = Object.create(arrayProto)

mutationMethods.forEach(method => {
    // 緩存原生數組方法
    const original = arrayProto[method]
    arrayMethods[method] = function (...args) {
        const result = original.apply(this, args)
        
        console.log('執行響應式功能')
        
        return result
    }
})

從代碼中可以看出來,我們調用 arrayMethods 這個對象中的方法有兩種情況:

  1. 調用功能拓展方法:直接調用 arrayMethods 中的方法
  2. 調用原生方法:這種情況下,通過原型鏈查找定義在數組原型中的原生方法

通過上述方法,我們實現了對數組原生方法進行功能的拓展,但是,有一個巨大的問題擺在面前:我們該如何讓數組實例調用功能拓展後數組方法呢?

解決這一問題的方法就是:數組劫持。

數組劫持

數組劫持,顧名思義就是將原本數組實例要繼承的方法替換成我們功能拓展後的方法。

想一想,我們在前面實現了一個功能拓展後的數組 arrayMethods ,這個自定義的數組繼承自數組對象,我們只需要將其和普通數組實例連接起來,讓普通數組繼承於它即可。

而想實現上述操作,就是通過原型鏈。

實現方法如下代碼所示:

let arr = []
// 通過隱式原型繼承arrayMethods
arr.__proto__ = arrayMethods

// 執行變異後方法
arr.push(1)

通過功能拓展和數組劫持,我們終於實現了變異數組,接下來讓我們看看 Vue 源碼是如何實現變異數組的。

源碼解析

我們來到 src/core/observer/index.js 中在 Observer 類中的 constructor 函數:

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 檢測是否是數組
    if (Array.isArray(value)) {
        // 能力檢測
        const augment = hasProto
        ? protoAugment
        : copyAugment
        // 通過能力檢測的結果選擇不同方式進行數組劫持
        augment(value, arrayMethods, arrayKeys)
        // 對數組的響應式處理
        this.observeArray(value)
    } else {
        this.walk(value)
    }
}

Observer 這個類是 Vue 響應式系統的核心組成部分,在初始化階段最主要的功能是將目標對象進行響應式化。在這裡,我們主要關註其對數組的處理。

其對數組的處理主要是以下代碼

// 能力檢測
const augment = hasProto
? protoAugment
: copyAugment
// 通過能力檢測的結果選擇不同方式進行數組劫持
augment(value, arrayMethods, arrayKeys)
// 對數組的響應式處理,很本文關係不大,略過
this.observeArray(value)

首先定義了 augment 常量,這個常量的值由 hasProto 決定。

我們來看看 hasProto

export const hasProto = '__proto__' in {}

可以發現, hasProto 其實就是一個布爾值常量,用來表示瀏覽器是否支持直接使用 __proto__ (隱式原型) 。

所以,第一段代碼很好理解:根據根據能力檢測結果選擇不同的數組劫持方法,如果瀏覽器支持隱式原型,則調用 protoAugment 函數作為數組劫持的方法,反之則使用 copyAugment

不同的數組劫持方法

現在我們來看看 protoAugment 以及 copyAugment

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

可以看到, protoAugment 函數極其簡潔,和在數組變異思路中所說的方法一致:將數組實例直接通過隱式原型與變異數組連接起來,通過這種方式繼承變異數組中的方法。

接下來我們再看看 copyAugment

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    // Object.defineProperty的封裝
    def(target, key, src[key])
  }
}

由於在這種情況下,瀏覽器不支持直接使用隱式原型,所以數組劫持方法要麻煩很多。我們知道該函數接收的第一個參數是數組實例,第二個參數是變異數組,那麼第三個參數是什麼?

// 獲取變異數組中所有自身屬性的屬性名
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

arrayKeys 在該文件的開頭就定義了,即變異數組中的所有自身屬性的屬性名,是一個數組。

回頭再看 copyAugment 函數就很清晰了,將所有變異數組中的方法,直接定義在數組實例本身,相當於變相的實現了數組的劫持。

實現了數組劫持後,我們再來看看 Vue 中是怎樣實現數組的功能拓展的。

功能拓展

數組功能拓展的代碼位於 src/core/observer/array.js ,代碼如下:

import { def } from '../util/index'

// 緩存數組原型
const arrayProto = Array.prototype
// 實現 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto)

// 需要進行功能拓展的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  // 緩存原生數組方法
  const original = arrayProto[method]
  // 在變異數組中定義功能拓展方法
  def(arrayMethods, method, function mutator (...args) {
    // 執行並緩存原生數組方法的執行結果
    const result = original.apply(this, args)
    // 響應式處理
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    // 返回原生數組方法的執行結果
    return result
  })
})

可以發現,源碼在實現的方式上,和我在數組變異思路中採用的方法一致,只不過在其中添加了響應式的處理。

總結

Vue 的變異數組從本質上是來說是一種裝飾器模式,通過學習它的原理,我們在實際工作中可以輕鬆處理這類保持原有功能不變的前提下對其進行功能拓展的需求。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 有很多學習大數據的朋友,在初期學習時,通常會對如何學習而感到迷茫。我經常在知乎上收到朋友關於如何入門、如何規劃學習大數據、大數據的學習流程是什麼的一些問題。今天我就粗淺的總結幾點學習大數據方法。 一、興趣建立 興趣是可以讓一個人持續關註一個事物的核心動力,那麼興趣的培養就非常重要了。如果你把寫程式單 ...
  • 今天講到的最重要的內容: Spring cloud是什麼? Spring cloud項目 spring cloud版本 什麼事springcloud? spring cloud 為開發人員提供了快速構建分散式系統的一些工具,包括配置管理、服務發現、斷路器、路由、微代理、事件匯流排、全局鎖、決策競選、分... ...
  • 本文由騰訊雲資料庫發表 前言 CynosDB是新一代分散式資料庫,100%相容MySQL和PostgreSQL,支持存儲彈性擴展,一主多從共用數據,性能更是超越社區原生MySQL和PostgreSQL。CynosDB採用share storage架構,其彈性擴展和高性價比的基石則是CynosDB F ...
  • 下麵的內容是關於Android下實現一個簡單的計算器的內容。 import android.app.Activity; import android.os.Bundle;import android.view.View;import android.widget.Button;import andr ...
  • 轉載請標明出處:https://www.cnblogs.com/tangZH/p/10060573.html 從gitlab上面把項目拉下來之後,用as打開,發現as無法關聯git,沒有git相關的菜單。 最終發現是因為沒有打開這個: 設置之後就可以了。 ...
  • In August 2015, Google announced the release of Android Mobile Vision API. At that time this API had mainly three components Face Detection, Barcode s... ...
  • 一、修改點擊的動畫 函數: 這是預設的點擊的動畫 我們用代碼修改一下: 這是之後的效果: 二、設置下劃線指示器的寬度不要填充完(動態的根據TabView的寬度來設置自身的寬度) 函數: 這是預設的效果 我們用代碼修改一下: 這是之後的效果: 三、設置下劃線指示器的樣式 函數: 這是預設的效果 我們用 ...
  • "覓知音"這個APP的第一個版本從提交審核到上架,歷時三個星期,其中遇到一些審核上的問題,它的處理或許能幫助到遇到同樣問題的小伙伴們,所以這裡列舉出來,這三個星期如何跟蘋果的審核團隊“鬥智鬥勇”。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...