vue資料繫結原始碼

陳鳳娟發表於2019-01-30

思路分析

資料的雙向繫結,就是資料變化了自動更新檢視,檢視變化了自動更新資料,實際上檢視變化更新資料只要通過事件監聽就可以實現了,並不是資料雙向繫結的關鍵點。關鍵還是資料變化了驅動檢視自動更新。

所有接下來,我們詳細瞭解下資料如何驅動檢視更新的。 資料驅動檢視更新的重點就是,如何知道資料更新了,或者說資料更新了要如何主動的告訴我們。可能大家都聽過,vue的資料雙向繫結原理是Object.defineProperty( )對屬性設定一個set/get,是這樣的沒錯,其實get/set只是可以做到對資料的讀取進行劫持,就可以讓我們知道資料更新了。但是你詳細的瞭解整個過程嗎? 先來看張大家都不陌生的圖:

vue資料繫結原始碼

  • Observe 類劫持監聽所有屬性,主要給響應式物件的屬性新增 getter/setter 用於依賴收集與派發更新
  • Dep 類用於收集當前響應式物件的依賴關係
  • Watcher 類是觀察者,例項分為渲染 watcher、計算屬性 watcher、偵聽器 watcher三種

介紹資料驅動更新之前,先介紹下面4個類和方法,然後從資料的入口initState開始按順序介紹,以下類和方法是如何協作,達到資料驅動更新的。

defineReactive

這個方法,用處可就大了。
我們看到他是給物件的鍵值新增get/set方法,也就是對屬性的取值和賦值都加了攔截,同時用閉包給每個屬性都儲存了一個Dep物件。
當讀取該值的時候,就把當前這個watcherDep.target)新增進他的dep裡的觀察者列表,這個watcher也會把這個dep新增進他的依賴列表。 當給設定值的時候,就讓這個閉包儲存的dep去通知他的觀察者列表的每一個watcher

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
      }
      
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製程式碼

Observer

什麼是可觀察者物件呢?
簡單來說:就是資料變更時可以通知所有觀察他的觀察者。
1、取值的時候,能把要取值的watcher(觀察者物件)加入它的dep(依賴,也可叫觀察者管理器)管理的subs列表裡(即觀察者列表);
2、設定值的時候,有了變化,所有依賴於它的物件(即它的dep裡收集到的觀察者watcher)都得到通知。

這個類功能就是把資料轉化成可觀察物件。針對Object型別就呼叫defineReactive方法迴圈把每一個鍵值都轉化。針對Array,首先是對Array經過特殊處理,使它可以監控到陣列發生了變化,然後對陣列的每一項遞迴呼叫Observer進行轉化。
對於Array是如何處理的呢?這個放在下面單獨說。

export class Observer {

  /**
   *如果是物件就迴圈把物件的每一個鍵值都轉化成可觀察者物件
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * 如果是陣列就對陣列的每一項做轉化
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
複製程式碼

Dep

這個類功能簡單來說就是管理資料的觀察者的。當有觀察者讀取資料時,儲存觀察者到subs,以便當資料變化了的時候,可以通知所有的觀察者去update,也可以刪除subs裡的某個觀察者。

export default class Dep {
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
    
  // 這個方法非常繞,Dep.target就是一個Watcher物件,Watcher把這個依賴加進他的依賴列表裡,然後呼叫dep.addSub再把這個Watcher加入到他的觀察者列表裡。
  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()
    }
  }
}
複製程式碼

Watcher

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // 省去了初始化各種屬性和option
    this.dirty = this.lazy // for lazy watchers
    // 解析expOrFn,賦值給this.getter
    // expOrFn也要明白他是什麼?
    // 當是渲染watcher時,expOrFn是updateComponent,即重新渲染執行render
    // 當是計算watcher時,expOrFn是計算屬性的計算方法
    // 當是偵聽器watcher時,expOrFn是watch屬性的取值表示式,可以去讀取要watch的資料,this.cb就是watch的handler屬性
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * 執行this.getter,同時重新進行依賴收集
   */
  get () {
    pushTarget(this)
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    if (this.deep) {
      // 對於deep的watch屬性,處理的很巧妙,traverse就是去遞迴讀取value的值,
      // 就會呼叫他們的get方法,進行了依賴收集
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

  /**
   * 不重複的把當前watcher新增進依賴的觀察者列表裡
   */
  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)
      }
    }
  }

  /**
   * 清理依賴列表:當前的依賴列表和新的依賴列表比對,存在於this.deps裡面,
   * 卻不存在於this.newDeps裡面,說明這個watcher已經不再觀察這個依賴了,所以
   * 要讓個依賴從他的觀察者列表裡刪除自己,以免造成不必要的watcher更新。然後
   * 把this.newDeps的值賦給this.deps,再把this.newDeps清空
   */
  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
  }

  /**
   * 當一個依賴改變的時候,通知它update
   */
  update () {
    if (this.lazy) {
      // 對於計算watcher時,不需要立即執行計算方法,只要設定dirty,意味著
      // 資料不是最新的了,使用時需要重新計算
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 排程watcher執行計算。
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
          this.cb.call(this.vm, value, oldValue)
      }
    }
  }

  /**
   * 對於計算屬性,當取值計算屬性時,發現計算屬性的watcher的dirty是true
   * 說明資料不是最新的了,需要重新計算,這裡就是重新計算計算屬性的值。
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * 把這個watcher所觀察的所有依賴都傳給Dep.target,即給Dep.target收集
   * 這些依賴。
   * 舉個例子:具體可以看state.js裡的createComputedGetter這個方法
   * 當render裡依賴了計算屬性a,當渲染watcher在執行render時就會去
   * 讀取a,而a會去重新計算,計算完了渲染watcher出棧,賦值給Dep.target
   * 然後執行watcher.depend,就是把這個計算watcher的所有依賴也加入給渲染watcher
   * 這樣,即使data.b沒有被直接用在render上,也通過計算屬性a被間接的是用了
   * 當data.b發生改變時,也就可以觸發渲染更新了
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
複製程式碼

綜上所述,就是vue資料驅動更新的方法了,下面是對整個過程的簡單概述: 每個vue例項元件都有相應的watcher物件,這個watcher是負責更新渲染的。他會在元件渲染過程中,把屬性記錄為依賴,也就是說,她在渲染的時候就把所有渲染用到的prop和data都新增進watcher的依賴列表裡,只有用到的才加入。同時把這個watcher加入進data的依賴的訂閱者列表裡。也就是watcher儲存了它都依賴了誰,data的依賴裡儲存了都誰訂閱了它。這樣data在改變時,就可以通知他的所有觀察者進行更新了。渲染的watcher觸發的更新就是重新渲染,後續的事情就是render生成虛擬DOM樹,進行diff比對,將不同反應到真實的DOM中。

queueWatcher

下面是Watcher的update方法,可以看的除了是計算屬性和標記了是同步的情況以外,全部都是推入觀察者佇列中,下一個tick時呼叫。也就是資料變化不是立即就去更新的,而是非同步批量去更新的。

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

複製程式碼

下面來看看queueWatcher方法

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

這裡使用了一個 has 的雜湊map用來檢查是否當前watcher的id是否存在,若已存在則跳過,不存在則就push到queue,佇列中並標記雜湊表has,用於下次檢驗,防止重複新增。因為執行更新佇列時,是每個watcher都被執行run,如果是相同的watcher沒必要重複執行,這樣就算同步修改了一百次檢視中用到的data,非同步更新計算的時候也只會更新最後一次修改。

nextTick(flushSchedulerQueue)把回撥方法flushSchedulerQueue傳遞給nextTick,一次非同步更新,只要傳遞一次非同步回撥函式就可以了,在這個非同步回撥裡統一批量的處理queue中的watcher,進行更新。

function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
  }

  resetSchedulerState()

}
複製程式碼

每次執行非同步回撥更新,就是迴圈執行佇列裡的watcher.run方法。

在迴圈佇列之前對佇列進行了一次排序:

  • 元件更新的順序是從父元件到子元件的順序,因為父元件總是比子元件先建立。
  • 一個元件的user watchers(偵聽器watcher)比render watcher先執行,因為user watchers往往比render watcher更早建立
  • 如果一個元件在父元件watcher執行期間被銷燬,它的watcher執行將被跳過

nextTick

export function nextTick (cb?: Function, ctx?: Object) {
// 這個方法裡,我把關於不寫回撥,使用promise的情況處理去掉了,把trycatch都去掉了。
  callbacks.push(() => {
     cb.call(ctx)
  })
  if (!pending) {
    pending = true
    setTimeout(flushCallbacks, 0) // 非同步任務進行了簡化
  }
}
複製程式碼

下面是非同步的回撥方法flushCallbacks,遍歷執行callbacks裡的方法,也就是遍歷執行呼叫nextTick時傳入的回撥方法。

你可能就要問了,queueWatcher的時候不是控制了只會呼叫一次nextTick嗎,為啥要用callbacks陣列來儲存呢。舉個例子:

你寫了一堆同步語句,改變了data等,然後又呼叫了一個this.$nextTick來做個非同步回撥,這個時候不就又會向callbacks陣列裡push了一個回撥方法嗎。

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
複製程式碼

如何把陣列處理成可觀察物件

不考慮相容處理
本質就是改寫陣列的原型方法。當陣列呼叫methodsToPatch這些方法時,就意味者陣列發生了變化,需要通知所有觀察者update。


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)
    // notify change
    ob.dep.notify()
    return result
  })
})

複製程式碼

後記 關於從資料入口initState開始解析的部分,寫在一篇裡篇幅太大,我放在下一篇文章了,記得去讀哦,可以加深理解。

參考文章

剖析Vue實現原理 - 如何實現雙向繫結mvvm

vue.js原始碼解讀系列 - 剖析observer,dep,watch三者關係 如何具體的實現資料雙向繫結

Vue原始碼學習筆記之Dep和Watcher

watcher排程原理

Vue原始碼閱讀 - 依賴收集原理

相關文章