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

vue已經是目前國內前端web端三分天下之一,同時也做爲本人主要技術棧之一,在平常使用中知其然也好奇着因此然,另外最近的社區涌現了一大票vue源碼閱讀類的文章,在下借這個機會從你們的文章和討論中汲取了一些養分,同時對一些閱讀源碼時的想法進行總結,出產一些文章,做爲本身思考的輸出,本人水平有限,歡迎留言討論~前端

目標Vue版本:2.5.17-beta.0vue

vue源碼註釋:github.com/SHERlocked9…react

聲明:文章中源碼的語法都使用 Flow,而且源碼根據須要都有刪節(爲了避免被迷糊 @_@),若是要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~git

感興趣的同窗能夠加文末的微信羣,一塊兒討論吧~github

1. 響應式系統

經過官網的介紹咱們知道 Vue.js 是一個MVVM框架,它並不關心視圖變化,而經過數據驅動視圖更新,這讓咱們的狀態管理很是簡單,而這是怎麼實現的呢。盜用官網一張圖web

每一個組件實例都有相應的 Watcher 實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的 setter 被調用時,會通知 watcher 從新計算,從而導致它關聯的組件得以更新。segmentfault

這裏有三個重要的概念 ObserveDepWatcher,分別位於src/core/observer/index.jssrc/core/observer/dep.jssrc/core/observer/watcher.jsapi

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

2. 代碼實現

2.1 initState

響應式化的入口位於 src/core/instance/init.js 的 initState 中:數組

// src/core/instance/state.js

export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)              // 初始化props
  if (opts.methods) initMethods(vm, opts.methods)        // 初始化methods
  if (opts.data) initData(vm)                            // 初始化data
  if (opts.computed) initComputed(vm, opts.computed)     // 初始化computed
  if (opts.watch) initWatch(vm, opts.watch)              // 初始化watch
  }
}
複製代碼

它很是規律的定義了幾個方法來初始化 propsmethodsdatacomputedwathcer,這裏看一下 initData 方法,來窺一豹緩存

// src/core/instance/state.js

function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
                    ? getData(data, vm)
                    : data || {}
  observe(data, true /* asRootData */)             // 給data作響應式處理
}
複製代碼

首先判斷了下 data 是否是函數,是則取返回值不是則取自身,以後有一個 observe 方法對 data 進行處理,這個方法嘗試給建立一個Observer實例 __ob__,若是成功建立則返回新的Observer實例,若是已有Observer實例則返回現有的Observer實例

2.2 Observer/defineReactive

// src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}
複製代碼

這個方法主要用 data 做爲參數去實例化一個 Observer 對象實例,Observer 是一個 Class,用於依賴收集和 notify 更新,Observer 的構造函數使用 defineReactive 方法給對象的鍵響應式化,給對象的屬性遞歸添加 getter/setter ,當data被取值的時候觸發 getter 並蒐集依賴,當被修改值的時候先觸發 getter 再觸發 setter 並派發更新

// src/core/observer/index.js

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    value: any;
    this.dep = new Dep()
    def(value, '__ob__', this)    // def方法保證不可枚舉
    this.walk(value)
  }

  // 遍歷對象的每個屬性並將它們轉換爲getter/setter
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) { // 把全部可遍歷的對象響應式化
      defineReactive(obj, keys[i])
    }
  }
}

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
  const dep = new Dep()         // 在每一個響應式鍵值的閉包中定義一個dep對象

  // 若是以前該對象已經預設了getter/setter則將其緩存,新定義的getter/setter中會將其執行
  const getter = property && property.get
  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         // 若是本來對象擁有getter方法則執行
      if (Dep.target) {                    // 若是當前有watcher在讀取當前值
        dep.depend()                       // 那麼進行依賴收集,dep.addSub
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val    // 先getter
      if (newVal === value || (newVal !== newVal && value !== value)) {   // 若是跟原來值同樣則無論
        return
      }
      if (setter) { setter.call(obj, newVal) }         // 若是本來對象擁有setter方法則執行
      else { val = newVal }
      dep.notify()                                     // 若是發生變動,則通知更新,調用watcher.update()
    }
  })
}
複製代碼

getter 的時候進行依賴的收集,注意這裏,只有在 Dep.target 中有值的時候纔會進行依賴收集,這個 Dep.target 是在Watcher實例的 get 方法調用的時候 pushTarget 會把當前取值的watcher推入 Dep.target,原先的watcher壓棧到 targetStack 棧中,當前取值的watcher取值結束後出棧並把原先的watcher值賦給 Dep.targetcleanupDeps 最後把新的 newDeps 裏已經沒有的watcher清空,以防止視圖上已經不須要的無用watcher觸發

setter 的時候首先 getter,而且比對舊值沒有變化則return,若是發生變動,則dep通知全部subs中存放的依賴本數據的Watcher實例 update 進行更新,這裏 update 中會 queueWatcher( ) 異步推送到調度者觀察者隊列 queue 中,在nextTick時 flushSchedulerQueue( ) 把隊列中的watcher取出來執行 watcher.run 且執行相關鉤子函數

2.3 Dep

上面屢次提到了一個關鍵詞 Dep,他是依賴收集的容器,或者稱爲依賴蒐集器,他記錄了哪些Watcher依賴本身的變化,或者說,哪些Watcher訂閱了本身的變化;這裏引用一個網友的發言:

@liuhongyi0101 :簡單點說就是引用計數 ,誰借了個人錢,我就把那我的記下來,之後個人錢少了 我就通知他們說我沒錢了

而把借錢的人記下來的小本本就是這裏 Dep 實例裏的subs

// src/core/observer/dep.js

let uid = 0            // Dep實例的id,爲了方便去重

export default class Dep {
  static target: ?Watcher           // 當前是誰在進行依賴的收集
  id: number
  subs: Array<Watcher>              // 觀察者集合
  
  constructor() {
    this.id = uid++                             // Dep實例的id,爲了方便去重
    this.subs = []                              // 存儲收集器中須要通知的Watcher
  }

  addSub(sub: Watcher) { ... }  /* 添加一個觀察者對象 */
  removeSub(sub: Watcher) { ... }  /* 移除一個觀察者對象 */
  depend() { ... }  /* 依賴收集,當存在Dep.target的時候把本身添加觀察者的依賴中 */
  notify() { ... }  /* 通知全部訂閱者 */
}

const targetStack = []           // watcher棧

export function pushTarget(_target: ?Watcher) { ... }  /* 將watcher觀察者實例設置給Dep.target,用以依賴收集。同時將該實例存入target棧中 */
export function popTarget() { ... }  /* 將觀察者實例從target棧中取出並設置給Dep.target */
複製代碼

這裏 Dep 的實例中的 subs 蒐集的依賴就是 watcher 了,它是 Watcher 的實例,未來用來通知更新

2.4 Watcher

// src/core/observer/watcher.js

/* 一個解析表達式,進行依賴收集的觀察者,同時在表達式數據變動時觸發回調函數。它被用於$watch api以及指令 */
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean      // 是不是渲染watcher的標誌位
  ) {
    this.getter = expOrFn                // 在get方法中執行
    if (this.computed) {                   // 是不是 計算屬性
      this.value = undefined
      this.dep = new Dep()                 // 計算屬性建立過程當中並未求值
    } else {                               // 不是計算屬性會馬上求值
      this.value = this.get()
    }
  }

  /* 得到getter的值而且從新進行依賴收集 */
  get() {
    pushTarget(this)                // 設置Dep.target = this
    let value
    value = this.getter.call(vm, vm)
    popTarget()                      // 將觀察者實例從target棧中取出並設置給Dep.target
    this.cleanupDeps()
    return value
  }

  addDep(dep: Dep) { ... }  /* 添加一個依賴關係到Deps集合中 */
  cleanupDeps() { ... }  /* 清理newDeps裏沒有的無用watcher依賴 */
  update() { ... }  /* 調度者接口,當依賴發生改變的時候進行回調 */
  run() { ... }  /* 調度者工做接口,將被調度者回調 */
  getAndInvoke(cb: Function) { ... }
  evaluate() { ... }  /* 收集該watcher的全部deps依賴 */
  depend() { ... }  /* 收集該watcher的全部deps依賴,只有計算屬性使用 */
  teardown() { ... }  /* 將自身從全部依賴收集訂閱列表刪除 */
}
複製代碼

get 方法中執行的 getter 就是在一開始new渲染watcher時傳入的 updateComponent = () => { vm._update(vm._render(), hydrating) },這個方法首先 vm._render() 生成渲染VNode樹,在這個過程當中完成對當前Vue實例 vm 上的數據訪問,觸發相應一衆響應式對象的 getter,而後 vm._update()patch

注意這裏的 get 方法最後執行了 getAndInvoke,這個方法首先遍歷watcher中存的 deps,移除 newDep 中已經沒有的訂閱,而後 depIds = newDepIds; deps = newDeps ,把 newDepIdsnewDeps 清空。每次添加完新的訂閱後移除舊的已經不須要的訂閱,這樣在某些狀況,好比 v-if 已不須要的模板依賴的數據發生變化時就不會通知watcher去 update

2.5 小結

整個收集的流程大約是這樣的,能夠對照着上面的流程看一下

watcher 有下面幾種使用場景:

  • render watcher 渲染 watcher,渲染視圖用的 watcher
  • computed watcher 計算屬性 watcher,由於計算屬性即依賴別人也被人依賴,所以也會持有一個 Dep 實例
  • watch watcher 偵聽器 watcher

只要會被別的觀察者 (watchers) 依賴,好比data、data的屬性、計算屬性、props,就會在閉包裏生成一個 Dep 的實例 dep 並在被調用 getter 的時候 dep.depend 收集它被誰依賴了,並把被依賴的watcher存放到本身的subs中 this.subs.push(sub),以便在自身改變的時候通知 notify 存放在 dep.subs 數組中依賴本身的 watchers 本身改變了,請及時 update ~

只要依賴別的響應式化對象的對象,都會生成一個觀察者 watcher ,用來統計這個 watcher 依賴了哪些響應式對象,在這個 watcher 求值前把當前 watcher 設置到全局 Dep.target,並在本身依賴的響應式對象發生改變的時候及時 update


本文是系列文章,隨後會更新後面的部分,共同進步~

  1. Vue源碼閱讀 - 文件結構與運行機制
  2. Vue源碼閱讀 - 依賴收集原理
  3. Vue源碼閱讀 - 批量異步更新與nextTick原理

網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~

參考:

  1. Vue2.1.7源碼學習
  2. Vue.js 技術揭祕
  3. 剖析 Vue.js 內部運行機制
  4. Vue.js 文檔
  5. 【大型乾貨】手拉手帶你過一遍vue部分源碼
  6. MDN - Object.defineProperty()
  7. Vue.js源碼學習一 —— 數據選項 State 學習

PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~

另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~

相關文章
相關標籤/搜索