前段時間把 vue
原始碼抽時間看了一遍,耐心點看再結合網上各種分析文章還是比較容易看明白的,沒太大問題,唯一的問題就是
看完即忘
當然了,也不是說啥都不記得了,大概流程以及架構這些東西還是能留下個印象的,對於 Vue
的構建算是有了個整體認知,只是具體到程式碼級別的細節很難記住多少,不過也情有可原嘛,又不是背程式碼誰能記住那麼多邏輯繞來繞去的東西?
不過嘛,如果能加深對這些細節的印象那也是最好不過了,於是就決定寫幾篇文章吧,但不可能從頭到尾把 Vue
全寫一遍,太多了也沒那時間,想來想去,響應式這個東西幾年前就已經被列入《三年前端,五年面試》考試大綱,那就它吧
本文以
vue@^2.6.6
進行分析
初始化
首先找入口,vue
原始碼的src
目錄下,存放的就是未打包前的程式碼,這個目錄下又分出幾個目錄:
compiler
跟模板編譯相關,將模板編譯成語法樹,再將 ast
編譯成瀏覽器可識別的 js
程式碼,用於生成 DOM
core
就是 Vue
的核心程式碼了,包括內建元件(slot
、transition
等),內建 api
的封裝(nextTick
、set
等)、生命週期、observer
、vdom
等
platforms
跟跨平臺相關,vue
目前可以執行在web
和 weex
上,這個目錄裡存在的檔案用於抹平平臺間的 api
差異,賦予開發者無感知的開發體驗
server
存放跟伺服器渲染(SSR
)相關的邏輯
sfc
,縮寫來自於 Single File Components
,即 單檔案元件
,用於配合 webpack
解析 .vue
檔案,由於我們一般會將單個元件的 template
、script
、style
,以及自定義的 customBlocks
寫在一個單 .vue
檔案中,而這四個都是不同的東西,肯定需要在解析的時候分別抽離出來,交給對應的處理器處理成瀏覽器可執行的 js
檔案
share
定義一些客戶端和伺服器端公用的工具方法以及常量,例如生命週期的名稱、必須的 polyfill
等
其他的就廢話不多說了,直接進入主題,資料的響應式肯定是跟 data
以及 props
有關,所以直接從 data
以及 props
的初始化開始
node_modules\vue\src\core\instance\state.js
檔案中的 initState
方法用於對 props
、data
、methods
等的初始化工作,在 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)
}
}
複製程式碼
可見,在此方法中,分別呼叫了 initProps
、initMethods
、initData
、initComputed
、initWatch
方法,這些方法中對 props
、methods
、data
、computed
、watch
進行了初始化過程,本文只是分析響應式,所以其他拋開不談,只看 initProps
和 initData
initProps
中,主要是使用了一個 for...in
對 props
進行遍歷,呼叫 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
而不是data
或watch
什麼的,而代理到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.defineProperty
的 get
和 set
方法代理了屬性的訪問
最後,這裡面還有個 toggleObserving
方法,這個方法是 vue
內部對邏輯的一個優化,如果當前元件是根元件,那麼根元件是不應該有 props
的,但是呢,你給根元件加個 props
,vue
也不會報錯,子元件的 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
現在回到上面沒說的 observe
和 defineReactive
,由於 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
,這兩個方法很重要,分別對應所謂的 依賴收集 和 派發更新
先上個上述所有流程的簡要示意圖,有個大體印象,不然說得太多容易忘
依賴收集
先看 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.target
是 null
,不符合 if(Dep.target){}
,所以這個值肯定在什麼地方被修改了,而且應該是通過 pushTarget
或 popTarget
來修改的
所以什麼地方會呼叫這兩個方法?
這又得回到 get
了,什麼時候會呼叫 get
?訪問這個屬性,也就是資料的時候就會呼叫這個資料的 get
(如果有的話),什麼時候會訪問資料呢?當然是在渲染頁面的時候,肯定需要拿到資料來填充模板
那麼這就是生命週期的事了,這個過程應該發生在 beforeMount
和 mount
中間
// 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 konw
,vue
使用觀察者模式實現響應式邏輯,前面的 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
}
複製程式碼
可以看到,在 Watcher
的 get
方法中,上來就呼叫了 pushTarget
方法,所以就把當前這個 watcher
push
到 targetStack
(位於 Dep
的定義檔案中)陣列中去了,並且把 Dep.target
的值置為這個 watcher
所以,從這裡可以看出 targetStack
陣列的作用就是類似於一個棧,棧內的項就是 watcher
try...catch...finally
的 finally
語句中,首先根據 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
的例項,那麼看下 Dep
的 depend
方法
// node_modules\vue\src\core\observer\dep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
複製程式碼
Dep.target
此時條件成立,所以繼續呼叫 Dep.target
上的 addDep
方法,Dep.target
指向 Watcher
,所以看 Watcher
的 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)
}
}
}
複製程式碼
首先通過 id
避免重複新增同一資料,最後又呼叫了 dep.addSub
將當前 Watcher
新增到 Dep
中去
這裡出現了幾個變數,newDepIds
、newDeps
、depIds
、deps
,這幾個變數其實就是在 Dep
新增 watcher
之前的一次校驗,以及方便後續移除訂閱,提升 vue
的效能,算是 vue
內部一種優化策略,這裡不用理會
// node_modules\vue\src\core\observer\dep.js
addSub (sub: Watcher) {
this.subs.push(sub)
}
複製程式碼
最終,在 Dep
中,會把 watcher
push
到 Dep
的 subs
陣列屬性中
即,最終 props
和 data
的響應式資料的 watcher
都將放到 Dep
的 subs
中,這就完成了一次依賴收集的過程
繼續回到 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
,保證子屬性也是響應式的,如果當前值是陣列,那麼保證這個陣列也是響應式的
這個依賴收集過程,簡要示意圖如下:
派發更新
依賴收集的目的就是將所有響應式資料通過 watcher
收集起來統一管理,當資料發生變化的時候,就通知檢視進行更新,這個更新的過程就是派發更新
繼續看 defineReactive
的 set
方法,這個方法實現派發更新的主要邏輯
// 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
,這裡也就是呼叫了 watcher
的 update
方法,通過 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.lazy
跟 computed
相關,computed
是惰性求值的,所以這裡只是把 this.dirty
設為 true
,並沒有做什麼更新的操作;
this.sync
跟 watch
相關,如果 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
放在前面(父元件 watcher
的 id
小於子元件的), 排序的目的也已經在註釋中解釋地很清楚了:
// 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
迴圈遍歷佇列,執行每個 watcher
的 run
方法,那麼就來看下這個 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()
獲取到當前值,呼叫 watcher
的 get
方法的時候會執行 watcher
的 getter
方法:
// 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
}
複製程式碼
這個方法主要是用於重置佇列狀態,比如最後將 waiting
、flushing
置為 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
的程式碼相比於 react
的其實還是挺適合閱讀的,我本來還打算打斷點慢慢看,沒想到根本沒用到,這也表明了vue
的輕量級確實是有原因的
少了各種模式和各種系統的堆砌,但同時又能滿足一般業務的開發需要,程式碼體積小意味著會有更多的人有興趣將其接入移動端,概念少意味著小白也能快速上手,俗話說得小白者得天下,vue
能與 react
這種頂級大廠團伙化規模維護的框架庫分庭抗禮也不是沒有道理的