[VUE]computed屬性的資料響應和依賴快取實現過程

hailx發表於2018-02-03

本文是一篇vue.js原始碼學習筆記,適合對vue的資料響應實現有一定了解的同學,文中有表述不準確的地方還望指出。那麼開始了

提示:文中會看到( 知識點:xxx )這樣的標記表示在原始碼的相應位置打上標記, 方便大家閱讀

一,先看computed的實現原始碼

/src/core/instance/state.js

// initState會在new Vue()時執行
export function initState (vm: Component) {
  /*
  	other
  */
  // 如果我們定義了comouted屬性則執行initComputed
  if (opts.computed) initComputed(vm, opts.computed)
  /*
  	other
  */
}
複製程式碼

在同一個檔案中找到initComputed的定義

function initComputed (vm, computed) {
  // 往元件例項上新增一個_computedWatchers屬性,儲存所有的computed watcher
  const watchers = vm._computedWatchers = Object.create(null)
  
  // 對所有的computed屬性遍歷處理
  for (const key in computed) {
  
    // 將我們定義的computed屬性值用userDef儲存
    const userDef = computed[key]
    
    // 我們在定義computed時可以是一個函式,也可以是一個物件{get:function(){}, set:function(){}}
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    
    // 資料響應過程中的watcher(注意第二個引數是我們剛才拿到的getter,記住了)
    watchers[key] = new Watcher(
      vm,
      getter || noop, // 注意這裡,注意這裡,注意這裡,(****知識點getter)
      noop,
      computedWatcherOptions
    )
    
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } 
  }
}
複製程式碼

接下來我們還是在這個檔案中找到defineComputed的實現

export function defineComputed (target, key, userDef) {
  /* other */

  // 這裡我對原始碼進行了簡化
  // sharedPropertyDefinition是一個全域性物件
  // 拿到一個get函式
  sharedPropertyDefinition.get = createComputedGetter(key)

  /* other */

  // 這個函式的主要功能是computed屬性的get進行了重寫
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製程式碼

還是繼續看createComputedGetter很重要,很重要,很重要

function createComputedGetter (key) {

  // 返回一個函式,也就是我們在上一個函式中那個get函式
  return function computedGetter () {

    // 拿到我們在initComputed函式中新增到vm上面的_computedWatchers
    const watcher = this._computedWatchers && this._computedWatchers[key]

    // 如果我們有定義computed屬性,watcher必定存在
    if (watcher) {
      
      // 注意,注意,注意,只要在模板中使用了這個computed屬性,之後每次頁面更新就是迴圈知識點1到知識點5這個過程
      // 第二節主要就是在講這一塊,在理解下面的步驟時可以對照的看一下
      if (watcher.dirty) { // ****標記:知識點1
        watcher.evaluate() // ****標記:知識點2
      }
      if (Dep.target) { // ****標記:知識點3
        watcher.depend() // ****標記:知識點4
      }
      return watcher.value // ****標記:知識點5
    }
  }
}
複製程式碼

二,computed的屬性響應

1,一切的起源

還是在這個檔案中我們可以找到這個物件

const computedWatcherOptions = { lazy: true }
複製程式碼

回到initComputed函式中new Watcher時

watchers[key] = new Watcher(
  vm,
  getter || noop, 
  noop,
  computedWatcherOptions  // 看這裡,看這裡,看這裡(傳入{lazy: true})
)
複製程式碼
2,一切的開始

Watcher的原始碼

constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    /* ... */
    if (options) {
      this.lazy = !!options.lazy // this.lazy = true
    }
    this.getter = expOrFn
    this.dirty = this.lazy // 初始化this.dirty = true
    /* ... */
    // 注意了,注意了,注意了
    // new時this.lazy為true所以this.value = 'undefined'
    this.value = this.lazy ? undefined : this.get()
  }
複製程式碼

主要流程在createComputedGetter函式裡面

  • new Watcherthis.dirty = this.lazy 所以知識點1:watcher.dirty = true
  • 接著知識點2: watcher.evaluate()會將this.dirty = false 接著會執行Watcher的this.get()最終其實就是執行知識點getter:this.getter()
  • 繼續知識點3:Dep.target永遠為真
  • 知識點4收集依賴
  • 最後知識點5獲得computed的值

對Watcher的實現這裡就說這麼多了,本文是建立在大家對Wathcer類有一定了解的基礎上講解的。如果大家有需要可以留言後期會給大家詳細梳理下資料響應的實現過程,網上好像已經有很多相關的文章了

3,開始表演
new Vue({
  data(){
    return {
      dataA: 'a',
      dataB: 'b'
    }
  },
  template: '<div>{{computedA}}-{{dataB}}</div>',
  computed: {
    computedA() {
      return 'computed ' + this.dataA
    }
  },
  method: {
      changeA(){
          this.dataA = 'change dataA'
      },
      changeB(){
          this.dataA = 'change dataB'
      }
  }
})
複製程式碼

看在createComputedGetter函式

  • 1,第一次頁面渲染時模板中的{{computedA}}執行computedA.get() 轉到函式createComputedGetter中
  • 2,知識點1: this.dirty = true
  • 3,知識點2:watcher.evaluate()執行,將this.dirty = false,watcher內部執行知識點getter:this.getter()
this.getter = computedA = function(){
    return 'computed' + this.dataA // 看這裡,看這裡,看這裡,知識點update
}
複製程式碼

得到了wacher.value 等於 computed a

  • 4,watcher.depend()重新收集依賴
  • 5,返回wacher.value,渲染到頁面上<div>computed a-b</div>
  • 6,我們通過呼叫this.changA()改變dataA,呼叫dataA中的dep.notify(),會執行dataA的所有watcher物件wathcer.update(),因為computed所屬watcher的lazy永遠為true知識點1: this.dirty = true
  • 7,因為dataA改變了,觸發頁面重新渲染,重新渲染模板,模板中的{{computedA}}再次呼叫computedA.get(),迴圈第1步

三,computed的屬性快取

通過第二節3段中computed的響應過程我們知道,computedA會監聽dataA的改變去改變知識點1: this.dirty = true才最終執行了知識點getter

假設:

  • 我們現在執行this.changeB(),改變了dataB值,
  • 會執行dataB的所有watcher物件wathcer.update()
  • 因為dataB改變了,觸發頁面重新渲染,重新渲染模板,模板中的{{computedA}}再次呼叫computedA.get()轉到函式createComputedGetter中
  • 因為computedA並沒有監聽dataB的改變,也就不會執行到computedAwatcher.update()知識點1:this.dirty = false,最終 並不會執行到知識點:getter
  • 直接返回上次知識點:getter的結果return watcher.value

總結

因為這篇主要是講computed的知識,對於資料響應的知識沒有詳細說明,可能看起來有點懵逼,後面有需要會詳細分析下vue資料響應的實現過程。

相關文章