vue數據綁定源碼

思路分析

數據的雙向綁定,就是數據變化了自動更新視圖,視圖變化了自動更新數據,實際上視圖變化更新數據只要經過事件監聽就能夠實現了,並非數據雙向綁定的關鍵點。關鍵仍是數據變化了驅動視圖自動更新。vue

全部接下來,咱們詳細瞭解下數據如何驅動視圖更新的。
數據驅動視圖更新的重點就是,如何知道數據更新了,或者說數據更新了要如何主動的告訴咱們。可能你們都聽過,vue的數據雙向綁定原理是Object.defineProperty( )對屬性設置一個set/get,是這樣的沒錯,其實get/set只是能夠作到對數據的讀取進行劫持,就可讓咱們知道數據更新了。可是你詳細的瞭解整個過程嗎?
先來看張你們都不陌生的圖:react

clipboard.png

  • Observe 類劫持監聽全部屬性,主要給響應式對象的屬性添加 getter/setter 用於依賴收集與派發更新
  • Dep 類用於收集當前響應式對象的依賴關係
  • Watcher 類是觀察者,實例分爲渲染 watcher、計算屬性 watcher、偵聽器 watcher三種

介紹數據驅動更新以前,先介紹下面4個類和方法,而後從數據的入口initState開始按順序介紹,如下類和方法是如何協做,達到數據驅動更新的。git

defineReactive

這個方法,用處可就大了。

咱們看到他是給對象的鍵值添加get/set方法,也就是對屬性的取值和賦值都加了攔截,同時用閉包給每一個屬性都保存了一個Dep對象。

當讀取該值的時候,就把當前這個watcherDep.target)添加進他的dep裏的觀察者列表,這個watcher也會把這個dep添加進他的依賴列表。
當給設置值的時候,就讓這個閉包保存的dep去通知他的觀察者列表的每個watchergithub

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

什麼是可觀察者對象呢?

簡單來講:就是數據變動時能夠通知全部觀察他的觀察者。

一、取值的時候,能把要取值的watcher(觀察者對象)加入它的dep(依賴,也可叫觀察者管理器)管理的subs列表裏(即觀察者列表);

二、設置值的時候,有了變化,全部依賴於它的對象(即它的dep裏收集到的觀察者watcher)都獲得通知。segmentfault

這個類功能就是把數據轉化成可觀察對象。針對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裏的某個觀察者。promise

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時調用。也就是數據變化不是當即就去更新的,而是異步批量去更新的。app

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源碼閱讀 - 依賴收集原理

相關文章
相關標籤/搜索