從原始碼解析vue的響應式原理-響應式的整體流程
vue官方對響應式原理的解釋:深入響應式原理

上一節講了VUE中 ofollow,noindex">依賴收集和依賴觸發的原理 ,然鵝對響應式的整體流程我們還是有很多疑問:
- VUE是何時進行依賴收集的?
- 依賴觸發了以後又是怎麼進行頁面響應式變化的?
- watcher物件到底起到了什麼作用?
為了回答以上的幾個問題,我們不得不梳理一波 VUE響應式的整體流程 了
從例項初始化階段開始說起
vue原始碼的 instance/init.js 中是初始化的入口,其中初始化中除了初始化的幾個步驟以外,在最後有這樣一段 程式碼:
if (vm.$options.el) { vm.$mount(vm.$options.el) } 複製程式碼
在初始化結束後,呼叫 options.el中。
關於$mount的定義在兩處可以看到:platforms/web/runtime/index.js、platforms/web/entry-runtime-with-compiler.js
其中runtime/index.js的程式碼如下:
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined // 劃重點!!! return mountComponent(this, el, hydrating) } 複製程式碼
runtime/index.js是執行時vue的入口,其中定義的 mount功能,其中主要呼叫了mountComponent()函式完成掛載。 entry-runtime-with-compiler.js是完整的vue的入口,在執行時vue的$mount基礎上加入了編譯模版的能力。
編譯模版,為掛載提供渲染函式
entry-runtime-with-compiler.js中定義了 mount()的基礎上添加了模版編譯。程式碼如下:
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) //檢查掛載點是不是<body>元素或者<html>元素 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // 判斷渲染函式不存在時 if (!options.render) { ...//構建渲染函式 } //呼叫執行時vue的$mount()函式, return mount.call(this, el, hydrating) } 複製程式碼
entry-runtime-with-compiler.js中的$mount()函式主要做了三件事:
- 判斷掛載點是不是元素或者元素,因為掛載點會被自身模版替代掉,因此掛載點不能為元素或者元素;
- 判斷渲染函式是否存在,如果渲染函式不存在,則構建渲染函式;
- 呼叫執行時vue的 mount();
建立渲染函式
上述第二步,若渲染函式不存在時,構建渲染函式,程式碼如下:
let template = options.template //如果template存在,則通過template獲取真正的【模版】 if (template) { //template是字串 if (typeof template === 'string') { //template第一個字元是#,則將該字串作為id選擇器獲取對應元素作為【模版】 if (template.charAt(0) === '#') { template = idToTemplate(template) ... //省略 } //如果template是元素節點,則將template的innerHTML作為【模版】 } else if (template.nodeType) { template = template.innerHTML //若template無效,則顯示提示 } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } //若template不存在,則將el元素的outerHTML作為【模版】 } else if (el) { template = getOuterHTML(el) } //此時template中是最終的【模版】,下面根據【模版】生成rander函式 if (template) { ... //省略 // 劃重點!!! // 使用compileToFunctions函式將【模版】template,編譯成為渲染函式。 const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns ... //省略 } 複製程式碼
建立渲染函式階段主要做了兩件事:
- 得到【模版】字串:
- 如果template存在,且template是字串以#開頭,則將該字串作為id選擇器獲取對應元素作為【模版】
- 如果template是元素節點,則將template的innerHTML作為【模版】
- 如果tempalte是無效字串,則顯示warning
- 若template不存在,則將el元素的outerHTML作為【模版】
- 根據【模版】字串生成渲染函式render()
- 生成的options.render,在掛載元件的mountComponent函式中用到
實現掛載的mountComponent()函式
上一步確保渲染函式render()存在後,就進入到了這正的掛載階段。前面講到掛載函式主要在mountComponent()中完成。
mountComponent()函式的定義在src/core/instance/lifecycle.js檔案中。程式碼如下:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el //如果render不存在 if (!vm.$options.render) { //為render賦初始值,並列印warning提示資訊 vm.$options.render = createEmptyVNode ... //省略 } } //觸發beforeMount鉤子 callHook(vm, 'beforeMount') // 開始掛載 let updateComponent /* istanbul ignore if */ // 定義並初始化updateComponent函式 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) // 呼叫_render函式生成vnode虛擬節點 const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) // 以虛擬節點vnode作為引數呼叫_update函式,生成真正的DOM vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { updateComponent = () => { //呼叫_render函式生成vnode虛擬節點;以虛擬節點vnode作為引數呼叫_update函式,生成真正的DOM vm._update(vm._render(), hydrating) } } 複製程式碼
mountComponent主要做了三件事:
- 如果render不存在,為render賦初始值,並列印warning資訊
- 觸發beforeMount
- 定義並初始化updateComponent函式:
- 呼叫_render函式生成vnode虛擬節點
- 虛擬節點vnode作為引數呼叫_update函式,生成真正的DOM
Watcher類
watcher類的定義在core/observer/watcher.js中,程式碼如下:
export default class Watcher { ... // // 建構函式 constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { // 將渲染函式的觀察者存入_watcher vm._watcher = this } //將所有觀察者push到_watchers列表 vm._watchers.push(this) // options if (options) { // 是否深度觀測 this.deep = !!options.deep // 是否為開發者定義的watcher(渲染函式觀察者、計算屬性觀察者屬於內部定義的watcher) this.user = !!options.user // 是否為計算屬性的觀察者 this.computed = !!options.computed this.sync = !!options.sync //在資料變化之後、觸發更新之前呼叫 this.before = options.before } else { this.deep = this.user = this.computed = this.sync = false } // 定義一系列例項屬性 this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.computed // for computed watchers this.deps = [] this.newDeps = [] // depIds 和 newDepIds 用書避免重複收集依賴 this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter // 相容被觀測資料,當被觀測資料是function時,直接將其作為getter // 當被觀測資料不是function時通過parsePath解析其真正的返回值 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.get()方法 this.value = this.get() } } // get方法 get () { ... } // 新增依賴 addDep (dep: Dep) { ... } // 移除廢棄觀察者;清空newDepIds 屬性和 newDeps 屬性的值 cleanupDeps () { ... } // 當依賴變化時,觸發更新 update () { ... } // 資料變化函式的入口 run () { ... } // 真正進行資料變化的函式 getAndInvoke (cb: Function) { ... } // evaluate () { ... } // depend () { ... } // teardown () { ... } } 複製程式碼
watcher建構函式
由以上程式碼可見,在watcher建構函式中做了如下幾件事:
- 將元件的渲染函式的觀察者存入_watcher,將所有的觀察者存入_watchers中
- 儲存before函式,在資料變化之後、觸發更新之前呼叫
- 定義一系列例項屬性
- 相容被觀測資料,當被觀測資料是function時,直接將其作為getter; 當被觀測資料不是function時通過parsePath解析其真正的返回值,被觀測資料是 'obj.name'時,通過parsePath拿到真正的obj.name的返回值
- 除計算屬性的觀察者以外的所有觀察者呼叫this.get()方法
get()中收集依賴
get中的程式碼如下:
get () { // 將觀察者物件儲存至Dep.target中(Dep.target在上一章提到過) pushTarget(this) let value const vm = this.vm try { //呼叫getter方法,獲得被觀察目標的值 value = this.getter.call(vm, vm) } catch (e) { ... } finally { ... } return value } 複製程式碼
get()函式中主要做了如下幾件事:
- 呼叫pushTarget()方法,將觀察者物件儲存至Dep.target中,其中Dep.target在上一章提到過
- 呼叫defineReactive中的get實現依賴收集、返回正確值
- 上一章講過,defineReactive中呼叫dep.depend(),dep.depend()中呼叫Dep.target.addDep()進行依賴收集
addDep新增依賴
// 新增依賴 addDep (dep: Dep) { const id = dep.id // newDepIds避免本次get中重複收集依賴 if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) // 避免多次求值中重複收集依賴,每次求值之後newDepIds會被清空,因此需要depIds來判斷。newDepIds中清空 if (!this.depIds.has(id)) { dep.addSub(this) } } } 複製程式碼
- 在addDep中新增依賴,並避免對一個數據多次求值時,其觀察者被重複收集。
- newDepIds避免一次求值的過程中重複收集依賴
- depIds 屬性避免多次求值中重複收集依賴
響應式的整體流程
根據上一章和本章的講解,總結一下響應式的整體流程: 假設有模版:
<div id="test"> {{str}} </div> 複製程式碼
- 呼叫$mount()函式進入到掛載階段
- 檢查是否有render()函式,根據上述模版建立render()函式
- 呼叫了mountComponent()函式完成掛載,並在mountComponen()中定義並初始化updateComponent()
- 為渲染函式新增觀察者,在觀察者中對渲染函式求值
- 在求值的過程中觸發資料物件str的get,在str的get中收集str的觀察者到資料的dep中
- 修改str的值時,觸發str的set,在set中呼叫資料的dep的notify觸發響應
- notify中對每一個觀察者呼叫update方法
- 在run中呼叫getAndInvoke函式,進行資料變化。 在getAndInvoke函式中呼叫回撥函式
- 對於渲染函式的觀察者來說getAndInvoke就相當於執行updateComponent函式
- 在updateComponent函式中呼叫_render函式生成vnode虛擬節點,以虛擬節點vnode作為引數呼叫_update函式,生成真正的DOM
至此響應式過程完成。
參考文章: 揭開資料響應系統的面紗