你不知道的Vue響應式原理

鈞嘢嘢發表於2018-02-02

文章首發於github Blog

本文根據Vue原始碼v2.x進行分析。這裡只梳理最原始碼中最主要的部分,略過非核心的一些部分。響應式更新主要涉及到WatcherDepObserver這幾個主要類。

watcher-dep-observer

本文主要弄清楚以下幾個容易搞混的問題:

  • WatcherDepObserver這幾個類之間的關係?
  • Dep中的 subs 儲存的是什麼?
  • Watcher中的 deps 儲存的是什麼?
  • Dep.target 是什麼,該值是何處賦值的?

本文直接從新建Vue例項入手,一步一步揭開Vue的響應式原理,假設有以下簡單的Vue程式碼:

var vue = new Vue({
    el: "#app",
    data: {
        counter: 1
    },
    watch: {
        counter: function(val, oldVal) {
            console.log('counter changed...')
        }
    }
})
複製程式碼

1. Vue例項初始化

從Vue的生命週期可知,首先進行init初始化操作,這部分程式碼在instance/init.js中。

src/core/instance/init.js

initLifecycle(vm) // vm生命週期相關變數初始化操作
initEvents(vm) // vm事件相關初始化
initRender(vm) // 模板解析相關初始化
callHook(vm, 'beforeCreate') // 呼叫beforeCreate鉤子函式
initInjections(vm) // resolve injections before data/props 
initState(vm) // vm狀態初始化(重點在這裡)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 呼叫created鉤子函式
複製程式碼

上述原始碼中的initState(vm)是要研究的重點,裡面實現了propsmethodsdatacomputedwatch的初始化操作。這裡根據上述例子,重點看datawatch,原始碼位置在instance/state.js

src/core/instance/state.js

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) // 對vm的data進行初始化,主要是通過Observer設定對應getter/setter方法
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  // 對新增的watch進行初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製程式碼

2. initData

Vue例項為它的每一個data都實現了getter/setter方法,這是實現響應式的基礎。關於getter/setter可檢視MDN web docs。 簡單來說,就是在取值this.counter的時候,可以自定義一些操作,再返回counter的值;在修改值this.counter = 10的時候,也可以在設定值的時候自定義一些操作。initData(vm)的實現在原始碼中的instance/state.js

src/core/instance/state.js

while (i--) {
	...
    // 這裡將data,props,methods上的資料全部代理到vue例項上
	// 使得vm.counter可以直接訪問
}
// 這裡略過上面的程式碼,直接看最核心的observe方法
// observe data
observe(data, true /* asRootData */)

複製程式碼

這裡observe()方法將data變成可觀察的,為什麼說是可觀察的?主要是實現了getter/setter方法,讓Watcher可以觀察到該資料的變化。下面看看observe的實現。

src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value) // 重點在這裡,響應式的核心所在
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
複製程式碼

這裡只關注new Observer(value),這是該方法的核心所在,通過Observer類將vue的data變成響應式。 根據我們的例子,此時入參value的值是{ counter: 1 }。 下面就具體看看Observer類。

3. Observer

首先看看該類的構造方法,new Observer(value)首先執行的是該構造方法。作者的註釋說了,Observer Class將每個目標物件的鍵值(即data中的資料)轉換成getter/setter形式,用於進行依賴收集和通過依賴通知更新。

src/core/observer/index.js

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value) // 遍歷data物件中{counter : 1, ..} 中的每個鍵值(如counter),設定其setter/getter方法。
    }
  }

  ...
}
複製程式碼

這裡最核心的就是this.walk(value)方法,this.observeArray(value)是對陣列資料的處理,實現對應的變異方法,這裡先不考慮。

繼續看walk()方法,註釋中已說明walk()做的是遍歷data物件中的每一設定的資料,將其轉為setter/getter

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
複製程式碼

那麼最終將對應資料轉為getter/setter的方法就是defineReactive()方法。從方法命名上也容易知道該方法是定義為可響應的,結合最開始的例子,這裡呼叫就是defineReactive(...)如圖所示:

defineReactive

原始碼如下:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // dep 為當前資料的依賴例項
  // dep 維護著一個subs列表,儲存依賴與當前資料(此時是當前資料是counter)的觀察者(或者叫訂閱者)。觀察者即是Watcher例項。
  const dep = new Dep() ---------------(1)

  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 = !shallow && observe(val)
  
  // 定義getter與setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      
      // 這裡在獲取值之前先進行依賴收集,如果Dep.target有值的話。
      if (Dep.target) {    -----------------(2)
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      
      // 依賴收集完後返回值
      return value
    },
    
    ...
}
複製程式碼

先看getter方法,該方法最重要的有兩處。

  1. 為每個data宣告一個dep例項物件,隨後dep就被對應的data給閉包引用了。舉例來說就是每次對counter取值或修改時,它的dep例項都可以訪問到,不會消失。
  2. 根據Dep.target來判斷是否收集依賴,還是普通取值。這裡Dep.target的賦值後面再將,這裡先知道有這麼一回事。

然後再看下setter方法,原始碼如下:

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 = !shallow && observe(newVal)
  
  // 最重要的是這一步,即通過dep例項通知觀察者我的資料更新了
  dep.notify()
}
複製程式碼

到這裡基本上Vue例項data的初始化就基本結束,通過下圖回顧下initData的過程:

initData flow

隨後要進行的是watch的初始化:

src/core/instance/state.js

export function initState (vm: Component) {
  ...
  
  if (opts.data) {
    initData(vm) // 對vm的data進行初始化,主要是通過Observer設定對應getter/setter方法
  } 
  
  // initData(vm) 完成後進行 initWatch(..)
  ...
  
  // 對新增的watch進行初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製程式碼

4. initWatch

這裡initWatch(vm, opts.watch)對應到我們的例子中如下所示:

initWatch

initWatch原始碼如下:

src/core/instance/state.js

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    // handler 是觀察物件的回撥函式
    // 如例子中counter的回撥函式
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
複製程式碼

createWatcher(vm, key, handler)是根據入參構建Watcher例項資訊,原始碼如下:

function createWatcher (
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 判斷是否是物件,是的話提取物件裡面的handler方法
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 判斷handler是否是字串,是的話說明是vm例項上的一個方法
  // 通過vm[handler]獲取該方法
  // 如 handler='sayHello', 那麼handler = vm.sayHello
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  
  // 最後呼叫vm原型鏈上的$watch(...)方法建立Watcher例項
  return vm.$watch(keyOrFn, handler, options)
}
複製程式碼

$watch是定義在Vue原型鏈上的方法,原始碼如下:

core/instance/state.js

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // 建立Watcher例項物件
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    
    // 該方法返回一個函式的引用,直接呼叫該函式就會呼叫watcher物件的teardown()方法,從它註冊的列表中(subs)刪除自己。
    return function unwatchFn () {
      watcher.teardown()
    }
  }
複製程式碼

經過一系列的封裝,這裡終於看到了建立Watcher例項物件了。下面將詳細講解Watcher類。

5. Watcher

根據我們的例子,new Watcher(...)如下圖所示:

newWatcher

首先執行Watcher類的構造方法,原始碼如下所示,省略了部分程式碼:

core/observer/watcher.js

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
  	 ...
    this.cb = cb // 儲存傳入的回撥函式
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = [] // 儲存觀察資料當前的dep例項物件
    this.newDeps = []  // 儲存觀察資料最新的dep例項物件
    this.depIds = new Set()
    this.newDepIds = new Set()

    // parse expression for getter
    // 獲取觀察物件的get方法
    // 對於計算屬性, expOrFn為函式
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
    // 通過parsePath方法獲取觀察物件expOrFn的get方法
      this.getter = parsePath(expOrFn)
      ...
    }
    
    // 最後通過呼叫watcher例項的get()方法,
    // 該方法是watcher例項關聯觀察物件的關鍵之處
    this.value = this.lazy
      ? undefined
      : this.get()
  }
複製程式碼

parsePath(expOrFn)的具體實現方法如下:

core/util/lang.js

/**
 * Parse simple path.
 */
const bailRE = /[^\w.$]/ // 匹配不符合包含下劃線的任意單詞數字組合的字串
export function parsePath (path: string): any {
  // 非法字串直接返回
  if (bailRE.test(path)) {
    return
  }
  // 舉例子如 'counter'.split('.') --> ['counter']
  const segments = path.split('.')
  // 這裡返回一個函式給this.getter
  // 那麼this.getter.call(vm, vm),這裡vm就是返回函式的入參obj
  // 實際上就是呼叫vm例項的資料,如 vm.counter,這樣就觸發了counter的getter方法。
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
複製程式碼

這裡很巧妙的返回了一個方法給this.getter, 即:

this.getter = function(obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
}
複製程式碼

this.getter將在this.get()方法內呼叫,用來獲取觀察物件的值,並觸發它的依賴收集,這裡即是獲取counter的值。

Watcher構造方法的最後一步,呼叫了this.get()方法,該方法原始碼如下:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // 該方法實際上是設定Dep.target = this
    // 把Dep.target設定為該Watcher例項
    // Dep.target是個全域性變數,一旦設定了在觀察資料中的getter方法就可使用了
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 呼叫觀察資料的getter方法
      // 進行依賴收集和取得觀察資料的值
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // 此時觀察資料的依賴已經收集完
      // 重置Dep.target=null
      popTarget()
      // 清除舊的deps
      this.cleanupDeps()
    }
    return value
  }
複製程式碼

關鍵步驟已經在上面程式碼中註釋了,下面給出一個Observer,Watcher類之間的關聯關係,圖中還是以我們的例子進行描述:

Observer-Watcher-rel

  • 紅色箭頭:Watcher類例項化,呼叫watcher例項的get()方法,並設定Dep.target為當前watcher例項,觸發觀察物件的getter方法。
  • 藍色箭頭:counter物件的getter方法被觸發,呼叫dep.depend()進行依賴收集並返回counter的值。依賴收集的結果:1.counter閉包的dep例項的subs新增觀察它的watcher例項w12. w1的deps中新增觀察物件counter的閉包dep
  • 橙色箭頭:當counter的值變化後,觸發subs中觀察它的w1執行update()方法,最後實際上是呼叫w1的回撥函式cb。

Watcher類中的其他相關方法都比較直觀這裡就直接略過了,詳細請看Watcher類的原始碼。

6. Dep

上圖中關聯Observer和Watcher類的是Dep,那麼Dep是什麼呢?

Dep可以比喻為出版社,Watcher好比讀者,Observer好比東野圭吾相關書籍。比如讀者w1對東野圭吾的白夜行(我們例子中的counter)感興趣,讀者w1一旦買了東野圭吾的書,那麼就會自動在這本書的出版社(Dep例項)裡面註冊填w1資訊,一旦該出版社有了東野圭吾這本書最新訊息(比如優惠折扣)就會通知w1。

現在看下Dep的原始碼:

core/observer/dep.js

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    // 儲存觀察者watcher例項的陣列
    this.subs = []
  }

  // 新增觀察者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // 移除觀察者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // 進行依賴收集
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 通知觀察者資料有變化
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
複製程式碼

Dep類比較簡單,對應方法也非常直觀,這裡最主要的就是維護了儲存有觀察者例項watcher的一個陣列subs

7. 總結

到這裡,主要的三個類都研究完了,現在基本可以回答文章開頭的幾個問題了。

Q1:WatcherDepObserver這幾個類之間的關係?

A1:Watcher是觀察者觀察經過Observer封裝過的資料,DepWatcher和觀察資料間的紐帶,主要起到依賴收集和通知更新的作用。

Q2:Dep中的subs儲存的是什麼?

A2: subs儲存的是觀察者Watcher例項。

Q3:Watcher中的deps儲存的是什麼?

A3:deps儲存的是觀察資料閉包中的dep例項。

Q4:Dep.target是什麼, 該值是何處賦值的?

A4:Dep.target是全域性變數,儲存當前的watcher例項,在new Watcher()的時候進行賦值,賦值為當前Watcher例項。

8. 擴充套件

這裡看一個計算屬性的例子:

var vue = new Vue({
    el: "#app",
    data: {
        counter: 1
    },
    computed: {
        result: function() {
            return 'The result is :' + this.counter + 1;
        }
    }
})
複製程式碼

這裡的result的值是依賴與counter的值,通過result更能體現出Vue的響應式計算。計算屬性是通過initComputed(vm, opts.computed)初始化的,跟隨原始碼追蹤下去會發現,這裡也有Watcher例項的建立:

core/instance/state.js

  watchers[key] = new Watcher(
    vm,  // 當前vue例項
    getter || noop,  // result對應的方法 function(){ return 'The result is :' + this.counter + 1;}
    noop, // noop是定義的一個空方法,這裡沒有回撥函式用noop代替
    computedWatcherOptions // { lazy: true }
  )
複製程式碼

示意圖如下所示:

computed-watcher

這裡計算屬性result因為依賴於this.counter,因此設定一個watcher用來觀察result的值。隨後通過definedComputed(vm, key, userDef)來定義計算屬性。在計算獲取result的時候,又會觸發this.countergetter方法,這樣使得result的值依賴於this.counter的值。

definedComputed

最後會為計算屬性result定義它的setter/getter屬性:Object.defineProperty(target, key, sharedPropertyDefinition)。更詳細資訊請檢視原始碼。

9. 參考

  1. vue 官方文件
  2. vue 原始碼
  3. vue 原始碼解析

相關文章