從原始碼看Vue的響應式原理

清夜發表於2019-04-10

前段時間把 vue原始碼抽時間看了一遍,耐心點看再結合網上各種分析文章還是比較容易看明白的,沒太大問題,唯一的問題就是

看完即忘

從原始碼看Vue的響應式原理

當然了,也不是說啥都不記得了,大概流程以及架構這些東西還是能留下個印象的,對於 Vue的構建算是有了個整體認知,只是具體到程式碼級別的細節很難記住多少,不過也情有可原嘛,又不是背程式碼誰能記住那麼多邏輯繞來繞去的東西?

不過嘛,如果能加深對這些細節的印象那也是最好不過了,於是就決定寫幾篇文章吧,但不可能從頭到尾把 Vue全寫一遍,太多了也沒那時間,想來想去,響應式這個東西幾年前就已經被列入《三年前端,五年面試》考試大綱,那就它吧

本文以 vue@^2.6.6 進行分析

初始化

首先找入口,vue原始碼的src目錄下,存放的就是未打包前的程式碼,這個目錄下又分出幾個目錄:

從原始碼看Vue的響應式原理

compiler跟模板編譯相關,將模板編譯成語法樹,再將 ast編譯成瀏覽器可識別的 js程式碼,用於生成 DOM

core就是 Vue的核心程式碼了,包括內建元件(slottransition等),內建 api的封裝(nextTickset等)、生命週期、observervdom

platforms跟跨平臺相關,vue目前可以執行在webweex上,這個目錄裡存在的檔案用於抹平平臺間的 api差異,賦予開發者無感知的開發體驗

server存放跟伺服器渲染(SSR)相關的邏輯

sfc,縮寫來自於 Single File Components,即 單檔案元件,用於配合 webpack解析 .vue檔案,由於我們一般會將單個元件的 templatescriptstyle,以及自定義的 customBlocks寫在一個單 .vue檔案中,而這四個都是不同的東西,肯定需要在解析的時候分別抽離出來,交給對應的處理器處理成瀏覽器可執行的 js檔案

share定義一些客戶端和伺服器端公用的工具方法以及常量,例如生命週期的名稱、必須的 polyfill

其他的就廢話不多說了,直接進入主題,資料的響應式肯定是跟 data 以及 props有關,所以直接從 data以及 props的初始化開始

node_modules\vue\src\core\instance\state.js檔案中的 initState方法用於對 propsdatamethods等的初始化工作,在 new vue的時候,會呼叫 _init方法,此方法位於 Vue的原型 Vue.prototype上,這個方法就會呼叫 initState

// node_modules\vue\src\core\instance\index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
// 往 Vue建構函式的 prototyp上掛載 _init方法
initMixin(Vue)
複製程式碼
// node_modules\vue\src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
    // 初始化 props  data  watch 等
    initState(vm)
    // ...
  }
複製程式碼

initState方法如下:

// node_modules\vue\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)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製程式碼

可見,在此方法中,分別呼叫了 initPropsinitMethodsinitDatainitComputedinitWatch方法,這些方法中對 propsmethodsdatacomputedwatch進行了初始化過程,本文只是分析響應式,所以其他拋開不談,只看 initPropsinitData

initProps中,主要是使用了一個 for...inprops進行遍歷,呼叫 defineReactive方法將每個 props值變成響應式的值defineReactive正是 vue響應式的核心方法,放到後面再說;

並且又呼叫 proxy方法把這些 props值代理到 vue上,這樣做的目的是能夠讓直接訪問 vm.xxx 得到和訪問 vm._props.xxx同樣的效果(也就是代理了)

上面的意思具體點就是,你定義在 props中的東西(比如:props: { a; 1 }),首先會被附加到 vm._props物件的屬性上(即 vm._props.a),然後遍歷 vm._props,對其上的屬性進行響應式處理(對 a響應式處理),但是我們一般訪問 props並沒有看到過什麼 this._props.a的程式碼,而是直接 this.a就取到了,原因就在於 vue內部已經為我們進行了一層代理

首先附加在 vm._props上的目的是方便 vue內部的處理,只要是掛載 vm._props上的資料就都是 props而不是 datawatch什麼的,而代理到 vm上則是方便開發者書寫

// node_modules\vue\src\core\instance\state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製程式碼

proxy方法的原理其實就是使用 Object.definePropertygetset方法代理了屬性的訪問

最後,這裡面還有個 toggleObserving方法,這個方法是 vue內部對邏輯的一個優化,如果當前元件是根元件,那麼根元件是不應該有 props的,但是呢,你給根元件加個 propsvue也不會報錯,子元件的 props可以由父元素改變,但是根元件是沒有父元件的,所以很顯然根元件的 props肯定是不會改變的,也就沒必要對這種 props進行依賴收集了

這裡呼叫 toggleObserving就是禁止掉根元件 props的依賴收集

initData裡做的事情跟 initProps差不多,首先,會把 data值取出放到 vm._data上,由於data的型別可以是一個物件也可以是一個函式,所以這裡會判斷下,如果是函式則呼叫 getData方法獲取 data物件,否則直接取 data的值即可,不傳 data的話,預設 data值是空物件 {}

let data = vm.$options.data
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}
複製程式碼

這個 getData其實就是執行了傳入的 function型別的data,得到的值就是物件型別的 data

export function getData (data: Function, vm: Component): any {
  // ...
  // 使用 call執行 function型別的data,得到物件型別的data
  return data.call(vm, vm)
  // ...
}
複製程式碼

另外,initData並沒有直接對 data進行遍歷以將 data中的值都變成是響應式的,而是另外呼叫 observe方法來做這件事,observe最終也呼叫了 defineReactive,但是在呼叫之前,還進行了額外的處理,這裡暫時不說太多,放到後面和 defineReactive一起說;除此之外,initData也呼叫了 proxy進行資料代理,作用和 props呼叫 proxy差不多,只不過其是對 data資料進行代理

構建 Observe

現在回到上面沒說的 observedefineReactive,由於 observe最終還是會呼叫 defineReactive,所以就直接從 observe說起

observe,字面意思就是觀察、觀測,其主要功能就是用於檢測資料的變化,由於其屬於響應式,算是 vue的一個關鍵核心,所以其專門有一個資料夾,用於存放相關邏輯檔案

// node_modules\vue\src\core\observer\index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  /// ...
  {
    ob = new Observer(value)
  }
  /// ...
}
複製程式碼

observe方法中,主要是這一句 ob = new Observer(value),這個 Observer是一個 class

// node_modules\vue\src\core\observer\index.js
export class Observer {
  // ...
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // ...
}
複製程式碼

在其 constructor中,做了一些事情,這裡的 new Dep()Dep也是跟響應式相關的一個東西,後面再說,然後呼叫了 def,這個方法很簡單,就是呼叫 Object.defineProperty將當前例項(this)新增到value__ob__屬性上:

// node_modules\vue\src\core\util\lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
複製程式碼

vue裡很多地方都用到了 Object.defineProperty,可以看出這個東西對於 vue來說還是很重要的,少了它會很麻煩,而 IE8卻不支援 Object.defineProperty,所以 Vue不相容 IE8也是有道理的

在前面的 observe方法中,也出現過 __ob__這個東西:

if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  ob = value.__ob__
}
複製程式碼

可以看到,__ob__在這裡用於做重複校驗,如果當前資料對戲 value上已經有了 __ob__屬性並且此屬性是由 Observer構造而來,則直接返回這個值,避免重複建立

回到 Observer類,接下里會判斷 value是不是陣列,如果是陣列,再判斷 hasProto是否為 truth值,這個 hasProto就是用於檢測當前瀏覽器是否支援使用 __proto__的:

// node_modules\vue\src\core\util\env.js
// can we use __proto__?
export const hasProto = '__proto__' in {}
複製程式碼

如果是就呼叫 protoAugment,否則呼叫 copyAugment,後者可以看做是前者相容 __proto__的一個 polyfill,這兩個方法的目的是一樣的,都是用於改寫 Array.prototype上的陣列方法,以便讓陣列型別的資料也具備響應式的能力

換句話說,陣列為什麼對陣列的修改,也能觸發響應式呢?原因就在於 vue內部對一些常用的陣列方法進行了一層代理,對這些陣列方法進行了修改,關鍵點在於,在呼叫這些陣列方法的時候,會同時呼叫 notify方法:

// node_modules\vue\src\core\observer\array.js
// notify change
ob.dep.notify()
複製程式碼

ob就是 __ob__,即資料物件上掛載的自身的觀察者,notify就是觀察者的通知事件,這個後面放到 defineReactive一起說,這裡呼叫 notify告訴 vue資料發生變化,就觸發了頁面的重渲染,也就相當於是陣列也有了響應式的能力

完了之後,繼續呼叫 observeArray進行深層便利,以保證所有巢狀資料都是響應式的

接上面,如果是物件的話就無需那麼麻煩,直接呼叫 this.walk方法:

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

walk方法會對傳入的物件進行遍歷,然後對每一個遍歷到的資料呼叫 defineReactive方法,終於到這個方法了,無論是 props的初始化還是 data的初始化最後都會呼叫這個方法,前面那些都是一些差異性的分別處理

大概看一眼 defineReactive這個方法,最後呼叫的 Object.defineProperty很顯眼,原來是在這個函式中修改了屬性的 get 以及 set,這兩個方法很重要,分別對應所謂的 依賴收集派發更新

先上個上述所有流程的簡要示意圖,有個大體印象,不然說得太多容易忘

從原始碼看Vue的響應式原理

依賴收集

先看 get

// node_modules\vue\src\core\observer\index.js
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
}
複製程式碼

首先,如果當前屬性以及顯式定義了 get方法,則執行這個 get獲取到值,接著判斷 Dep.target

這裡又出現了一個新的東西: Dep,這是一個 class類,比較關鍵,是整個依賴收集的核心

// node_modules\vue\src\core\observer\dep.js

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

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

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
複製程式碼

進入 Dep的定義,此類的靜態屬性 target初始化的值是 null,但是可以通過兩個暴露出去的方法來修改這個值

另外,在 Dep.target = null的上面還有一段註釋,主要是說由於同一時間只能有一個 watcher被執行(當前執行完了再進行下一個),而這個 Dep.target的指向就是這個正在執行的 watcher,所以 Dep.target就應該是全域性唯一的,這也正是為什麼 target是個靜態屬性的原因

那麼現在由於 Dep.targetnull,不符合 if(Dep.target){},所以這個值肯定在什麼地方被修改了,而且應該是通過 pushTargetpopTarget來修改的

所以什麼地方會呼叫這兩個方法?

這又得回到 get了,什麼時候會呼叫 get?訪問這個屬性,也就是資料的時候就會呼叫這個資料的 get(如果有的話),什麼時候會訪問資料呢?當然是在渲染頁面的時候,肯定需要拿到資料來填充模板

那麼這就是生命週期的事了,這個過程應該發生在 beforeMountmount中間

// node_modules\vue\src\core\instance\lifecycle.js

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
複製程式碼

主要是 new Watcher這句程式碼,as we all konwvue 使用觀察者模式實現響應式邏輯,前面的 Observe是監聽器,那麼這裡的 Watcher就是觀察者,資料的變化會被通知給 Watcher,由 Watcher進行檢視更新等操作

進入 Watcher方法

其建構函式 constructor的最後:

this.value = this.lazy
    ? undefined
    : this.get()
複製程式碼

this.lazy是傳入的修飾符,暫時不用管,這裡可以認為直接呼叫 this.get()

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    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)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}
複製程式碼

可以看到,在 Watcherget方法中,上來就呼叫了 pushTarget方法,所以就把當前這個 watcher pushtargetStack(位於 Dep的定義檔案中)陣列中去了,並且把 Dep.target的值置為這個 watcher

所以,從這裡可以看出 targetStack陣列的作用就是類似於一個棧,棧內的項就是 watcher

try...catch...finallyfinally語句中,首先根據 this.deep來決定是否觸發當前資料子屬性的 getter,這裡暫時不看,然後就是呼叫 popTarget,這個方法就是將當前 watcher出棧,並將 Dep.target指向上一個 watcher

然後 this.cleanupDeps()其實就是依賴清空,因為已經實現了對當前 watcher的依賴收集,Dep.target已經指向了其他的 watcher,所以當前 watcher的訂閱就可以取消了,騰出空間給其他的依賴收集過程使用

接著執行 value = this.getter.call(vm, vm),這裡的 this.getter就是:

// node_modules\vue\src\core\instance\lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
複製程式碼

_update_render都是掛載在 Vue.prototype上的方法,跟元件更新相關,vm._render方法返回一個 vnode,所以肯定涉及到資料的訪問,不然怎麼構建 vnode,既然訪問資料,那麼就會呼叫資料的 get方法(如果有的話)

那麼就又回到前面了:

// node_modules\vue\src\core\observer\index.js
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
}
複製程式碼

經過上面 Watcher的構建過程,可以知道這個時候 Dep.target其實的指向已經已經被更正為當前的 watcher了,也就是 trueth值,可以進入條件語句

首先執行 dep.depend()dep是在 defineReactive方法中 new Dep的例項,那麼看下 Depdepend方法

// node_modules\vue\src\core\observer\dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
複製程式碼

Dep.target此時條件成立,所以繼續呼叫 Dep.target上的 addDep方法,Dep.target指向 Watcher,所以看 WatcheraddDep方法

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

首先通過 id避免重複新增同一資料,最後又呼叫了 dep.addSub將當前 Watcher新增到 Dep中去

這裡出現了幾個變數,newDepIdsnewDepsdepIdsdeps,這幾個變數其實就是在 Dep新增 watcher之前的一次校驗,以及方便後續移除訂閱,提升 vue的效能,算是 vue內部一種優化策略,這裡不用理會

// node_modules\vue\src\core\observer\dep.js
addSub (sub: Watcher) {
  this.subs.push(sub)
}
複製程式碼

最終,在 Dep中,會把 watcher pushDepsubs陣列屬性中

即,最終 propsdata的響應式資料的 watcher都將放到 Depsubs中,這就完成了一次依賴收集的過程

繼續回到 defineReactive,在呼叫了 dep.depend()之後,還有幾行程式碼:

// node_modules\vue\src\core\observer\index.js
let childOb = !shallow && observe(val)
// ...
if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}
複製程式碼

遞迴呼叫 observe,保證子屬性也是響應式的,如果當前值是陣列,那麼保證這個陣列也是響應式的

這個依賴收集過程,簡要示意圖如下:

從原始碼看Vue的響應式原理

派發更新

依賴收集的目的就是將所有響應式資料通過 watcher收集起來統一管理,當資料發生變化的時候,就通知檢視進行更新,這個更新的過程就是派發更新

繼續看 defineReactiveset方法,這個方法實現派發更新的主要邏輯

// node_modules\vue\src\core\observer\index.js
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()
  }
  // #7981: for accessor properties without setter
  if (getter && !setter) return
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  dep.notify()
}
複製程式碼

首先是一系列的驗證判斷,可以不用管,然後設定資料的值為傳入的值,這是一般 set函式都會執行的方法

然後到 childOb = !shallow && observe(newVal),一般情況下,shallow都是 trueth值,所以會呼叫 observe,經過上面的分析,我們知道這個 observe就是依賴收集相關的東西,這裡的意思就是對新設定的值也進行依賴收集,加入到響應式系統中來

接下來這行程式碼才是關鍵:

dep.notify()
複製程式碼

看下 Dep

// node_modules\vue\src\core\observer\dep.js
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  // ...
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
複製程式碼

notify方法中,遍歷了 subs,對每個項呼叫 update方法,經過前面的分析我們知道,subs的每個項其實都是依賴收集起來的 watcher,這裡也就是呼叫了 watcherupdate方法,通過 update來觸發對應的 watcher實現頁面更新

所以,Dep其實就是一個 watcher管理模組,當資料變化時,會被 Observer監測到,然後由 Dep通知到 watcher

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
複製程式碼

this.lazycomputed相關,computed是惰性求值的,所以這裡只是把 this.dirty設為 true,並沒有做什麼更新的操作;

this.syncwatch相關,如果 watch設定了這個值為 true,則是顯式要求 watch更新需要在當前 Tick 一併執行,不必放到下一個 Tick

這兩個暫時不看,不擴充太多避免邏輯太亂,正常流程會執行 queueWatcher(this)

// node_modules\vue\src\core\observer\scheduler.js
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
複製程式碼

queueWatcher首先會根據 has[id]來避免同一 watcher的重複新增,接下來引入了佇列的概念,vue並不會在每次資料改變的時候就立即執行 watcher重渲染頁面,而是把這些 watcher 先推送到一個佇列裡,然後在nextTick 裡呼叫 flushSchedulerQueue批量執行這些 watcher,更新 DOM

這裡在 nextTick裡執行 flushSchedulerQueue的目的就是為了要等到當前 Tick中所有的 watcher都加入到 queue中,再在下一 Tick中執行佇列中的 watcher

看下這個 flushSchedulerQueue方法,首先對佇列中的 watcher根據其 id進行排序,將 id小的 watcher放在前面(父元件 watcherid小於子元件的), 排序的目的也已經在註釋中解釋地很清楚了:

// node_modules\vue\src\core\observer\scheduler.js

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
//    created before the child)
// 2. A component's user watchers are run before its render watcher (because
//    user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
//    its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
複製程式碼

大概意思就是,在清空佇列之前對佇列進行排序,主要是為了以下 3

  • 元件的更新是由父到子的(因為父元件的建立在子元件之前),所以 watcher的建立也應該是先父後子,執行順序也應該保持先父後子

  • 使用者自定義 watcher應該在 渲染 watcher之前執行(因為使用者自定義 watcher的建立在 渲染watcher之前)

  • 如果一個元件在父元件的 watcher 執行期間被銷燬,那麼這個子元件的 watcher 都可以被跳過

排完序之後,使用了一個 for迴圈遍歷佇列,執行每個 watcherrun方法,那麼就來看下這個 run方法

// node_modules\vue\src\core\observer\watcher.js
run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
複製程式碼

首先判斷 this.active,這個 this.active的初始值是 true,那麼什麼時候會變成 false呢?當 watcher從所有 Dep中移除的時候,也就是這個 watcher移除掉了,所以也就沒有什麼派發更新的事情了

// node_modules\vue\src\core\observer\watcher.js
teardown () {
  // ...
  this.active = false
}
複製程式碼

接著執行 const value = this.get()獲取到當前值,呼叫 watcherget方法的時候會執行 watchergetter方法:

// node_modules\vue\src\core\observer\watcher.js
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // ...
  }
  // ...
  return value
}
複製程式碼

而這個 getter前面已經說了,其實就是:

// node_modules\vue\src\core\instance\lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
複製程式碼

也就是執行了 DOM更新的操作

回到 flushSchedulerQueue,在執行完 watcher.run()之後,還有些收尾工作,主要是執行了 resetSchedulerState方法

// node_modules\vue\src\core\observer\scheduler.js
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}
複製程式碼

這個方法主要是用於重置佇列狀態,比如最後將 waitingflushing置為 false,這樣一來,當下次呼叫 queueWatcher的時候,就又可以往 queue佇列裡堆 watcher

回到 queueWatcher這個方法

if (!flushing) {
  queue.push(watcher)
} else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}
複製程式碼

flushSchedulerQueue執行,進行批量處理 watcher的時候,flushing將被置為 true,這個時候如果再次新增新的 user watcher進來,那麼就會立即新增到 queue中去

這裡採取改變 queue的方式是原陣列修改,也就是說新增進去的 watcher會立即加入到 flushSchedulerQueue批處理的程式中,因而在 flushSchedulerQueue中對 queue的迴圈處理中,for迴圈是實時獲取 queue的長度的

// node_modules\vue\src\core\observer\scheduler.js
function flushSchedulerQueue () {
  // ...
  for (index = 0; index < queue.length; index++) {
    // ...
  }
  // ...
}
複製程式碼

另外,新加入的 watcher加到 queue的位置也是根據id進行排序的,契合上面所說的 watch執行先父後子的理念

大體流程示意圖如下:

從原始碼看Vue的響應式原理

總結

vue的程式碼相比於 react的其實還是挺適合閱讀的,我本來還打算打斷點慢慢看,沒想到根本沒用到,這也表明了vue的輕量級確實是有原因的

少了各種模式和各種系統的堆砌,但同時又能滿足一般業務的開發需要,程式碼體積小意味著會有更多的人有興趣將其接入移動端,概念少意味著小白也能快速上手,俗話說得小白者得天下,vue能與 react這種頂級大廠團伙化規模維護的框架庫分庭抗禮也不是沒有道理的

相關文章