最近部門分享,有同窗提到了 Vue 響應式原理,你們在討論時,發現一些同窗對這一知識理解還不夠深刻,不能造成一個閉環,爲了幫助你們理解這個問題,我從新過了一下 Vue 源碼,並整理了多張流程圖,便於你們理解。vue
本文 Vue 源碼版本:2.6.11,爲了便於理解,均有所刪減。
本文將從如下兩個方面進行探索:node
先從最簡單的一段 Vue 代碼開始:dom
<template> <div> {{ message }} </div> </template> <script> new Vue({ data() { return { message: "hello world", }; }, }); </script>
這段代碼很簡單,最終會在頁面上打印一個 hello world,它是如何實現的呢?異步
咱們從源頭:new Vue 的地方開始分析。函數
// 執行 new Vue 時會依次執行如下方法 // 1. Vue.prototype._init(option) // 2. initState(vm) // 3. observe(vm._data) // 4. new Observer(data) // 5. 調用 walk 方法,遍歷 data 中的每個屬性,監聽數據的變化。 function walk(obj) { const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); } } // 6. 執行 defineProperty 監聽數據讀取和設置。 function defineReactive(obj, key, val) { // 爲每一個屬性建立 Dep(依賴蒐集的容器,後文會講) const dep = new Dep(); // 綁定 get、set Object.defineProperty(obj, key, { get() { const value = val; // 若是有 target 標識,則進行依賴蒐集 if (Dep.target) { dep.depend(); } return value; }, set(newVal) { val = newVal; // 修改數據時,通知頁面從新渲染 dep.notify(); }, }); }
數據描述符綁定完成後,咱們就能獲得如下的流程圖:源碼分析
圖中咱們能夠看到,Vue 初始化時,進行了數據的 get、set 綁定,並建立了一個 Dep 對象。this
對於數據的 get、set 綁定咱們並不陌生,可是 Dep 對象什麼呢?spa
Dep 對象用於依賴收集,它實現了一個發佈訂閱模式,完成了數據 Data 和渲染視圖 Watcher 的訂閱,咱們一塊兒來剖析一下。prototype
class Dep { // 根據 ts 類型提示,咱們能夠得出 Dep.target 是一個 Watcher 類型。 static target: ?Watcher; // subs 存放蒐集到的 Watcher 對象集合 subs: Array<Watcher>; constructor() { this.subs = []; } addSub(sub: Watcher) { // 蒐集全部使用到這個 data 的 Watcher 對象。 this.subs.push(sub); } depend() { if (Dep.target) { // 蒐集依賴,最終會調用上面的 addSub 方法 Dep.target.addDep(this); } } notify() { const subs = this.subs.slice(); for (let i = 0, l = subs.length; i < l; i++) { // 調用對應的 Watcher,更新視圖 subs[i].update(); } } }
根據對 Dep 的源碼分析,咱們獲得了下面這張邏輯圖:3d
瞭解 Data 和 Dep 以後,咱們來繼續揭開 Watcher 的面紗。
class Watcher { constructor(vm: Component, expOrFn: string | Function) { // 將 vm._render 方法賦值給 getter。 // 這裏的 expOrFn 其實就是 vm._render,後文會講到。 this.getter = expOrFn; this.value = this.get(); } get() { // 給 Dep.target 賦值爲當前 Watcher 對象 Dep.target = this; // this.getter 其實就是 vm._render // vm._render 用來生成虛擬 dom、執行 dom-diff、更新真實 dom。 const value = this.getter.call(this.vm, this.vm); return value; } addDep(dep: Dep) { // 將當前的 Watcher 添加到 Dep 收集池中 dep.addSub(this); } update() { // 開啓異步隊列,批量更新 Watcher queueWatcher(this); } run() { // 和初始化同樣,會調用 get 方法,更新視圖 const value = this.get(); } }
源碼中咱們看到,Watcher 實現了渲染方法 _render
和 Dep 的關聯, 初始化 Watcher 的時候,打上 Dep.target 標識,而後調用 get 方法進行頁面渲染。加上上文的 Data,目前 Data、Dep、Watcher 三者的關係以下:
咱們再拉通串一下整個流程:Vue 經過 defineProperty
完成了 Data 中全部數據的代理,當數據觸發 get 查詢時,會將當前的 Watcher 對象加入到依賴收集池 Dep 中,當數據 Data 變化時,會觸發 set 通知全部使用到這個 Data 的 Watcher 對象去 update 視圖。
目前的總體流程以下:
上圖的流程中 Data 和 Dep 都是 Vue 初始化時建立的,但如今咱們並不知道 Wacher 是從哪裏建立的,帶着這個問題,咱們接着往下探索。
上文中,咱們分析了初始化 Vue 過程當中處理數據的部分,接下來,咱們分析一下數據渲染的部分。
其實 new Vue 執行到最後,會調用 mount 方法,將 Vue 實例渲染成 dom 。
// new Vue 執行流程。 // 1. Vue.prototype._init(option) // 2. vm.$mount(vm.$options.el) // 3. render = compileToFunctions(template) ,編譯 Vue 中的 template 模板,生成 render 方法。 // 4. Vue.prototype.$mount 調用上面的 render 方法掛載 dom。 // 5. mountComponent // 6. 建立 Watcher 實例 const updateComponent = () => { vm._update(vm._render()); }; // 結合上文,咱們就能得出,updateComponent 就是傳入 Watcher 內部的 getter 方法。 new Watcher(vm, updateComponent); // 7. new Watcher 會執行 Watcher.get 方法 // 8. Watcher.get 會執行 this.getter.call(vm, vm) ,也就是執行 updateComponent 方法 // 9. updateComponent 會執行 vm._update(vm._render()) // 10. 調用 vm._render 生成虛擬 dom Vue.prototype._render = function (): VNode { const vm: Component = this; const { render } = vm.$options; let vnode = render.call(vm._renderProxy, vm.$createElement); return vnode; }; // 11. 調用 vm._update(vnode) 渲染虛擬 dom Vue.prototype._update = function (vnode: VNode) { const vm: Component = this; if (!prevVnode) { // 初次渲染 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false); } else { // 更新 vm.$el = vm.__patch__(prevVnode, vnode); } }; // 12. vm.__patch__ 方法就是作的 dom diff 比較,而後更新 dom,這裏就不展開了。
看完 Vue 模板渲染的過程,咱們能夠獲得以下的流程圖:
到這裏,咱們就知道了 Watcher 實際上是在 Vue 初始化的階段建立的,屬於生命週期中 beforeMount 的位置建立的,建立 Watcher 時會執行 render 方法,最終將 Vue 代碼渲染成真實的 DOM。
咱們再將以前的流程整合一下,就能獲得如下的流程:
上圖分析了 Vue 初始化到渲染 DOM 的整個過程,最後咱們再分析一下,當數據變化時,Vue 又是怎麼進行更新的?
其實,在上圖也能看出,在 Data 變化時,會調用 Dep.notify 方法,隨即調用 Watcher 內部的 update 方法,此方法會將全部使用到這個 Data 的 Watcher 加入一個隊列,並開啓一個異步隊列進行更新,最終執行 _render
方法完成頁面更新。
總體的流程以下:
好了,探索到這裏,Vue 的響應式原理,已經被咱們分析透徹了,若是你尚未明白,不妨再細品一下上圖。
原本探索到上面的流程圖就結束了,但好奇的我又想到了一個問題 😂。
Vue 組件又是怎麼渲染的呢?
帶着這個問題,我繼續查閱了源碼。
// 從模板編譯開始,當發現一個自定義組件時,會執行如下函數 // 1. compileToFunctions(template) // 2. compile(template, options); // 3. const ast = parse(template.trim(), options) // 4. const code = generate(ast, options) // 5. createElement // 6. createComponent export function createComponent( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // $options._base 其實就是全局 Vue 構造函數,在初始化時 initGlobalAPI 中定義的:Vue.options._base = Vue const baseCtor = context.$options._base; // Ctor 就是 Vue 組件中 <script> 標籤下 export 出的對象 if (isObject(Ctor)) { // 將組件中 export 出的對象,繼承自 Vue,獲得一個構造函數 // 至關於 Vue.extend(YourComponent) Ctor = baseCtor.extend(Ctor); } const vnode = new VNode(`vue-component-${Ctor.cid}xxx`); return vnode; } // 7. 實現組件繼承 Vue,並調用 Vue._init 方法,進行初始化 Vue.extend = function (extendOptions: Object): Function { const Super = this; const Sub = function VueComponent(options) { // 調用 Vue.prototype._init,以後的流程就和首次加載保持一致 this._init(options); }; // 原型繼承,至關於:Component extends Vue Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; return Sub; };
看完組件渲染的源碼後,結合上文,從新整理了一張流程圖,圖中的藍色部分就是渲染組件的過程。
好了,如今是真的結束了,最終的流程圖就是上面的這一張圖。
問個問題,如今你理解 Vue 響應式原理了嗎?
若是仍以爲很差理解,我這裏還準備了一張帶標註的簡圖 😂
本文從源碼的角度,介紹了 Vue 響應式原理,來簡單回顧一下吧。
最後,若是你對此有任何想法,歡迎留言評論!