Vue 依賴收集原理分析


此文已由作者吳維偉授權網易雲社區發布。

歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。



Vue實例在初始化時,可以接受以下幾類數據:

  • 模板

  • 初始化數據

  • 傳遞給組件的屬性值

  • computed

  • watch

  • methods

Vue 根據實例化時接受的數據,在將數據和模板轉化成DOM節點的同時,分析其依賴的數據。在特定數據改變時,自動在下一個周期重新渲染DOM節點

本文主要分析Vue是如何進行依賴收集的。

Vue中,與依賴收集相關的類有:

Dep : 一個訂閱者的列表類,可以增加或刪除訂閱者,可以向訂閱者發送消息

Watcher : 訂閱者類。它在初始化時可以接受getter, callback兩個函數作為參數。getter用來計算Watcher對象的值。當Watcher被觸發時,會重新通過getter計算當前Watcher的值,如果值改變,則會執行callback.

對初始化數據的處理

對於一個Vue組件,需要一個初始化數據的生成函數。如下:

export default {
    data () {        
    return {           
     text: 'some texts',       
          arr: [],           
           obj: {}
        }
    }
}

Vue為數據中的每一個key維護一個訂閱者列表。對於生成的數據,通過Object.defineProperty對其中的每一個key進行處理,主要是為每一個key設置get, set方法,以此來為對應的key收集訂閱者,並在值改變時通知對應的訂閱者。部分代碼如下:

  const dep = new Dep()  const property = Object.getOwnPropertyDescriptor(obj, key)  if (property && property.configurable === false) {    return
  }  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,    get: function reactiveGetter () {      const value = getter ? getter.call(obj) : val      if (Dep.target) {
        dep.depend()        if (childOb) {
          childOb.dep.depend()
        }        if (Array.isArray(value)) {
          dependArray(value)
        }
      }      return value
    },    set: function reactiveSetter (newVal) {      const value = getter ? getter.call(obj) : val      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {        return
      }      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)
      dep.notify()
    }
  })

每一key都有一個訂閱者列表 const dep = new Dep()

在為key進行賦值時,如果值發生了改變,則會通知所有的訂閱者 dep.notify()

在對key進行取值時,如果Dep.target有值,除正常的取值操作外會進行一些額外的操作來添加訂閱者。大多數時間里,Dep.target的值都為null,只有訂閱者在進行訂閱操作時,Dep.target才有值,為正在進行訂閱的訂閱者。此時進行取值操作,會將訂閱者加入到對應的訂閱者列表中。

訂閱者在進行訂閱操作時,主要包含以下3個步驟:

  • 將自己放在Dep.target上

  • 對自己依賴的key進行取值

  • 將自己從Dep.target移除

在執行訂閱操作后,訂閱者會被加入到相關key的訂閱者列表中。

針對對象和數組的處理

如果為key賦的值為對象:

  • 會遞歸地對這個對象中的每一key進行處理

如果為key賦的值為數組:

  • 遞歸地對這個數組中的每一個對象進行處理

  • 重新定義數組的push,pop,shift,unshift,splice,sort,reverse方法,調用以上方法時key的訂閱者列表會通知訂閱者們“值已改變”。如果調用的是push,unshift,splice方法,遞歸處理新增加的項

對模板的處理

Vue將模板處理成一個render函數。需要重新渲染DOM時,render函數結合Vue實例中的數據生成一個虛擬節點。新的虛擬節點和原虛擬節點進行對比,對需要修改的DOM節點進行修改。

訂閱者

訂閱者在初始化時主要接受2個參數getter, callback。getter用來計算訂閱者的值,所以其在執行時會對訂閱者所有需要訂閱的key進行取值。訂閱者的訂閱操作主要是通過getter來實現。

部分代碼如下:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)    let value
    const vm = this.vm    if (this.user) {      try {        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {      value = this.getter.call(vm, vm)
    }    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()    this.cleanupDeps()    return value
  }

主要步驟:

  • 將自己放在Dep.target上(pushTarget(this))

  • 執行getter(this.getter.call(vm, vm))

  • 將自己從Dep.target移除(popTarget())

  • 清理之前的訂閱(this.cleanupDeps())

此后,訂閱者在依賴的key的值發生變化會得到通知。獲得通知的訂閱者並不會立即被觸發,而是會被加入到一個待觸發的數組中,在下一個周期統一被觸發。

訂閱者在被觸發時,會執行getter來計算訂閱者的值,如果值改變,則會執行callback.

負責渲染DOM的訂閱者

Vue實例化后都會生成一個用於渲染DOM的訂閱者。此訂閱者在實例化時傳入的getter方法為渲染DOM的方法。

部分代碼如下:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

vm._watcher = new Watcher(vm, updateComponent, noop)

vm._render()結合模板和數據,計算出虛擬DOM vm._update()根據虛擬DOM渲染真實的DOM節點

此訂閱者在初始化時就會進行訂閱操作。實例化時傳入的getter為updateComponent。其中的vm._render()在執行時一定會對所有依賴的key進行取值,能完成對依賴的key的訂閱。同時vm._update()完成了第一次DOM渲染。當前依賴的key的值發生變化,訂閱者被觸發時,作為getter的updateComponent會重新執行,重新渲染DOM。因為getter返回的值一直為undefined,所以此訂閱者中的callback並沒有被用到,於是傳入了一個空函數noop作為callback

對computed的處理

通過computed可以定義一組計算屬性,通過計算屬性可以將一些復雜的計算過程抽離出來,保持模板的簡單和清晰。

代碼示例:

export default {
    data () {        return {            text: 'some texts',            arr: [],            obj: {}
        }
    },    computed: {        key1: function () {            return this.text + this.arr.length
        }
    }
}

在定義一個計算屬性時,需要定義一個key和一個計算方法。

Vue在對computed進行處理時,會為每一個計算屬性生成一個lazy狀態的訂閱者。普通的訂閱者在實例化和觸發時會執行getter來計算自身的值和進行訂閱操作。而lazy狀態的訂閱者在上述情況下只會將自身置為dirty狀態,不進行其它操作。在訂閱者執行自身的evaluate方法時,會清除自身的dirty狀態並執行getter來計算自身的值和進行訂閱。

Vue在為計算屬性生成訂閱者時的示例代碼如下:

const computedWatcherOptions = { lazy: true }// create internal watcher for the computed property.watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)

傳入的getter為自定義的計算方法,callback為空函數。(lazy狀態的訂閱者永遠都沒有機會執行callback)

Vue 在自身實例上為指定key定義get方法,使可以通過Vue實例獲取計算屬性的值。

部分代碼如下:

function createComputedGetter (key) {  return function computedGetter () {    const watcher = this._computedWatchers && this._computedWatchers[key]    if (watcher) {      if (watcher.dirty) {
        watcher.evaluate()
      }      if (Dep.target) {
        watcher.depend()
      }      return watcher.value
    }
  }
}

在對計算屬性定義的key進行取值時,會首先獲取之前生成好的訂閱者。只有訂閱者處於dirty狀態時,才會執行evaluate計算訂閱者的值。所以為計算屬性定義的計算方法只有在對計算屬性的key進行取值並且計算屬性依賴的key曾經改變時才會執行。

假如對上文定義的計算屬性key1進行取值

vm.key1; //第一次取值,自定義計算方法執行vm.key1; //第二次取值,依賴的key的值沒有變化,自定義計算方法不會執行vm.text = '' //改變計算屬性依賴的key的值,計算屬性對應的訂閱者會進入dirty狀態,自定義計算方法不會執行vm.key1; //第三次取值,計算屬性依賴的key的值發生了變化並且對計算屬性進行取值,自定義的計算方法執行
訂閱計算屬性值的變化

計算屬性的key不會維護一個訂閱者列表,也不能通過計算屬性的set方法在觸發所有訂閱者。(計算屬性不能被賦值)。一個訂閱者執行訂閱操作來訂閱計算屬性值的變化其實是訂閱了計算屬性依賴的key的值的變化。 在計算屬性的get方法中

if (Dep.target) {    watcher.depend()}

如果有訂閱者來訂閱計算屬性的變化,計算屬性會將自己的訂閱復制到正在進行訂閱的訂閱者上。watcher.depend()的作用就是如此。

例如:

//初始化訂閱者watcher, 依賴計算屬性key1var watcher = new Watcher(function () {    return vm.key1
}, noop)

vm.text = '' //計算屬性key1依賴的text的值發生變化,watcher會被觸發

對watch的處理

Vue實例化時可以傳入watch對象,來監聽某些值的變化。 例如:

export default {
    watch: {        'a.b.c': function (val, oldVal) {            console.log(val)            console.log(oldVal)
        }
    }
}

Vue 會為watch中的每一項生成一個訂閱者。訂閱者的getter通過處理字符串得到。如'a.b.c'會被處理成

function (vm) {    var a = vm.a    var b = a.b    var c = b.c    return c
}

處理字符串的源碼如下:

/**
 * Parse simple path.
 */const bailRE = /[^\w.$]/export function parsePath (path: string): any {  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
  }
}

訂閱者的callback為定義watch時傳入的監聽函數。當訂閱者被觸發時,如果訂閱者的值發生變化,則會執行callback。callback執行時會傳入變化后的值,變化前的值作為參數。


網易雲免費體驗館,0成本體驗20+款雲產品! 

更多網易技術、產品、運營經驗分享請點擊


相關文章:
【推薦】 == vs === in Javascript


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM