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

寫在前面:本文爲個人在日常工作和學習中的一些總結,非權威性資料,請帶着自己的思考^-^。

說起響應式,首先會想到Vue實例中的data屬性,例如:對data中的某一屬性重新賦值,如果該屬性用在了頁面渲染上面,則頁面會自動進行重新渲染,這裏就以data作爲切入點,來看一下Vue中的響應式是怎樣的一個實現思路。

Vue實例創建階段

在創建Vue實例的時候,執行到了一個核心方法:initState,該方法會對methods/props/methods/data/computed/watch進行初始化,此時我們只關注data的初始化:

function initData(vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  ...
  
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    
    ...
    proxy(vm, '_data', key)
    ...
    
  }
  
  ...
  
  observe(data, true)
}

代碼中省略了一些和當前研究的內容無關的代碼,用...表示;
可以看到這個方法主要做了兩件事:

  1. 將data代理到vm._data,即:訪問data中的屬性key vm[key]將觸發getter,返回vm._data[key],賦值同理;作用是顯而易見的:以後我們想要訪問/賦值data中的某個屬性key時,直接this[key]這樣就可以了,無需this._data[key]這樣
  2. 執行observe(data, true)函數,遍歷data中的每一個屬性,通過defineObject將其設爲響應式的,即:在正常使用場景中,我們訪問this[key](其中key爲data中的一個屬性)時,會由於1中緣故觸發getter進而訪問this._data[key],這樣就又觸發了當前添加的getter執行某些操作

proxy代碼:

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

observe代碼:

observe (value: any, asRootData: ?boolean): Observer | void {
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert && // 新添加屬性轉爲reactive
    !isServerRendering() && // 非服務端渲染
    (Array.isArray(value) || isPlainObject(value)) && // 數組或者對象
    Object.isExtensible(value) && // 可擴展對象
    !value._isVue // 非Vue實例
  ) {
    ob = new Observer(value) // 創建Observer實例
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this) // value.__ob__ = this, 且__ob__爲不可枚舉屬性
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys) // value.__proto__ = Array.prototype
      this.observeArray(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], obj[keys[i]])
    }
  }
}

function defineReactive (
  obj: Object, // vm instance of Vue
  key: string, // '$attr' 等屬性名
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  let childOb = !shallow && observe(val) // 對當前屬性的值繼續進行observe
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      
      ...
      
    },
    set: function reactiveSetter (newVal) {
      
      ...
      
    }
  })
}

仍然逃脫不了粘貼代碼,但是找不出比代碼更直觀的解釋了...
不過核心方法是defineReactive,它仍然是使用defineProperty對vm._data中的每一個屬性設置了getter/setter,至於getter/setter中的內容,先不去管他。
到了這裏,對於data的初始化已經告一段落。

模板編譯/掛載階段

這裏是Vue.prototype.$mount的執行階段,此階段其實包含了對於模板的編譯、對編譯結果進行轉化生成render函數、render函數的執行進行掛載
這個階段對於data的操作只存在於render函數的執行進行掛載時,核心函數的執行:new Watcher(vm, updateComponent, noop, null, true)
Watcher代碼:

 class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function, // 回調函數
    options?: ?Object,
    isRenderWatcher?: boolean // render時實例的Watcher
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy
    this.deps = [] // 依賴列表
    this.newDeps = [] // 新的依賴列表
    this.depIds = new Set() // 依賴ids
    this.newDepIds = new Set() // 新的依賴ids
    // parse expression for getter
    // 將表達式 expOrFn包裝爲getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   * 源碼其實已經給出了註釋,這裏是進行render和依賴收集
   */
  get () {
    pushTarget(this) // 爲Dep.target賦值爲當前Watcher實例
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 這裏是render函數
    } catch (e) {
      ...
      
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget() // 彈出Deptarget
      this.cleanupDeps() // 本次添加的依賴落入this.deps,同時清空this.newDeps
    }
    return value
  },
  addDep (dep: Dep) { // 將Dep實例添加至this.newDeps隊列中,這裏的Dep實例產生自通過defineReactive爲data屬性定義getter/setter時,也就是說這裏的Dep實例對應一個data屬性
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  ...
  
}

從上面的代碼可以看出,響應式相關的核心在於所謂的“依賴收集”,也就是在render函數執行的過程中勢必會對頁面渲染需要的data屬性進行讀取,這就觸發了響應data屬性的getter,還記得之前省略掉的observe函數中執行defineReactive函數時有關data屬性getter函數相關的代碼嗎?

defineReactive (
  obj: Object, // vm instance of Vue
  key: string, // '$attr' 等屬性名
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  ...
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 在$mount函數中new Watcher進行依賴收集的時候已經爲Dep.target賦值爲Watcher實例
        dep.depend() // 這裏的Dep實例對應當前data屬性,此處會將當前dep實例放入watcher的依賴列表中
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // setter相關代碼
      ...
      
    }
  })
 }
 
 // dep.depend 代碼:
...
  depend () {
    if (Dep.target) { // 此處Dep.target已被賦值爲Watcher實例
      Dep.target.addDep(this)
    }
  }
...

頁面首次渲染時的小結

頁面的首次渲染基本上包含上述兩個大的過程,這裏先主要基於data進行討論
new Vue(options)中主要做了:

  1. 將data函數轉爲data對象賦值給vm._data,此時訪問data屬性可以通過vm._data[key];
  2. 通過defineProperty進行一層代理,訪問vm[key] 將返回vm._data[key],方便使用;
  3. 遍歷vm._data,執行defineReactive函數,爲vm._data的每一個屬性添加getter/setter,在getter中進行依賴收集,在setter進行通知響應;

$mount中主要做了:

  1. 編譯模板/生成渲染render函數;
  2. 通過實例Watcher對象,在執行render函數時,頁面渲染所用到的data屬性會被訪問,從而觸發vm._data[key]的getter,

在getter中將當前屬性對應的dep實例添加至Watcher實例的deps列表中,同時將Watcher實例添加進dep的subs觀察者列表中;

當data屬性值發生變化時

爲什麼data屬性變化了,頁面會重新渲染得到更新呢?前面做了很多鋪墊,接下來看一下data屬性的變更會進行哪些操作
還記得前面提到得通過defineReactive函數爲vm._data[key]設置得setter嗎?當data變化時會觸發該setter

    ...
    
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) { // 如果更新前後值相同,則直接返回
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal) // 如果newVal爲引用類型,則對其屬性也進行劫持
      dep.notify() // 這裏纔是屬性更新觸發操作得核心,它會通知Watcher進行相應得更新
    }
    
    ...
    
    // dep.notify方法
    
    ...
    notify () {
      const subs = this.subs.slice() // 這裏存放的是觀察者Watcher列表
      for (let i = 0, l = subs.length; i < l; i++) { // 通知每一個Watcher,執行其update方法,進行相應更新
        subs[i].update()
      }
    }
    ...
    
    // Watcher.prototype.update方法
    
    ...
    
    update () {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) { // 同步更新
        this.run()
      } else {
        queueWatcher(this) // 這個方法是nextTick中去執行Watcher.prototype.run方法,也就是說data屬性更新觸發setter然後通知Watcher去update這個過程通常並非同步執行的,而是會先被放入一個隊列,異步執行,落地到我們使用中:我們不用擔心同時修改多個data屬性帶來嚴重的性能問題,因爲其觸發的更新並非同步執行的;還有一點是Watcher.prototype.run方法中會執行get方法(還記得在首次渲染進行依賴收集的時候有這個方法嗎?)該方法中會執行render進行vnode生成,當然會訪問到data中的屬性,這樣就是一個依賴更新的過程,是不是一個閉環?
      }
    }
  
    ...
    

queueWatcher(this)
這個方法是nextTick中去執行Watcher.prototype.run方法,也就是說data屬性更新觸發setter然後通知Watcher去update這個過程通常並非同步執行的,而是會先被放入一個隊列,異步執行,落地到我們使用中:我們不用擔心同時修改多個data屬性帶來嚴重的性能問題,因爲其觸發的更新並非同步執行的;
還有一點是Watcher.prototype.run方法中會執行get方法(還記得在首次渲染進行依賴收集的時候有這個方法嗎?)該方法中會執行render進行vnode生成,當然會訪問到data中的屬性,這樣就是一個依賴更新的過程,是不是一個閉環?
另外不能忽略的一點是,在這個方法執行中還會觸發updated 鉤子函數,當然這裏不做深入研究,只做一個大致瞭解,因爲Vue中的細節很多,但是它不影響我們瞭解主要流程。

最後

放兩張在debugg源碼時寫的兩張圖,只有自己能看懂當初想到哪裏。。。
image.png
image.png
THE END

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