Vue2.0源碼閱讀筆記(二):響應式原理

  Vue是數據驅動的框架,在修改數據時,視圖會進行更新。數據響應式系統使得狀態管理變的簡單直接,在開發過程當中減小與DOM元素的接觸。而深刻學習其中的原理十分有必要,可以迴避一些常見的問題,使開發變的更爲高效。
react

1、實現簡單的數據響應式系統

  Vue使用觀察者模式(又稱發佈-訂閱模式)加數據劫持的方式實現數據響應式系統,劫持數據時使用 Object.defineProperty 方法將數據屬性變成訪問器屬性。Object.defineProperty 是 ES5 中一個沒法 shim 的特性,所以Vue 不支持 IE8 以及更低版本瀏覽器。
  Vue源碼中對數據響應式系統的實現比較複雜,在深刻學習這部分源碼以前,先實現一個較爲簡單的版本更有助於後續的理解。代碼以下所示:
express

let uid = 0 

// 容器構造函數
function Dep() {
    // 收集觀察者的容器
    this.subs = []
    this.id = uid++
}

Dep.prototype = {
    // 將當前觀察者收集到容器中
    addSub: function(sub) {
        this.subs.push(sub)
    },

    // 收集依賴,調用觀察者的addDep方法
    depend: function() {
        if(Dep.target){
            Dep.target.addDep(this)
        }
    },

    // 遍歷執行容器中各觀察者的run方法,以執行回調
    notify: function() {
        this.subs.forEach(sub => {
            sub.run()
        })
    }
}

// 初始化當前觀察者對象爲空
Dep.target = null

// 數據劫持函數
function observe(data) {
    // 防止重複對數據作劫持處理
    if(data.__ob__) return
    let keys = Object.keys(data)
    keys.forEach(key => {
        let val = data[key]
        let dep = new Dep()

        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                dep.depend()
                return val
            },
            set: function(newValue) {
                if((newValue !== newValue) || (newValue === val)){
                    return
                } else {
                    val = newValue
                    dep.notify()
                }
            }
        })
    });
    // 在被劫持的數據上定義一個不可遍歷的內部屬性
    Object.defineProperty(data, '__ob__',{
        configurable: true,
        enumerable: false,
        value: true,
        writable: true
    })
}

// 觀察者構造函數
function Watcher(data, exp, callback) {
    this.cb = callback
    this.deps = {}
    this.exp = exp
    // 獲取獲得數據的函數
    this.getter = this.parseExp(exp.trim())
    this.data = data
    this.value = this.get()
}

Watcher.prototype = {
    run: function() {
        let value = this.get()
        let oldValue = this.value

        if(value !== oldValue){
            this.value = value
            this.cb.call(null, value, oldValue)
        }
    },

    addDep: function(dep) {
        // 防止收集重複數據
        if(!this.deps.hasOwnProperty(dep.id)){
            dep.addSub(this)
            this.deps[dep.id] = dep
        }
    },

    get: function() {
        // 將實例對象變爲當前觀察者對象
        Dep.target = this
        // 讀取數據,從而觸發數據get方法
        let value = this.getter.call(this.data, this.data)
        // 依賴收集完畢,當前觀察者對象置爲空
        Dep.target = null

        return value
    },

    // 經過形如‘a.b’的字符串形式獲取數據值
    parseExp: function(exp) {
        if(/[^\w.$]/.test(exp)) return

        let exps = exp.split('.')

        return function(obj) {
            return obj[exps[1]]
        }
    }
}

// 監測函數
function $watch(data, exp, cb) {
    observe(data)
    new Watcher(data, exp, cb)
}
複製代碼

  首先使用監測函數 $watch 來測試一下,代碼以下:
數組

let a = {
    b: 100,
    c: 200
}

const callback = function(newValue, oldValue) {
    console.log(`新值爲:${newValue},舊值爲:${oldValue}`)
}

$watch(a, 'a.b', callback)
$watch(a, 'a.c', callback)

a.b = 101 
a.c = 201
複製代碼

  輸出結果:
瀏覽器

新值爲:101,舊值爲:100
新值爲:201,舊值爲:200
複製代碼

  上述代碼的邏輯結構圖以下所示:
服務器

響應式原理簡單實現
  響應式系統能夠分爲三個階段:數據劫持(圖中藍線表示)、收集依賴(圖中紅線表示)、觸發依賴(圖中綠線表示)。

一、數據劫持

  在數據劫持函數 observe 中,首先檢測對象中是否存在不可遍歷的屬性 __ob__ 。若是存在,則表示該對象已經轉化爲響應式的;若是不存在,在數據轉化以後添加上 __ob__ 屬性。
  而後循環遍歷對象的屬性,將數據屬性變成訪問器屬性,每一個訪問器屬性經過閉包引用一個 Dep 實例 dep 。在讀取屬性時,會觸發 get 方法,而後調用 dep.depend() ,收集依賴。在爲屬性設置新值時,會觸發 set 方法,而後調用 dep.notify() ,觸發依賴。經過 observe 方法僅僅是改造對象屬性,對象屬性的 get 和 set 此時並無觸發。
閉包

二、收集依賴

  經過 Watcher 函數爲響應式數據添加依賴。所謂依賴,是指當數據變動時須要觸發的回調函數。
  Watcher 函數實例化時經過調用 get() 方法先將實例對象設置成當前觀察者對象,而後讀取數據,數據的 get 方法被調用,接着調用數據閉包引用的數據 dep 的 depend() 方法。
  在 depend() 中,會將 dep 傳入當前觀察者的 addDep() 方法。在 addDep() 方法中,首先防止重複收集依賴,而後調用 dep.addSub() 方法將當前觀察者添加到 dep 的subs 屬性中,完成依賴的收集。
app

三、觸發依賴

  觸發依賴是指在數據發生改變時,將數據閉包引用的變量 dep 中存儲的觀察者對象的回調依次執行。
  當數據改變時,會調用 set 方法,而後執行 dep.notify() 方法,該方法遍歷數組 dep.subs ,執行數組中每一個觀察者對象的 run() 方法。Watcher 實例的 run() 方法將數據改變前、改變後的值傳入回調函數中執行,完成依賴的觸發。
框架

四、存在的問題

  上述代碼實現了一個最簡單的數據響應式系統,可是存在不少的問題,好比:若是數據類型爲數組怎麼辦?若是對象的屬性自己就是訪問器屬性呢?刪除對象屬性怎麼觸發依賴?等等問題。
  2019年Vue做者尤雨溪接受訪問時說:
異步

「我當時一方面是想本身實現一個簡單的框架練練手,另外一方面是想嘗試一下用 ES5 的Object.defineProperty 實現數據變更偵測。」函數

  隨着時間的推移,Vue功能愈來愈完善,早已不是當初那用來練手的框架。在瞭解最基礎的實現思路以後,讓咱們深刻Vue源碼中關於數據響應式系統的實現。

2、observe

  observe 函數的功能是劫持數據,改造數據,使數據被訪問時可以收集依賴,數據改變時可以觸發依賴
  observe 函數代碼以下所示:

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 (
    shouldObserve && !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
複製代碼

  首先,保證處理的數據類型是對象;若是數據對象上有 __ob__ 屬性,且該屬性是 Observer 的實例,說明數據已是響應式的,再也不重複處理數據,直接返回該屬性。
  而後判斷是否符合如下五個條件:shouldObserve 變量爲 true、不是服務器端渲染、數據是純對象或者數組、數據是可擴展的、數據不是 Vue 實例。同時知足這五個條件,會經過函數 Observer 處理數據,並返回一樣的值。也就是說函數 observe 要麼返回 undefined ,要麼返回 Observer 的實例。
  Observer 函數的 constructor 方法源碼以下所示:

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)
  }
}
複製代碼

  Observer 的實例對象上有三個屬性:Dep 的實例屬性 dep 、指向被劫持數據的屬性 value、vmCount。而被劫持數據上會添加 __ob__ 屬性,該屬性指向 Observer 的實例對象,該實例對象與被劫持數據爲循環引用。
  observe 函數處理的數據分爲兩種:純對象、數組。雖然數組也是對象,可是有它的特殊性:數組的索引是非響應式的。observe 函數對這兩種類型數據有不一樣的處理方式。

一、處理純對象

  若是被劫持數據爲純對象,則通過 walk 方法處理。

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}
複製代碼

  方法 walk 是將對象上每一個屬性都調用 defineReactive 方法處理。

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  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
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set

  let childOb = !shallow && 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 = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製代碼

(一)、純對象數據劫持原理

  從 defineReactive 方法的代碼能夠看到:劫持數據的原理就是將數據屬性轉變成訪問器屬性,若是數據自己就是訪問器屬性,則在重寫的 get、set 方法中調用以前對應的 get、set 方法。
  每一個屬性閉包引用一個 Dep 的實例 dep ,在 get 方法中經過 dep.depend() 來收集依賴,在 set 方法中經過 dep.notify() 來觸發依賴。

(二)、若是數據屬性值爲純對象或者數組

  若是純對象數據的屬性值爲純對象或者數組該怎麼處理?想要弄清楚這個問題首先要明白下面代碼中的 childOb 值究竟是什麼。

let childOb = !shallow && observe(val)
複製代碼

  shallow 是 defineReactive 方法的形參,含義爲是否深度觀測數據,當不傳該參數時,默認爲深度觀測。
  observe 方法處理數據爲非對象時返回 undefined ,處理對象時返回 Observer 的實例。childOb 的值存在,則代表處理的屬性值 val 爲純對象或者數組,且childOb 爲 Observer(val) 的實例。由於循環引用的存在,childOb 與 val.__ob__ 相等。
  在屬性值爲對象的狀況下,當觸發依賴時處理比較簡單,僅僅只是將新值經過 observe 方法遞歸處理,使其變成響應式數據。
  而在 get 方法中收集依賴則比較麻煩,首先執行以下代碼:

childOb.dep.depend()
複製代碼

  也就是執行下列代碼:

val.__ob__.dep.depend()
複製代碼

  前面說過,屬性依賴的收集是存儲在閉包引用的 dep 變量中的,那麼每一個對象數據的 __ob__ 屬性的 dep 是用來作什麼的?這裏爲何會重複收集一遍依賴呢?其實,主要是由於這兩個dep 觸發的時機不一樣,閉包引用的 dep 是在屬性值改變時使用的,對象__ob__ 屬性的 dep 是在對象引用改變時使用的。在下面講 Vue.set 與 Vue.delete 的原理時將詳細說明。

二、處理數組

  數組中有七種實例方法會改變數組自身的值:push、pop、shift、unshift、splice、sort 與 reverse。在對象類型是數組的狀況下,在數組被讀取時收集依賴,在用戶使用這七種方法改變數組時觸發依賴。

(一)、收集依賴

  數組沒法將索引變成訪問器屬性,因此不能像純對象同樣利用每一個屬性的閉包來收集和觸發依賴。在處理數組時,會先經過 observe() 處理,這樣數組上就添加了 __ob__ 屬性,指向 Observer 的實例對象。在 __ob__.dep 中收集依賴。
  有一點比較特殊,在數組收集依賴時有以下代碼:

if (Array.isArray(value)) {
    dependArray(value)
}
複製代碼

  dependArray 遞歸函數代碼以下所示:

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
複製代碼

  這段代碼的功能是:在數組中有純對象、數組時,除了將依賴收集到數組的 __ob__.dep 屬性中,還要遞歸的收集到包含的純對象、數組的 __ob__.dep 屬性中
  爲何要這樣處理?這是由於這樣一個前提:數組中任何值的改變都算做是數組的改變,只要依賴了該數組,就等價於依賴了數組中的每個元素。

{
  a:[ {b: 1} , [1], 2 ]
}
複製代碼

  如上面的例子所示,在訪問數組 a 時會添加 __ob__ 屬性,在 a.__ob__.dep 中存儲對 a 的依賴。當經過改變數組自身的實例方法操做 a 時,會調用 a.__ob__.dep.notify() 來觸發依賴;當經過 Vue.set() 來改變 a 的某個值時,會轉化成實例方法調用的形式,而後調用 a.__ob__.dep.notify() 來觸發依賴;。
  可是,若是改變 a[0].b 的值,因爲在對象 a[0] 中並無收集對數組 a 的依賴,則沒法觸發 a 的依賴。這就違背了數組中任何值的改變都算做是數組的改變這一前提。
  所以 Vue 經過遞歸調用 dependArray 方法來將對數組依賴收集到數組包含的每個對象中,這樣數組中任何數值的改變都會觸發該數組的依賴。

(二)、觸發依賴

  爲了可以在經過實例方法改變數組時可以觸發依賴,Vue重寫了能夠改變數組自身的方法。以下代碼所示:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    ob.dep.notify()
    return result
  })
})
複製代碼

  arrayMethods 對象的原型 爲 Array.prototype ,在該對象上添加通過處理後的這七種方法。重寫的實例方法主要包含三個功能:

一、調用原生實例方法。
二、當經過push、unshift、splice添加數據時,將新添加的數據變成響應式的。
三、當數組改變時,觸發依賴。

  其中,觸發依賴的原理須要注意一下,與以前說對象引用改變時觸發自身屬性中的 dep 同樣,數組自身發生改變,觸發的也是經過自身 __ob__ 屬性的 dep 的 notify() 來觸發依賴的。
  ES6新增了對象的 __proto__ 屬性,用來讀取或設置當前對象的prototype對象,兼容到IE11。Vue在處理數組時,若是數組擁有 __proto__ 屬性,則直接將該屬性指向 arrayMethods 對象,即修改數組的原型對象。這樣調用七種改變數組自己的方法時,會調用 arrayMethods 對象的方法,從而實現觸發依賴的功能。以下圖所示:

處理數組原理
  Vue框架兼容到 IE9 ,對於 IE9 和 IE10 瀏覽器來講,對象中沒有 __ob__ 屬性。Vue對於這種狀況的處理方式是:直接將 arrayMethods 對象上的方法拷貝到數組自身上,且這七種方法是不可枚舉的。以下代碼所示:

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
複製代碼

三、Vue.set與Vue.delete

  總結上面講述的依賴收集和觸發的狀況以下:

一、若是是對象,將對象的屬性轉化爲訪問器屬性,訪問屬性時收集依賴存儲到屬性閉包引用的變量 dep 中,更改屬性時觸發閉包引用的變量 dep 中的依賴。
二、若是是數組,在讀取數組時添加 __ob__ 對象屬性,在該對象的 dep 屬性中收集依賴。而後重寫可以改變自身的七種實例方法,在調用這些實例方法時,觸發 __ob__.dep 中存儲的依賴。

  這兩種狀況就致使了官網列表渲染注意事項 提到的問題:

因爲 JavaScript 的限制,Vue 不能檢測如下變更的數組:
一、當你利用索引直接設置一個項時,例如:vm.items[indexOfItem] = newValue
二、當你修改數組的長度時,例如:vm.items.length = newLength

仍是因爲 JavaScript 的限制,Vue 不能檢測對象屬性的添加或刪除。

  Vue 提供 Vue.set()、Vue.delete() 兩個全局 API 以及 vm.set()、vm.delete() 兩個實例方法來解決上述問題。

(一)、Vue.set

  Vue.set() 與 vm.$set() 都是調用 src/core/observer/index.js 中的 set 方法。

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
複製代碼

  由上述代碼可知,set 方法的功能以下:

一、若target爲數組,key爲有效索引,則先判斷是否調整數組大小,再調用splice方法觸發依賴。
二、若target爲純對象,key是對象是存在的屬性,則直接改變key值,進而調用屬性set方法觸發依賴。
三、若target不是響應式的,則直接往對象上添加key屬性,target上有key屬性則直接覆蓋。
四、若target是響應式的,且自己沒有key屬性,則經過defineReactive方法將值轉成響應式的添加到target上,而後經過target.__ob__.dep.notify()觸發依賴。

(二)、Vue.delete

  Vue.delete() 與 vm.$delete() 都是調用 src/core/observer/index.js 中的 del 方法。

export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}
複製代碼

  由上述代碼可知,del 方法的功能以下:

一、若target爲數組,key爲有效索引,則調用splice方法完成刪除操做,而且觸發依賴。
二、若target爲純對象,key屬性不存在,則不執行刪除操做,直接返回undefined。
三、若target爲純對象,key屬性存在,則刪除該屬性,而後經過target.__ob__.dep.notify()觸發依賴。

3、Dep

  Dep 函數的主要功能是生成被觀察數據存放觀察者的容器,其靜態屬性target指向當前要收集的觀察者
  Dep 函數以下所示:

export default class Dep {
  constructor () {
    this.id = uid++
    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 () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
複製代碼

  實例屬性subs爲存放觀察者的數組,depend()收集依賴、notify()觸發依賴。有一點須要注意:depend 方法調用當前觀察者對象的 addDep 方法,addDep 方法又調用 Dep 實例的 addSub 方法來將 Dep.target 存入 subs 中,爲何不直接將當前要收集的觀察者 Dep.target push到subs中?
  這樣作的緣由主要有三點:避免重複收集依賴、方便記錄被觀察數據變化先後的值、觀察者對象中保存着被觀察數據的數量。若是僅僅是爲了不重複收集依賴,能夠利用觀察者對象的id,刪除重複觀察者對象來實現。
  另外,Dep.target 的值不是簡單的將當前觀察者賦值完成的,而是由 pushTarget 來實現,在賦值以前先存儲本來觀察者,當前觀察者被數據收集以後,經過 popTarget 來將 Dep.target 的值恢復到本來的觀察者對象。

4、Watcher

  Watcher 函數的主要功能是爲被觀察的數據提供依賴(回調函數)
  觀察者函數觀察數據的方式是讀取數據,數據通過 observe 函數處理後變成響應式的,在被讀取的過程當中可以存儲 Watcher 實例對象的引用,這就是收集依賴的過程。
  當被觀察數據發生改變時,數據會遍歷存儲的 Watcher 實例對象引用,來分別執行各 Watcher 實例對象上回調函數,這就是觸發依賴的過程。

一、概述

  Watcher 函數的 constructor 方法源碼以下所示:

constructor (vm: Component, expOrFn: string | Function,
    cb: Function, options?: ?Object, isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) { vm._watcher = this }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } 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 // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy ? undefined : this.get()
  }
複製代碼

  Watcher 函數接收五個參數:當前組件實例對象、被觀察的目標、被觀察數據發生改變後的回調函數、傳遞的選項、是否爲渲染函數的標識。
  觀察者對象的 vm 屬性指向當前組件的實例對象,組件實例對象上的 _watcher 屬性指向渲染函數的觀察者,而 _watchers 屬性則包含當前實例對象上的所有觀察者對象。
  根據傳入選項 options 的值來初始化觀察者對象上的五個值,其含義分別爲:

屬性deep:是否深度觀測數據,默認爲 false 。
屬性user:觀察者是否由開發者定義的,默認爲 false 。
屬性lazy:觀察者是否惰性求值,是內部爲實現計算屬性功能建立的觀察者,默認爲 false 。
屬性sync:數據變化時是否同步求值,默認爲 false 。
屬性before:鉤子函數,在數據變化以後、觸發更新以前調用。

  觀察者對象的 getter 方法是根據傳入 expOrFn 參數的類型來肯定的。若是傳入函數,getter 方法直接等於該函數;若是傳入字符串,getter 方法爲返回目標值的函數。getter 方法的功能就是可以讀取目標數據。
  在 constructor 方法的最後,會 lazy 屬性是否爲 true 來決定 value 的值,lazy 屬性只有是計算屬性的觀察者時才爲 true 。若是不是計算屬性的觀察者,會調用 get() 方法,並用 value 記錄返回值。
  get() 方法主要有兩個功能:讀取被觀察數據、返回數據值。當數據被讀取時,數據對應的的 dep.depend() 方法被調用,而後調用觀察者對象的 addDep() 方法, 繼而調用 dep.addSub() 方法,完成對當前依賴的收集。
  當被觀察數據發生改變時,會調用 dep.notify() 方法,而後調用其中包含的每個觀察者對象的 update() 方法。在 update 中,若是不是計算屬性的觀察者,最終會調用 run() 方法。run() 方法先執行 get() 方法將獲取數據新值,而後將新舊值做爲參數調用回調函數。

二、避免收集重複依賴

  Watcher 函數中避免收集重複依賴主要依靠兩組屬性:newDeps 與 newDepIds、deps 與 depIds 。
  newDeps 與 newDepIds 是爲了不一次求值的過程當中收集重複依賴的。當 expOrFn 參數爲函數,且在函數中存在一個值屢次使用時,使用 newDeps 與 newDepIds 來避免重複收集該值的依賴。newDeps 存儲的是當前求值收集到的 Dep 實例對象
  deps 與 depIds 是爲了不屢次求值的過程當中收集重複依賴的。當被觀察數據改變,從新讀取數據時,經過這兩個屬性來避免再次收集依賴。deps 存儲的是上一次求值時收集的 Dep 實例對象
  在 get() 方法的 finally 部分中會調用 cleanupDeps() 方法。

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}
複製代碼

  cleanupDeps() 方法有兩個功能:

一、移除廢棄的觀察者。
二、在清空 newDepIds 與 newDeps 以前,分別賦值給 depIds 與 deps 。

  避免收集重複依賴的代碼在 addDep() 方法中。

addDep (dep: Dep) {
  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)
    }
  }
}
複製代碼

  在 addDep() 方法中,首先判斷當前id是否已經存在於 newDepIds 中,若是存在,則代表該依賴在本次求值時已經被收集,不用再重複收集。若是不存在,則將 id 添加到 newDepIds 中,將 dep 添加到 newDeps 中。
  接着判斷依賴是否在上次求值時被收集,若是是,也不用再重複收集。若是上次求值時依賴也沒有被收集,則收集依賴。

三、異步執行

  在觸發依賴的過程當中調用 update() 方法,該方法有三種狀況:做爲計算屬性的觀察者、指明要同步執行、默認異步執行。

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
複製代碼

  當選項 sync 爲 true 時,直接經過執行 run() 方法直接調用回調函數。而選項 sync 除非明確指定,默認是 false ,也就是說,默認對依賴的觸發是異步執行的。
  異步執行主要是爲了優化性能,例如當模板中的數據改變時,渲染函數會從新求值,完成從新渲染。若是同步執行,每次修改一個值都要從新渲染,在複雜的業務場景下可能會同時修改不少數據,屢次渲染會致使很嚴重的性能問題。
  若是異步執行,每次修改屬性的值以後並無當即從新求值,而是將須要執行更新操做的觀察者放入一個隊列中,隊列中沒有重複的觀察者對象,這樣就能達到優化性能的目的。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) { i-- }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}
複製代碼

  queueWatcher() 方法首先判斷觀察者隊列中是否含有要添加的觀察者,若是已經存在,則不作任何操做。flushing 變量爲隊列是否正在執行更新的標誌,若是隊列沒有執行則直接將觀察者對象存到隊列中,若是隊列正在執行更新,則須要保證觀察者的執行順序。
  變量 waiting 初始值爲 false,在執行 if 判斷以後變爲 true ,也就是說 nextTick(flushSchedulerQueue) 只會執行一次。nextTick() 方法比較複雜,在這裏能夠簡單理解成與 setTimeout(fn, 0) 功能相同,做用就是在下一事件循環開始時當即調用 flushSchedulerQueue ,從而將隊列中的觀察者統一執行更新。

5、總結

  Vue數據響應式系統整體來講分爲兩步:一、劫持數據;二、將數據改變要觸發的回調函數與數據關聯起來。
  observe 函數的功能就是劫持數據,讓數據在被讀取時收集依賴,在改變時觸發依賴。若是數據爲純對象,則將其屬性轉變成訪問器屬性;若是數據是數組類型,則經過重寫可以改變自身的方法來實現。對象添加、刪除屬性以及數組經過直接賦值的方式改變並不會觸發依賴,這時要使用 Vue.set()、Vue.delete() 方法來操做。
  Dep 函數是用來生成盛放依賴的容器。收集依賴最終都是收集到其實例對象的 subs 數組屬性中,觸發依賴最終操做時遍歷執行 subs 中的觀察者對象上的回調函數。
  Watcher 函數主要是將被觀察的數據與數據改變後要執行的回調函數關聯起來。爲了提高性能,在這個過程當中要避免數據收集的依賴有重複;當數據改變時要異步執行更新。

如需轉載,煩請註明出處:juejin.im/post/5cb6ee…

相關文章
相關標籤/搜索