瞭解一下Vue - [Vue是怎麼實現響應式的(二)]

寫在前面:本文爲個人在日常工作和學習中的一些總結,便於後來查漏補缺,非權威性資料,請帶着自己的思考^-^。
前文鏈接:瞭解一下Vue - [Vue是怎麼實現響應式的(二)]
前言:上一篇文章簡單介紹了基於data的Vue響應式的實現,這篇將進行一點擴展,data變更會自動觸發computed進行重新計算,從而反映到視圖層面,那這個過程又是怎麼做到的呢?

從Vue實例生成過程的initComputed說起

顧名思義,在initComputed中主要進行的工作是對computed進行初始化,上代碼:

function initComputed (vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null) // 用於存放computed 相關的watcher
  
  for (const key in computed) { // 遍歷computed,爲每一個computed屬性創建watcher實例,這個watcher實例的作用後面會體現
    // 這裏可以看出,平時我們的computed一般都是函數形式的,但很多時候我們也可以寫成{get() {}, set() {}},這種對象形式
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop, // 此函數參數會在watcher實例的get方法中進行執行
        noop,
        computedWatcherOptions // {dirty: true},可以翻一下之前的class Watcher代碼或者找源碼看一下,這個options其中的一個作用就在於控制實例化watcher的時候是否先執行一次get() 方法,這個get方法內部會對參數傳進來的getter進行執行
      )
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

// function defineComputed

function defineComputed ( // 整體來說此函數的作用就是通過defineProperty定義getter/setter將computed中的屬性代理到vm上
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering() // 非服務端渲染,則爲true
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key) // computed的計算結果會被緩存,不需要每次訪問computed屬性時都重新進行計算
      : userDef // computed 不使用緩存的情況
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

實例化Vue期間,對computed的處理,做了:

  1. 將computed[key]的值作爲getter參數,實例化一個Watcher對象(至於Watcher對象的作用,後面會提到);
  2. 將computed[key]的值(其實是經過包裝的)作爲getter,通過defineProperty將computed[key]代理到vm[key];

說到$mount的執行過程

$mount包含了模板編譯、render函數生成... 不再贅述
和響應式相關的是在$mount中實例化了一個render Watcher,前文已經有過標註,在實例化Watcher中會執行get()函數,從而執行render函數,在render函數的執行過程中勢必會讀取頁面渲染使用到的computed屬性,觸發其getter:

// computed屬性的getter
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key] // watcher就是initComputed時傳入computed[key]作爲getter參數實例化的watcher
    if (watcher) {
      if (watcher.dirty) { // 如果是首次讀取computed[key],一般watcher.dirty爲true
        watcher.evaluate()
        /**
        evaluate () {
            this.value = this.get()
            this.dirty = false // 執行完get之後就會將dirty置爲false,這樣下次讀取computed[key]時就不會再重新計算
        }
        執行watcher.get(),在這個函數內部會做幾個事情:
        執行pushTarget函數,將當前的Dep.target置爲當前watcher實例
        執行computed[key],計算得到結果賦值給watcher.value
        如果computed[key]函數內容是通過幾個data計算得到值,則將會觸發data的getter,
        這將會把這個幾個data的dep對象作爲依賴添加至watcher的deps列表,同時將當前watcher添加至這些dep的subs列表中,
        通俗一點說,這個過程對於當前watcher來說就是依賴收集過程,將其依賴的項添加至deps中
        對於某一個data的dep來說,就是將當前watcher添加至其觀察者列表subs中
        執行完以上過程,就會將Dep.target重置爲之前的值,
        */
      }
      if (Dep.target) {
        watcher.depend() // 這一步也很重要,此處是將當前watcher的依賴也加入到Dep.target的依賴列表中
        /**
        爲什麼要有這一步呢?
        因爲當前的Dep.target在執行完watcher.evaluate之後就被重置回了上一個Dep.target,一般來說當前的Dep.target
        就是render Watcher
        設想有這種情況:某一個data的屬性x並沒有直接用於render,那麼在render執行過程的依賴收集x就不會被添加
        到render Watcher的deps中,x的dep.subs中也沒有render Watcher 也就是說之後如果對x進行重新賦值,則不會
        通知render Watcher,此時還沒有問題,但時如果x被computed用到,由於computed沒有setter,則x被重新賦值
        通知到computed Watcher去重新計算,但是computed並沒有直接通知render Watcher的方法,這個時候render就不會
        重新執行,頁面也就不會進行更新。。。
        */
      }
      return watcher.value
    }
  }
}

總之,詳見代碼註釋。。。

當computed依賴的data更新時

這裏已經知道對data重新賦值,會觸發其對應setter

// setter
set: function reactiveSetter (newVal) {
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      
      val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify() // 這裏會通知到所有watcher,讓它們進行update
    }
    
// dep.notify
notify () { // 在render階段進行依賴收集時會將watcher加入subs列表,computed在進行計算的時候會收集依賴的data,
// 與此同時會將computed的watcher對象添加至data的dep.subs列表
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

render watcher和computed watcher執行update是不同的,computed watcher的update方法只會將watcher.dirty置爲true,這代表該computed依賴的data發生了更新,需要重新計算;這樣在render函數再次執行的時候會讀取computed,觸發computed的getter,在getter中會重新計算得出computed的新值,並且將dirty置爲false,代表只需計算一次,在同一個render loop中多次引用該computed將不會重新計算。

THE END...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章