1. 程式人生 > 實用技巧 >手牽手,從零學習Vue原始碼 系列二(變化偵測篇)

手牽手,從零學習Vue原始碼 系列二(變化偵測篇)

系列文章:

  • 手牽手,從零學習Vue原始碼 系列一(前言-目錄篇)

  • 手牽手,從零學習Vue原始碼 系列二(變化偵測篇)

    陸續更新中...

    預計八月中旬更新完畢。

    1 概述

    Vue最大的特點之一就是資料驅動檢視,那麼什麼是資料驅動檢視呢?

    其實,我們可以把資料理解為狀態,而檢視就是使用者可直觀看到頁面。頁面不可能是一成不變的,它應該是動態變化的,而它的變化也應該是有跡可尋的,或者是由使用者操作引起的,亦或者是由後端資料變化引起的,當狀態發生改變時,頁面也就應該隨之而變化,所以我們就可以得到如下一個公式:

    UI = render(state)

    上述公式中:狀態state是輸入,頁面UI輸出,狀態輸入一旦變化了,頁面輸出也隨之而變化。我們把這種特性稱之為資料驅動檢視。

    我們可以把上述公式拆成三部分:state、render()以及UI。我們知道state和UI都是使用者定的,而不變的是這個render()。所以Vue就扮演了render()這個角色,當Vue發現state變化之後,經過一系列加工,最終將變化反應在UI上。

    那麼第一個問題來了,Vue怎麼知道state變化了呢?

    變化偵測

    那麼第一個問題來了,Vue怎麼知道state變化了呢? 那麼,這就引出了Vue中的變化偵測。

    變化偵測就是追蹤狀態,亦或者說是資料的變化,一旦發生了變化,就要去更新檢視。

    變化偵測可不是個新名詞,它在目前的前端三大框架中均有涉及。在Angular中是通過髒值檢查流程來實現變化偵測;在React是通過對比虛擬DOM來實現變化偵測,而在Vue中也有自己的一套變化偵測實現機制。

    2.Object的變化偵測

    在上一篇文章中,我們知道:資料驅動檢視的關鍵點則在於我們如何知道資料發生了變化,只要知道資料在什麼時候變了,那麼問題就變得迎刃而解,我們只需在資料變化的時候去通知檢視更新即可。

    要想知道資料什麼時候被讀取了或資料什麼時候被改寫了,其實不難,JS為我們提供了Object.defineProperty方法,通過該方法我們就可以輕鬆的知道資料在什麼時候發生變化。

    那麼,我們從原始碼出發,學習在Vue中是如何對資料進行變化偵測的。

    2.1 使Object資料變得“可觀測”

    資料的每次讀和寫能夠被我們看的見,即我們能夠知道資料什麼時候被讀取了或資料什麼時候被改寫了,我們將其稱為資料變的‘可觀測’。

    要將資料變的‘可觀測’,我們就要藉助前言中提到的Object.defineProperty方法了,在本文中,我們就使用這個方法使資料變得“可觀測”。


    首先,我們定義一個數據物件car

    let car = {
      'brand':'BMW',
      'price':3000
    }
    

    我們定義了這個car的品牌brandBMW,價格price3000。現在我們可以通過car.brandcar.price直接讀寫這個car對應的屬性值。但是,當這個car的屬性被讀取或修改時,我們並不知情。那麼應該如何做才能夠讓car主動告訴我們,它的屬性被修改了呢?

    接下來,我們使用Object.defineProperty改寫上面的例子:

    let car = {}
    let val = 3000
    Object.defineProperty(car, 'price', {
      enumerable: true,
      configurable: true,
      get(){
        console.log('price屬性被讀取了')
        return val
      },
      set(newVal){
        console.log('price屬性被修改了')
        val = newVal
      }
    })
    

    通過Object.defineProperty方法給car定義了一個price屬性,並把這個屬性的讀和寫分別使用getset進行攔截,每當該屬性進行讀或寫操作的時候就會觸發get()set()。如下圖: 可以看到,car已經可以主動告訴我們它的屬性的讀寫情況了,這也意味著,這個car的資料物件已經是“可觀測”的了。

    為了把car的所有屬性都變得可觀測,我們可以編寫如下程式碼:

    /**
     * Observer類會通過遞迴的方式把一個物件的所有屬性都轉化成可觀測物件
     */
    export class Observer {
      constructor (value) {
        this.value = value
        // 給value新增一個__ob__屬性,值為該value的Observer例項
        // 相當於為value打上標記,表示它已經被轉化成響應式了,避免重複操作
        def(value,'__ob__',this)
        if (Array.isArray(value)) {
          // 當value為陣列時的邏輯
          // ...
        } else {
          this.walk(value)
        }
      }
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
    }
    /**
     * 使一個物件轉化成可觀測物件
     * @param { Object } obj 物件
     * @param { String } key 物件的key
     * @param { Any } val 物件的某個key的值
     */
    function defineReactive (obj,key,val) {
      // 如果只傳了obj和key,那麼val = obj[key]
      if (arguments.length === 2) {
        val = obj[key]
      }
      if(typeof val === 'object'){
          new Observer(val)
      }
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get(){
          console.log(`${key}屬性被讀取了`);
          return val;
        },
        set(newVal){
          if(val === newVal){
              return
          }
          console.log(`${key}屬性被修改了`);
          val = newVal;
        }
      })
    }
    

    在上面的程式碼中,我們定義了observer類,它用來將一個正常的object轉換成可觀測的object

    並且給value新增一個ob屬性,值為該valueObserver例項。這個操作相當於為value打上標記,表示它已經被轉化成響應式了,避免重複操作

    然後判斷資料的型別,只有object型別的資料才會呼叫walk將每一個屬性轉換成getter/setter的形式來偵測變化。最後,在defineReactive中當傳入的屬性值還是一個object時使用new observer(val)來遞迴子屬性,這樣我們就可以把obj中的所有屬性(包括子屬性)都轉換成getter/seter的形式來偵測變化。也就是說,只要我們將一個object傳到observer中,那麼這個object就會變成可觀測的、響應式的object

    那麼現在,我們就可以這樣定義car:

    let car = new Observer({
      'brand':'BMW',
      'price':3000
    })
    

    這樣,car的兩個屬性都變得可觀測了。

    2.2 依賴收集

    2.2.1 什麼是依賴收集

    在上一章中,我們邁出了第一步:讓object資料變的可觀測。變的可觀測以後,我們就能知道資料什麼時候發生了變化,那麼當資料發生變化時,我們去通知檢視更新就好了。那麼問題又來了,檢視那麼大,我們到底該通知誰去變化?總不能一個數據變化了,把整個檢視全部更新一遍吧,這樣顯然是不合理的。此時,你肯定會想到,視圖裡誰用到了這個資料就更新誰唄。對!你想的沒錯,就是這樣。

    視圖裡誰用到了這個資料就更新誰,我們換個優雅說法:我們把"誰用到了這個資料"稱為"誰依賴了這個資料",我們給每個資料都建一個依賴陣列(因為一個數據可能被多處使用),誰依賴了這個資料(即誰用到了這個資料)我們就把誰放入這個依賴陣列中,那麼當這個資料發生變化的時候,我們就去它對應的依賴陣列中,把每個依賴都通知一遍,告訴他們:"你們依賴的資料變啦,你們該更新啦!"。這個過程就是依賴收集。

    2.2.2 何時收集依賴?何時通知依賴更新?

    明白了什麼是依賴收集後,那麼我們到底該在何時收集依賴?又該在何時通知依賴更新?

    其實這個問題在上一小節中已經回答了,我們說過:誰用到了這個資料,那麼當這個資料變化時就通知誰。所謂誰用到了這個資料,其實就是誰獲取了這個資料,而可觀測的資料被獲取時會觸發getter屬性,那麼我們就可以在getter中收集這個依賴。同樣,當這個資料變化時會觸發setter屬性,那麼我們就可以在setter中通知依賴更新。

    總結一句話就是:在getter中收集依賴,在setter中通知依賴更新。

    2.2.3 把依賴收集到哪裡

    明白了什麼是依賴收集以及何時收集何時通知後,那麼我們該把依賴收集到哪裡?

    在2.2.1小節中也說了,我們給每個資料都建一個依賴陣列,誰依賴了這個資料我們就把誰放入這個依賴陣列中。單單用一個數組來存放依賴的話,功能好像有點欠缺並且程式碼過於耦合。我們應該將依賴陣列的功能擴充套件一下,更好的做法是我們應該為每一個數據都建立一個依賴管理器,把這個資料所有的依賴都管理起來。OK,到這裡,我們的依賴管理器Dep類應運而生,程式碼如下:

    export default class Dep {
      constructor () {
        this.subs = []
      }
      addSub (sub) {
        this.subs.push(sub)
      }
      // 刪除一個依賴
      removeSub (sub) {
        remove(this.subs, sub)
      }
      // 新增一個依賴
      depend () {
        if (window.target) {
          this.addSub(window.target)
        }
      }
      // 通知所有依賴更新
      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    /**
     * Remove an item from an array
     */
    export function remove (arr, item) {
      if (arr.length) {
        const index = arr.indexOf(item)
        if (index > -1) {
          return arr.splice(index, 1)
        }
      }
    }
    

    在上面的依賴管理器Dep類中,我們先初始化了一個subs陣列,用來存放依賴,並且定義了幾個例項方法用來對依賴進行新增,刪除,通知等操作。

    有了依賴管理器後,我們就可以在getter中收集依賴,在setter中通知依賴更新了,程式碼如下:

    function defineReactive (obj,key,val) {
      if (arguments.length === 2) {
        val = obj[key]
      }
      if(typeof val === 'object'){
        new Observer(val)
      }
      const dep = new Dep()  //例項化一個依賴管理器,生成一個依賴管理陣列dep
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get(){
          dep.depend()    // 在getter中收集依賴
          return val;
        },
        set(newVal){
          if(val === newVal){
              return
          }
          val = newVal;
          dep.notify()   // 在setter中通知依賴更新
        }
      })
    }
    

    在上述程式碼中,我們在getter中呼叫了dep.depend()方法收集依賴,在setter中呼叫dep.notify()方法通知所有依賴更新。

    2.2.4 依賴到底是誰

    通過上一章節,我們明白了什麼是依賴?何時收集依賴?以及收集的依賴存放到何處?那麼我們收集的依賴到底是誰?

    雖然我們一直在說”誰用到了這個資料誰就是依賴“,但是這僅僅是在口語層面上,那麼反應在程式碼上該如何來描述這個”誰“呢?

    其實在Vue中還實現了一個叫做Watcher的類,而Watcher類的例項就是我們上面所說的那個"誰"。換句話說就是:誰用到了資料,誰就是依賴,我們就為誰建立一個Watcher例項****。在之後資料變化時,我們不直接去通知依賴更新,而是通知依賴對應的Watch例項**,由Watcher例項去通知真正的檢視。

    Watcher類的具體實現如下:

    export default class Watcher {
      constructor (vm,expOrFn,cb) {
        this.vm = vm;
        this.cb = cb;
        this.getter = parsePath(expOrFn)
        this.value = this.get()
      }
      get () {
        window.target = this;
        const vm = this.vm
        let value = this.getter.call(vm, vm)
        window.target = undefined;
        return value
      }
      update () {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
      }
    }
    /**
     * Parse simple path.
     * 把一個形如'data.a.b.c'的字串路徑所表示的值,從真實的data物件中取出來
     * 例如:
     * data = {a:{b:{c:2}}}
     * parsePath('a.b.c')(data)  // 2
     */
    const bailRE = /[^\w.$]/
    export function parsePath (path) {
      if (bailRE.test(path)) {
        return
      }
      const segments = path.split('.')
      return function (obj) {
        for (let i = 0; i < segments.length; i++) {
          if (!obj) return
          obj = obj[segments[i]]
        }
        return obj
      }
    }
    

    誰用到了資料,誰就是依賴,我們就為誰建立一個Watcher例項,在建立Watcher例項的過程中會自動的把自己新增到這個資料對應的依賴管理器中,以後這個Watcher例項就代表這個依賴,當資料變化時,我們就通知Watcher例項,由Watcher例項再去通知真正的依賴。

    那麼,在建立Watcher例項的過程中它是如何的把自己新增到這個資料對應的依賴管理器中呢?

    下面我們分析Watcher類的程式碼實現邏輯:

    當例項化Watcher類時,會先執行其建構函式; 在建構函式中呼叫了this.get()例項方法; 在get()方法中,首先通過window.target = this把例項自身賦給了全域性的一個唯一物件window.target上,然後通過let value = this.getter.call(vm, vm)獲取一下被依賴的資料,獲取被依賴資料的目的是觸發該資料上面的getter,上文我們說過,在getter裡會呼叫dep.depend()收集依賴,而在dep.depend()中取到掛載window.target上的值並將其存入依賴陣列中,在get()方法最後將window.target釋放掉。 而當資料變化時,會觸發資料的setter,在setter中呼叫了dep.notify()方法,在dep.notify()方法中,遍歷所有依賴(即watcher例項),執行依賴的update()方法,也就是Watcher類中的update()例項方法,在update()方法中呼叫資料變化的更新回撥函式,從而更新檢視。 簡單總結一下就是:Watcher先把自己設定到全域性唯一的指定位置(window.target),然後讀取資料。因為讀取了資料,所以會觸發這個資料的getter。接著,在getter中就會從全域性唯一的那個位置讀取當前正在讀取資料的Watcher,並把這個watcher收集到Dep中去。收集好之後,當資料發生變化時,會向Dep中的每個Watcher傳送通知。通過這樣的方式,Watcher可以主動去訂閱任意一個數據的變化。為了便於理解,我們畫出了其關係流程圖,如下圖:

    以上,就徹底完成了對Object資料的偵測,依賴收集,依賴的更新等所有操作。

    2.2.5 不足之處

    雖然我們通過Object.defineProperty方法實現了對object資料的可觀測,但是這個方法僅僅只能觀測到object資料的取值及設定值,當我們向object資料裡新增一對新的key/value或刪除一對已有的key/value時,它是無法觀測到的,導致當我們對object資料新增或刪除值時,無法通知依賴,無法驅動檢視進行響應式更新。

    當然,Vue也注意到了這一點,為了解決這一問題,Vue增加了兩個全域性API:Vue.setVue.delete,這兩個API的實現原理將會在後面學習全域性API的時候說到。

    2.2.6 小結

    首先,我們通過Object.defineProperty方法實現了對object資料的可觀測,並且封裝了Observer類,讓我們能夠方便的把object資料中的所有屬性(包括子屬性)都轉換成getter/seter的形式來偵測變化。

    接著,我們學習了什麼是依賴收集?並且知道了在getter中收集依賴,在setter中通知依賴更新,以及封裝了依賴管理器Dep,用於儲存收集到的依賴。

    最後,我們為每一個依賴都建立了一個Watcher例項,當資料發生變化時,通知Watcher例項,由Watcher例項去做真實的更新操作。

    其整個流程大致如下:

    Data通過observer轉換成了getter/setter的形式來追蹤變化。 當外界通過Watcher讀取資料時,會觸發getter從而將Watcher新增到依賴中。 當資料發生了變化時,會觸發setter,從而向Dep中的依賴(即Watcher)傳送通知。 Watcher接收到通知後,會向外界傳送通知,變化通知到外界後可能會觸發檢視更新,也有可能觸發使用者的某個回撥函式等。

    3.Array的變化偵測

    3.1 前言

    上一節文章中我們介紹了Object資料的變化偵測方式,本篇文章我們來看一下對Array型資料的變化Vue是如何進行偵測的。

    為什麼Object資料和Array型資料會有兩種不同的變化偵測方式?

    這是因為對於Object資料我們使用的是JS提供的物件原型上的方法Object.defineProperty,而這個方法是物件原型上的,所以Array無法使用這個方法,所以我們需要對Array型資料設計一套另外的變化偵測機制。

    萬變不離其宗,雖然對Array型資料設計了新的變化偵測機制,但是其根本思路還是不變的。那就是:還是在獲取資料時收集依賴,資料變化時通知依賴更新。

    下面我們就通過原始碼來看看Vue對Array型資料到底是如何進行變化偵測的。

    3.2 在哪裡收集依賴

    首先還是老規矩,我們得先把用到Array型資料的地方作為依賴收集起來,那麼第一問題就是該在哪裡收集呢?

    其實Array型資料的依賴收集方式和Object資料的依賴收集方式相同,都是在getter中收集。那麼問題就來了,不是說Array無法使用Object.defineProperty方法嗎?無法使用怎麼還在getter中收集依賴呢?

    其實不然,我們回想一下平常在開發的時候,在元件的**data8*中是不是都這麼寫的:

    data(){
      return {
        arr:[1,2,3]
      }
    }
    

    想想看,arr這個資料始終都存在於一個object資料物件中,而且我們也說了,誰用到了資料誰就是依賴,那麼要用到arr這個資料,是不是得先從object資料物件中獲取一下arr資料,而從object資料物件中獲取arr資料自然就會觸發arrgetter,所以我們就可以在getter中收集依賴。

    總結一句話就是:Array型資料還是在getter中收集依賴。

    3.3 使Array型資料可觀測

    上一章節中我們知道了Array型資料還是在getter中收集依賴,換句話說就是我們已經知道了Array型資料何時被讀取了。

    回想上一篇文章中介紹Object資料變化偵測的時候,我們先讓Object資料變的可觀測,即我們能夠知道資料什麼時候被讀取了、什麼時候發生變化了。同理,對於Array型資料我們也得讓它變的可觀測,目前我們已經完成了一半可觀測,即我們只知道了Array型資料何時被讀取了,而何時發生變化我們無法知道,那麼接下來我們就來解決這一問題:當Array型資料發生變化時我們如何得知?

    3.3.1 思路分析

    Object的變化時通過setter來追蹤的,只有某個資料發生了變化,就一定會觸發這個資料上的setter。但是Array型資料沒有setter,怎麼辦?

    我們試想一下,要想讓Array型資料發生變化,那必然是操作了Array,而JS中提供的運算元組的方法就那麼幾種,我們可以把這些方法都重寫一遍,在不改變原有功能的前提下,我們為其新增一些其他功能,例如下面這個例子:

    let arr = [1,2,3]
    arr.push(4)
    Array.prototype.newPush = function(val){
      console.log('arr被修改了')
      this.push(val)
    }
    arr.newPush(4)
    

    在上面這個例子中,我們針對陣列的原生push方法定義個一個新的newPush方法,這個newPush方法內部呼叫了原生push方法,這樣就保證了新的newPush方法跟原生push方法具有相同的功能,而且我們還可以在新的newPush方法內部幹一些別的事情,比如通知變化。

    是不是很巧妙?Vue內部就是這麼幹的。

    3.3.2 陣列方法攔截器

    基於上一小節的思想,在Vue中建立了一個數組方法攔截器,它攔截在陣列例項與Array.prototype之間,在攔截器內重寫了運算元組的一些方法,當陣列例項使用運算元組方法時,其實使用的是攔截器中重寫的方法,而不再使用Array.prototype上的原生方法。如下圖所示:

    經過整理,Array原型中可以改變陣列自身內容的方法有7個,分別是:push,pop,shift,unshift,splice,sort,reverse。那麼原始碼中的攔截器程式碼如下:

    const arrayProto = Array.prototype
    // 建立一個物件作為攔截器
    export const arrayMethods = Object.create(arrayProto)
    // 改變陣列自身內容的7個方法
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    /**
     * Intercept mutating methods and emit events
     */
    methodsToPatch.forEach(function (method) {
      const original = arrayProto[method]      // 快取原生方法
      Object.defineProperty(arrayMethods, method, {
        enumerable: false,
        configurable: true,
        writable: true,
        value:function mutator(...args){
          const result = original.apply(this, args)
          return result
        }
      })
    })
    

    在上面的程式碼中,首先建立了繼承自Array原型的空物件arrayMethods,接著在arrayMethods上使用object.defineProperty方法將那些可以改變陣列自身的7個方法遍歷逐個進行封裝。最後,當我們使用push方法的時候,其實用的是arrayMethods.push,而arrayMethods.push就是封裝的新函式mutator,也就後說,實標上執行的是函式mutator,而mutator函式內部執行了original函式,這個original函式就是Array.prototype上對應的原生方法。那麼,接下來我們就可以在mutator函式中做一些其他的事,比如說傳送變化通知。

    3.3.3 使用攔截器

    在上一小節的圖中,我們把攔截器做好還不夠,還要把它掛載到陣列例項與Array.prototype之間,這樣攔截器才能夠生效。

    其實掛載不難,我們只需把資料的proto屬性設定為攔截器arrayMethods即可,原始碼實現如下:

    export class Observer {
      constructor (value) {
        this.value = value
        if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
        } else {
          this.walk(value)
        }
      }
    }
    // 能力檢測:判斷__proto__是否可用,因為有的瀏覽器不支援該屬性
    export const hasProto = '__proto__' in {}
    const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
    /**
     * Augment an target Object or Array by intercepting
     * the prototype chain using __proto__
     */
    function protoAugment (target, src: Object, keys: any) {
      target.__proto__ = src
    }
    /**
     * Augment an target Object or Array by defining
     * hidden properties.
     */
    /* istanbul ignore next */
    function copyAugment (target: Object, src: Object, keys: Array<string>) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
      }
    }
    

    上面程式碼中首先判斷了瀏覽器是否支援proto,如果支援,則呼叫protoAugment函式value.proto = arrayMethods;如果不支援,則呼叫copyAugment函式把攔截器中重寫的7個方法迴圈加入到value上。

    攔截器生效以後,當陣列資料再發生變化時,我們就可以在攔截器中通知變化了,也就是說現在我們就可以知道陣列資料何時發生變化了,OK,以上我們就完成了對Array型資料的可觀測。

    3.4. 再談依賴收集

    3.4.1 把依賴收集到哪裡

    在第二章中我們說了,陣列資料的依賴也在getter中收集,而給陣列資料新增getter/setter都是在Observer類中完成的,所以我們也應該在Observer類中收集依賴,原始碼如下:

    export class Observer {
      constructor (value) {
        this.value = value
        this.dep = new Dep()    // 例項化一個依賴管理器,用來收集陣列依賴
        if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
        } else {
          this.walk(value)
        }
      }
    }
    

    上面程式碼中,在Observer類中例項化了一個依賴管理器,用來收集陣列依賴。

    3.4.2 如何收集依賴

    在第二章中我們說了,陣列的依賴也在getter中收集,那麼在getter中到底該如何收集呢?這裡有一個需要注意的點,那就是依賴管理器定義在Observer類中,而我們需要在getter中收集依賴,也就是說我們必須在getter中能夠訪問到Observer類中的依賴管理器,才能把依賴存進去。原始碼是這麼做的:

    function defineReactive (obj,key,val) {
      let childOb = observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get(){
          if (childOb) {
            childOb.dep.depend()
          }
          return val;
        },
        set(newVal){
          if(val === newVal){
            return
          }
          val = newVal;
          dep.notify()   // 在setter中通知依賴更新
        }
      })
    }
    /**
     * Attempt to create an observer instance for a value,
     * returns the new observer if successfully observed,
     * or the existing observer if the value already has one.
     * 嘗試為value建立一個0bserver例項,如果建立成功,直接返回新建立的Observer例項。
     * 如果 Value 已經存在一個Observer例項,則直接返回它
     */
    export function observe (value, asRootData){
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else {
        ob = new Observer(value)
      }
      return ob
    }
    

    在上面程式碼中,我們首先通過observe函式為被獲取的資料arr嘗試建立一個Observer例項,在observe函式內部,先判斷當前傳入的資料上是否有ob屬性,因為在上篇文章中說了,如果資料有ob屬性,表示它已經被轉化成響應式的了,如果沒有則表示該資料還不是響應式的,那麼就呼叫new Observer(value)將其轉化成響應式的,並把資料對應的Observer例項返回。

    而在defineReactive函式中,首先獲取資料對應的Observer例項childOb,然後在getter中呼叫Observer例項上依賴管理器,從而將依賴收集起來。

    3.4.3 如何通知依賴

    到現在為止,依賴已經收集好了,並且也已經存放好了,那麼我們該如何通知依賴呢?

    其實不難,在前文說過,我們應該在攔截器裡通知依賴,要想通知依賴,首先要能訪問到依賴。要訪問到依賴也不難,因為我們只要能訪問到被轉化成響應式的資料value即可,因為vaule上的ob就是其對應的Observer類例項,有了Observer類例項我們就能訪問到它上面的依賴管理器,然後只需呼叫依賴管理器的dep.notify()方法,讓它去通知依賴更新即可。原始碼如下:

    /**
     * Intercept mutating methods and emit events
     */
    methodsToPatch.forEach(function (method) {
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        // notify change
        ob.dep.notify()
        return result
      })
    })
    

    上面程式碼中,由於我們的攔截器是掛載到陣列資料的原型上的,所以攔截器中的this就是資料value,拿到value上的Observer類例項,從而你就可以呼叫Observer類例項上面依賴管理器的dep.notify()方法,以達到通知依賴的目的。

    OK,以上就基本完成了Array資料的變化偵測

    3.5. 深度偵測

    在前文所有講的Array型資料的變化偵測都僅僅說的是陣列自身變化的偵測,比如給陣列新增一個元素或刪除陣列中一個元素,而在Vue中,不論是Object型資料還是Array型資料所實現的資料變化偵測都是深度偵測,所謂深度偵測就是不但要偵測資料自身的變化,還要偵測資料中所有子資料的變化。舉個例子:

    let arr = [
      {
        name:'NLRX'    age:'18'
      }
    ]
    

    陣列中包含了一個物件,如果該物件的某個屬性發生了變化也應該被偵測到,這就是深度偵測。

    這個實現起來比較簡單,原始碼如下:

    export class Observer {
      value: any;
      dep: Dep;
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value)   // 將陣列中的所有元素都轉化為可被偵測的響應式
        } else {
          this.walk(value)
        }
      }
      /**
       * Observe a list of Array items.
       */
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    export function observe (value, asRootData){
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else {
        ob = new Observer(value)
      }
      return ob
    }
    

    在上面程式碼中,對於Array型資料,呼叫了observeArray()方法,該方法內部會遍歷陣列中的每一個元素,然後通過呼叫observe函式將每一個元素都轉化成可偵測的響應式資料。

    而對應object資料,在上一節文章中我們已經在defineReactive函式中進行了遞迴操作。

    3.6 陣列新增元素的偵測

    對於陣列中已有的元素我們已經可以將其全部轉化成可偵測的響應式資料了,但是如果向數組裡新增一個元素的話,我們也需要將新增的這個元素轉化成可偵測的響應式資料。

    這個實現起來也很容易,我們只需拿到新增的這個元素,然後呼叫observe函式將其轉化即可。我們知道,可以向陣列內新增元素的方法有3個,分別是:pushunshiftsplice。我們只需對這3中方法分別處理,拿到新增的元素,再將其轉化即可。原始碼如下:

    /**
     * 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   // 如果是push或unshift方法,那麼傳入引數就是新增的元素
            break
          case 'splice':
            inserted = args.slice(2) // 如果是splice方法,那麼傳入引數列表中下標為2的就是新增的元素
            break
        }
        if (inserted) ob.observeArray(inserted) // 呼叫observe函式將新增的元素轉化成響應式
        // notify change
        ob.dep.notify()
        return result
      })
    })
    

    在上面攔截器定義程式碼中,如果是pushunshift方法,那麼傳入引數就是新增的元素;如果是splice方法,那麼傳入引數列表中下標為2的就是新增的元素,拿到新增的元素後,就可以呼叫observe函式將新增的元素轉化成響應式的了。

    3.7 不足之處

    前文中我們說過,對於陣列變化偵測是通過攔截器實現的,也就是說只要是通過陣列原型上的方法對陣列進行操作就都可以偵測到,但是別忘了,我們在日常開發中,還可以通過陣列的下標來操作資料,如下:

    let arr = [1,2,3]
    arr[0] = 5;       // 通過陣列下標修改陣列中的資料
    arr.length = 0    // 通過修改陣列長度清空陣列
    123
    

    而使用上述例子中的操作方式來修改陣列是無法偵測到的。同樣,Vue也注意到了這個問題, 為了解決這一問題,Vue增加了兩個全域性API:Vue.setVue.delete,這兩個API的實現原理將會在後面學習全域性API的時候說到。

    3.8 總結

    在本篇文章中,首先我們分析了對於Array型資料也在getter中進行依賴收集;其次我們發現,當陣列資料被訪問時我們輕而易舉可以知道,但是被修改時我們卻很難知道,為了解決這一問題,我們建立了陣列方法攔截器,從而成功的將陣列資料變的可觀測。接著我們對陣列的依賴收集及資料變化如何通知依賴進行了深入分析;最後我們發現Vue不但對陣列自身進行了變化偵測,還對陣列中的每一個元素以及新增的元素都進行了變化偵測,我們也分析了其實現原理。

    以上就是對Array型資料的變化偵測分析。

    文章參考資料:

    關於本系列文章,其實更多的是參考的各大佬的部落格、gitHub,加上自己的總結,希望與大家共同進步!

    系列文章:

    陸續更新中...

    預計八月中旬更新完畢。

    我曾踏足山巔,也曾跌入低谷,這兩者都讓我受益良多。個人網站:https://zhaohongcheng.com