原文發布在語雀:
上一章,我們講了 Vue 依賴收集的準備工作。我們知道,依賴收集一定是觸發了我們給 data 定義的 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 為觀察者實例。
我們知道,要想觸發 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 方法的定義有兩處。
先來看第一處,比較簡單:
// 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 行省略的部分:
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。
那麼我們來看看運行時版 $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。
// 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。
31 到 54 行:
if 內部有大量的 mark,這些代碼的作用是性能分析。
59 到 65 行:
new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, beforeUpdate) } } }, true /* isRenderWatcher */)
new Watcher
終於遇到了 Watcher 了, 代碼比較長,我們這裡節選我認為比較重要的部分講一下:
// src/core/observer/watcher.js export default class Watcher { constructor() {} get () {} addDep () {} cleanupDeps () {} update () {} run () {} getAndInvoke () {} evaluate () {} depend () {} teardown () {} }
Watcher 類有九個實例方法和一大堆的實例屬性。不必一次性弄清楚所有的方法,我們遇到了哪個就來瞭解哪個。
先來看一下 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() :
為一大堆實例屬性賦值
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 為函數時:
vm._update(vm._render())
否則:
this.getter = parsePath(expOrFn)
// 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_],即數字字母下劃線。所以這個正則匹配的就是非數字、字母、下劃線、點、$符。
/[^w.$]/
[^……]
w
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
看下源碼:
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)
// 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。
生成 VNode,自然會對觀測的數據求值,進而觸發在依賴收集的準備工作中 defineReactive 定義的 get 屬性。
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。
這部分在上一章已經相信講解過,不清楚的話可以回頭再看看。
// src/core/observer/index.js defineReactive get: function reactiveGetter () { // ... if (Dep.target) { dep.depend() // ... } // ... },
Dep.target 為當前 Watcher 實例。
// src/core/observer/dep.js depend () { if (Dep.target) { Dep.target.addDep(this) } }
// 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 的代碼:
// src/core/observer/dep.js addSub (sub: Watcher) { this.subs.push(sub) }
將當前 Watcher 實例保存到 dep 實例的 subs 屬性中。
本章我們主要從渲染函數的觀察者角度,分析了 Vue 從 $mount 到最終渲染頁面的過程中,如何觸發了依賴收集動作。
推薦閱讀: