這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 最近部門分享,有同學提到了 Vue 響應式原理,大家在討論時,發現一些同學對這一知識理解還不夠深入,不能形成一個閉環,為了幫助大家理解這個問題,我重新過了一下 Vue 源碼,並整理了多張流程圖,便於大家理解。 Vue 初始化 模板渲染 組 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
最近部門分享,有同學提到了 Vue 響應式原理,大家在討論時,發現一些同學對這一知識理解還不夠深入,不能形成一個閉環,為了幫助大家理解這個問題,我重新過了一下 Vue 源碼,並整理了多張流程圖,便於大家理解。
- Vue 初始化
- 模板渲染
- 組件渲染
本文 Vue 源碼版本:2.6.11,為了便於理解,均有所刪減。
本文將從以下兩個方面進行探索:
-
從 Vue 初始化,到首次渲染生成 DOM 的流程。
-
從 Vue 數據修改,到頁面更新 DOM 的流程。
Vue 初始化
先從最簡單的一段 Vue 代碼開始:
<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 對象。
對於數據的 get、set 綁定我們並不陌生,但是 Dep 對象什麼呢?
Dep 對象用於依賴收集,它實現了一個發佈訂閱模式,完成了數據 Data 和渲染視圖 Watcher 的訂閱,我們一起來剖析一下。
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 的源碼分析,我們得到了下麵這張邏輯圖:
瞭解 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 的響應式原理,已經被我們分析透徹了,如果你還沒有明白,不妨再細品一下上圖。
組件渲染
本來探索到上面的流程圖就結束了,但好奇的我又想到了一個問題