1. 前言

原文發布在語雀:

<Vue 源碼筆記系列2>依賴收集的觸發 · 語雀?

www.yuque.com
圖標

上一章,我們講了 Vue 依賴收集的準備工作。我們知道,依賴收集一定是觸發了我們給 data 定義的 get 屬性。

回顧一下我們定義的 get 屬性:

// src/core/observer/index.js line134 function defineReactive
get: function reactiveGetter () {
// ...
if (Dep.target) {
dep.depend()
// ...
}
// ...
},

當時我們在 defineReactive 方法中為 data 定義 get 屬性時,涉及到了兩個東西。一個是 Dep.target, 一個是 dep.depend。當時我們只是講,dep 為 Dep 實例,是負責收集依賴的盒子。Dep.target 為觀察者實例。

接下來我們詳細看看 Dep 與 Watcher 是如何產生的,以及各自有哪些功能。

我們知道,要想觸發 get,那麼我們一定是對這個 data 進行了求值操作。哪些操作會觸發呢,很明顯,render 應該是可以的。我們將數據渲染到頁面上理所當然會對其求值嘛。

我們知道 new Vue 時會執行其原型鏈上的_init 方法,即 Vue.prototype._init 方法:

// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

在最後執行 vm.$mount 來最終將 VNode 渲染到頁面。

所以我們從 $mount 開始往下找找看。

2. 流程圖

還是老規矩,先放一張大致的流程圖。不必弄懂,可以先大致瀏覽,方便後邊對照著看。

2. $mount

$mount 方法的定義有兩處。

2.1 為 $mount 添加編譯模板能力

先來看第一處,比較簡單:

// src/platforms/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
const options = this.options
if (!options.render) {
if (template) {
// ...
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
// ...
}
}
return mount.call(this, el, hydrating)
}

第3行:

const mount = Vue.prototype.$mount

緩存一份舊的 $mount, 後邊會用到。

第 8 行省略的部分:

主要作用是為 template 賦值,我們這裡先不管詳細的實現。

9 到 23 行:

const options = this.options
if (!options.render) {
if (template) {
// ...
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
// ...
}
}

當 options.render 不存在時,使用 compileToFunctions 將 template 轉化為 render 函數。也就是說實例化 Vue 時只有 render 渲染函數選項不存在,Vue 才會編譯模板。

24 行:

return mount.call(this, el, hydrating)

最終返回的仍然是之前我們緩存的 $mount。

可以知道,這部分代碼的主要作用是為 Vue.prototype.$mount 補充了編譯模板的能力, 從文件名 entry-runtime-with-compiler.js 也能看出一二。最終執行的仍然是運行時版 $mount。

2.2 運行時版 $mount

那麼我們來看看運行時版 $mount 究竟幹了些什麼吧:

// src/platforms/web/runtime/index.js

Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

很簡單,只是調用 mountComponent。

3. mountComponent

// src/core/instance/lifecycle.js

export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== production) {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== #) ||
vm.$options.el || el) {
warn(
You are using the runtime-only build of Vue where the template +
compiler is not available. Either pre-compile the templates into +
render functions, or use the compiler-included build.,
vm
)
} else {
warn(
Failed to mount component: template or render function not defined.,
vm
)
}
}
}
callHook(vm, beforeMount)

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== production && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

// we set this to vm._watcher inside the watchers constructor
// since the watchers initial patch may call $forceUpdate (e.g. inside child
// components mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, beforeUpdate)
}
}
}, true /* isRenderWatcher */)
hydrating = false

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, mounted)
}
return vm
}

9 到 29 行:

if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== production) {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== #) ||
vm.$options.el || el) {
warn(
You are using the runtime-only build of Vue where the template +
compiler is not available. Either pre-compile the templates into +
render functions, or use the compiler-included build.,
vm
)
} else {
warn(
Failed to mount component: template or render function not defined.,
vm
)
}
}
}
callHook(vm, beforeMount)

如果 render 為空,那麼將 render 賦值為 createEmptyVNode。看名字就能知道這個方法是創建空 VNode。

如果不是生產環境,列印警告信息。之後觸發 beforeMount 生命週期鉤子。

31 到 54 行:

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== production && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

if 內部有大量的 mark,這些代碼的作用是性能分析。

所以要弄清楚這部分代碼的作用,主要看下 else 內的代碼就可以。為 updateComponent 賦值,其作用主要是生成 VNode(vm._render),並渲染(vm._update)。

59 到 65 行:

new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, beforeUpdate)
}
}
}, true /* isRenderWatcher */)

new Watcher

我們來看一下實例化 Watcher 時的幾個參數:
  1. vm Vue 實例
  2. updateComponent 我們剛剛在上邊定義的,主要作用是生成 VNode 並渲染。
  3. noop 空函數
  4. true 是否是渲染函數的觀察者,這裡當然為 true

4. Watcher

終於遇到了 Watcher 了, 代碼比較長,我們這裡節選我認為比較重要的部分講一下:

// src/core/observer/watcher.js

export default class Watcher {
constructor() {}
get () {}
addDep () {}
cleanupDeps () {}
update () {}
run () {}
getAndInvoke () {}
evaluate () {}
depend () {}
teardown () {}
}

Watcher 類有九個實例方法和一大堆的實例屬性。不必一次性弄清楚所有的方法,我們遇到了哪個就來瞭解哪個。

4.1 構造函數

先來看一下 constructor 構造函數:

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
vm._watchers.push(this)
// ...
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== production
? expOrFn.toString()
:
// parse expression for getter
if (typeof expOrFn === function) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== production && warn(
`Failed watching path: "${expOrFn}" ` +
Watcher only accepts simple dot-delimited paths. +
For full control, use a function instead.,
vm
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}

11 到 21 行:

this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== production
? expOrFn.toString()
:

為一大堆實例屬性賦值

cb 為我們剛剛new Watcher時傳入的第三個參數即空函數 noop;uid 為在文件開始處聲明的變數,初始值為 0;dirty 如注釋所說,為 computed 使用,這與 computed 惰性求值有關,我們這裡討論的是 renderWatcher,所以暫時不必管;接下來聲明瞭兩個空數組 this.deps this.newDeps, 兩個空 Set this.depIds this.newDepIds;

23 到 36 行:

if (typeof expOrFn === function) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== production && warn(
`Failed watching path: "${expOrFn}" ` +
Watcher only accepts simple dot-delimited paths. +
For full control, use a function instead.,
vm
)
}
}

為 this.getter 賦值。

當 expOrFn 為函數時:

expOrFn 為我們 new Watcher 傳入的第二個參數,即 updateComponent, 其主體為 vm._update(vm._render()), 生成 VNode 並渲染。

否則:

this.getter = parsePath(expOrFn)我們來看一下 parsePath 的代碼:

// src/core/util/lang.js

const bailRE = /[^w.$]/
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split(.)
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}

對傳入的參數使用正則 /[^w.$]/檢測,[^……] 匹配不在方括弧內任意字元,w 表示任何ASCⅡ字元組成的單詞,等價於[a-zA-Z0-9_],即數字字母下劃線。所以這個正則匹配的就是非數字、字母、下劃線、點、$符。

正則匹配成功則代表參數不合法。接下來的代碼比較簡單,聯想下我們平時使用$watch:

vm.$watch(a.b, function (newVal, oldVal) {
// 做點什麼
})

可知parsePath 方法返回一個函數,這個函數的作用是取得該路徑對應的屬性值並返回。

接著往下,37 到 41 行:

if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}

我們這裡為 renderWatcher ,所以走 else,調用 this.get

4.2 實例方法 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
}

第 2 行:

pushTarget(this)來看下 pushTarget 代碼:

// src/core/observer/dep.js
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

給class Dep 的屬性 target 賦值,值為傳入的 Watcher 實例。

5 到 21 行:

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()
}

調用 this.getter ,對於 renderWatcher ,此方法為傳入的 updateComponent。

該方法主體為 vm._update(vm._render()), 生成 VNode 並渲染。

生成 VNode,自然會對觀測的數據求值,進而觸發在依賴收集的準備工作中 defineReactive 定義的 get 屬性。

在依賴收集完成後,會調用 this.cleanupDeps, 我們先來看一下這個方法再講依賴收集。

4.3 實例方法 cleanupDeps

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
}

deps newDeps 為 Dep 實例數組。在依賴收集時將當前 dep push 到 this.newDeps。

依賴收集完畢後,執行本方法:

2 到 8 行:

let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}

遍歷 this.deps, 移除 this.newDepIds 中不存在的 dep,此舉的目的是移除當前已經用不到的舊 dep。

9 到 16 行:

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

很簡單,將 newDeps 的值與 deps 互換,並且情況 newDeps。

你也可以在下一節依賴收集結束後來回顧這個方法。

5. 依賴收集

這部分在上一章已經相信講解過,不清楚的話可以回頭再看看。

來看一下我們定義的 get 屬性

// src/core/observer/index.js defineReactive

get: function reactiveGetter () {
// ...
if (Dep.target) {
dep.depend()
// ...
}
// ...
},

Dep.target 為當前 Watcher 實例。

dep 為 Dep 實例,我們來看一下dep.depend的代碼:

// src/core/observer/dep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

調用 Watcher 實例的 addDep 方法,代碼如下:

// src/core/observer/watcher.js
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)
}
}
}

將 dep 和其 id 保存到 this.newDeps this.newDepIds ,判斷語句的目的是防止重複依賴。

如果沒有重複的話,調用 dep.addSub

來看下 dep.addSub 的代碼:

// src/core/observer/dep.js
addSub (sub: Watcher) {
this.subs.push(sub)
}

將當前 Watcher 實例保存到 dep 實例的 subs 屬性中。

6. 小結

本章我們主要從渲染函數的觀察者角度,分析了 Vue 從 $mount 到最終渲染頁面的過程中,如何觸發了依賴收集動作。

可以看到,關於 Dep 和 Watcher 類,我們還有很多屬性沒有講到,這些主要和依賴更新有關。依賴收集完成後,我們又如何在數據變化時更新頁面呢,這就是我們下一章的內容:依賴更新。

7. 參考文獻

  1. Vue 技術內幕

推薦閱讀:

相關文章