Vue組件的渲染更新原理解析


本版本是對於vue2.x的總結,未來等學習了vue3,再完善對比一波!然后就是說,我們這里主要介紹原理部分,所謂二八原則,我們一切都從最重要的說起吧!

一切從這張圖開始

 

讓我們一步步看吧! 

一、初始化

 

在 new Vue() 之后。 Vue 會調用 _init 函數進行初始化,也就是這里的 init 過程,它會初始化生命周期、事件、 props、 methods、 data、 computed 與 watch 等。

二、模板編譯

 

上面就是使用vue template complier(compile編譯可以分成 parse、optimize 與 generate 三個階段),將模板編譯成render函數,執行render函數后,變成vnode。

parse、optimize 與 generate 三個階段

parse

parse 會用正則等方式解析 template 模板中的指令、class、style等數據,形成AST,就是with語法的過程。

optimize

optimize 的主要作用是標記 static 靜態節點,這是 Vue 在編譯過程中的一處優化,后面當 update更新界面時,會有一個 patch 的過程, diff 算法會直接跳過靜態節點,從而減少了比較的過程,優化了 patch 的性能。

generate

generate 是將 AST 轉化成 render function 字符串的過程,得到結果是 render 的字符串以及 staticRenderFns 字符串。

在經歷過 parse、optimize 與 generate 這三個階段以后,組件中就會存在渲染 VNode 所需的 render function 了。

三、vue的響應式原理:

 

 

前置知識: 

observer (value) ,其中 value(需要「響應式」化的對象)。
defineReactive ,這個方法通過 Object.defineProperty 來實現對對象的「響應式」化,入參是一個 obj(需要綁定的對象)、key(obj的某一個屬性),val(具體的值)。
對象被讀,就是說,這個值已經在頁面中使用或則說已經使用插值表達式插入。
正式知識: 

 1.首先我們一開始會進行響應式初始化,也即是我們開始前的哪個init過程,通過observer (value) 方法,然后通過defineReactive()方法遍歷,對每個對象的每個屬性進行setter和getter初始化。

2.依賴收集:我們在閉包中增加了一個 Dep 類的對象,用來收集 Watcher 對象。在對象被「讀」的時候,會觸發 reactiveGetter 函數把當前的 Watcher 對象,收集到 Dep 類中去。之后如果當該對象被「寫」的時候,則會觸發 reactiveSetter 方法,通知 Dep 類調用 notify 來觸發所有 Watcher 對象的 update 方法更新對應視圖。

附加知識點:object.defineproperty()的缺點

我們知道vue響應式主要使用的是object.defineproperty()這個api,那他也會帶來一些缺點:

需要深度監聽,需要遞歸到底,一次性計算量大(比如引用類型層級較深)

無法監聽新增屬性/刪除屬性,需要使用Vue.set和Vue.delete才行
無法監聽原生數組,需要重寫數組方法
 四、虛擬dom

DOM操作非常耗時,所以使用VDOM,我們把計算轉移為JS計算,
VDOM-用JS模擬DOM結構,計算出最小的變更,操作DOM
因為有了虛擬DOM,所以讓Vue有了跨平台的能力
看一道題目:將下面的東西手寫成vdom/vnode結構

1 <div id="div1" class="container">
2     <p>vdom</p>
3     <ul style="font-size:12px"></ul>
4 </div>


vue3 已經重寫了vdom的代碼,優化了性能,但是理念不變!

五、patch函數,diff算法上台

 

這部分涉及算法

前置知識:

insert:在父幾點下插入節點,如果指定ref則插入道ref這個子節點的前面。
createElm:用來新建一些節點,tag節點存在創建一個標簽節點,否則創建一個文本節點。
addVnodes:用來批量調用createElm新建節點。
removeNode:用來移除一個節點
removeVnodes:會批量調用removeNode移除節點
patch函數:

patch的核心就是diff算法,diff算法通過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,所以時間復雜度只有o(n),比較高效,我們看下圖所示:

 

我們看下patch這個函數的demo:

 1 function patch (oldVnode, vnode, parentElm) {
 2     if (!oldVnode) {
 3         addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
 4     } else if (!vnode) {
 5         removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
 6     } else {
 7         if (sameVnode(oldVNode, vnode)) {
 8             patchVnode(oldVNode, vnode);
 9         } else {
10             removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
11             addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
12         }
13     }
14 }

 


首先在 oldVnode(老 VNode 節點)不存在的時候,相當於新的 VNode 替代原本沒有的節點,所以直接用 addVnodes 將這些節點批量添加到 parentElm 上。
如果 vnode(新 VNode 節點)不存在的時候,相當於要把老的節點刪除,所以直接使用 removeVnodes 進行批量的節點刪除即可。
當 oldVNode 與 vnode 都存在的時候,需要判斷它們是否屬於 sameVnode(相同的節點)。如果是則進行patchVnode(比對 VNode )操作,否則刪除老節點,增加新節點 
patchVnode函數:

我們看下關鍵代碼

 1 function patchVnode (oldVnode, vnode) {
 2     // 新老節點相同,直接return
 3     if (oldVnode === vnode) {
 4         return;
 5     }
 6     // 節點是否靜態,並且新老接待你的key相同,只要把老節點拿來用就好了
 7     if (vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
 8         vnode.elm = oldVnode.elm;
 9         vnode.componentInstance = oldVnode.componentInstance;
10         return;
11     }
12  
13     const elm = vnode.elm = oldVnode.elm;
14     const oldCh = oldVnode.children;
15     const ch = vnode.children;
16     // 當VNode是文本節點,直接setTextContent來設置text
17     if (vnode.text) {
18         nodeOps.setTextContent(elm, vnode.text);
19     // 不是文本節點
20     } else {
21         // oldch(老)與ch(新)存在且不同,使用updateChildren()
22         if (oldCh && ch && (oldCh !== ch)) {
23             updateChildren(elm, oldCh, ch);
24         // 只有ch存在,若oldch(老)節點是文本節點,先刪除,再將ch(新)節點插入elm節點下
25         } else if (ch) {
26             if (oldVnode.text) nodeOps.setTextContent(elm, '');
27             addVnodes(elm, null, ch, 0, ch.length - 1);
28         // 同理當只有oldch(老)節點存在,說明需要將oldch(老)節點通過removeVnode全部刪除
29         } else if (oldCh) {
30             removeVnodes(elm, oldCh, 0, oldCh.length - 1)
31         // 當老節點是文本節點,清除其節點內容
32         } else if (oldVnode.text) {
33             nodeOps.setTextContent(elm, '')
34         }
35     }
36 }

 


整理如下:

新老節點相同,直接return
節點是否靜態,並且新老接待你的key相同,只要把老節點拿來用就好了
當VNode是文本節點,直接setTextContent來設置text,若不是文本節點者執行4-7
oldch(老)與ch(新)存在且不同,使用updateChildren()(后面介紹)
只有ch存在,若oldch(老)節點是文本節點,先刪除,再將ch(新)節點插入elm節點下
同理當只有oldch(老)節點存在,說明需要將oldch(老)節點通過removeVnode全部刪除
當老節點是文本節點,清除其節點內容
updateChildren函數

下面是關鍵代碼:

 

直接看我的代碼注釋吧!

 1 // sameVnode() 就是說key,tag,iscomment(注釋節點),data四個同時定義
 2 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
 3   if (!oldStartVnode) {
 4       oldStartVnode = oldCh[++oldStartIdx];
 5   } else if (!oldEndVnode) {
 6       oldEndVnode = oldCh[--oldEndIdx];
 7   // 老節點的開頭與新節點的開頭對比
 8   } else if (sameVnode(oldStartVnode, newStartVnode)) {
 9       patchVnode(oldStartVnode, newStartVnode);
10       oldStartVnode = oldCh[++oldStartIdx];
11       newStartVnode = newCh[++newStartIdx];
12   // 老節點的結尾與新節點的結尾對比
13   } else if (sameVnode(oldEndVnode, newEndVnode)) {
14       patchVnode(oldEndVnode, newEndVnode);
15       oldEndVnode = oldCh[--oldEndIdx];
16       newEndVnode = newCh[--newEndIdx];
17   // 老節點的開頭與新節點的結尾
18   } else if (sameVnode(oldStartVnode, newEndVnode)) {
19       patchVnode(oldStartVnode, newEndVnode);
20       nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
21       oldStartVnode = oldCh[++oldStartIdx];
22       newEndVnode = newCh[--newEndIdx];
23   // 老節點的結尾與新節點的開頭
24   } else if (sameVnode(oldEndVnode, newStartVnode)) {
25       patchVnode(oldEndVnode, newStartVnode);
26       nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
27       oldEndVnode = oldCh[--oldEndIdx];
28       newStartVnode = newCh[++newStartIdx];
29   // 如果上面的情況都沒有滿足
30   } else {
31       // 把老的元素進行移動
32       let elmToMove = oldCh[idxInOld];
33       // 如果老的節點找不到對應索引則創建
34       if (!oldKeyToIdx) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
35       // 在新節點中的key值找到老節點索引
36       idxInOld = newStartVnode.key ? oldKeyToIdx[newStartVnode.key] : null;
37       // 如果沒有找到相同的節點,則通過 createElm 創建一個新節點,並將 newStartIdx 向后移動一位。
38       if (!idxInOld) {
39           createElm(newStartVnode, parentElm);
40           newStartVnode = newCh[++newStartIdx];
41       // 否則如果找到了節點,同時它符合 sameVnode,則將這兩個節點進行 patchVnode,將該位置的老節點賦值 undefined
42       } else {
43           // 這是是想把相同的節點進行移動
44           elmToMove = oldCh[idxInOld];
45           // 然后再進行對比
46           if (sameVnode(elmToMove, newStartVnode)) {
47               patchVnode(elmToMove, newStartVnode);
48               oldCh[idxInOld] = undefined;
49               nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
50               newStartVnode = newCh[++newStartIdx];
51               // 如果不符合 sameVnode,只能創建一個新節點插入到 parentElm 的子節點中,newStartIdx 往后移動一位。
52           } else {
53               createElm(newStartVnode, parentElm);
54               newStartVnode = newCh[++newStartIdx];
55           }
56       }
57   }
58 }
59 // 當oldStartIdx > oldEndIdx 或oldStartIdx> oldEndIdx說明結束
60 if (oldStartIdx > oldEndIdx) {
61   refElm = (newCh[newEndIdx + 1]) ? newCh[newEndIdx + 1].elm : null;
62   addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx);
63 } else if (newStartIdx > newEndIdx) {
64   removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
65 }
66 }

 


上面涉及了很多東西,也不是一時半會能夠講完的,看代碼的過程也挺艱辛的!

最后總結下渲染過程

初次渲染:

解析模板為render函數(或再開發環境已完成)
觸發響應式,監聽data屬性的getter的依賴收集,也即是往dep里面添加watcher的過程
執行render函數,生成vnode,patch
更新過程:

修改data,setter(必需是初始渲染已經依賴過的)調用Dep.notify(),將通知它內部的所有的Watcher對象進行視圖更新
重新執行rendern函數,生成newVnode
然后就是patch的過程(diff算法)
在過不久,vue3就要到來,很多的API也會更愛,像增加proxy代理的方式,也更改了編譯模板的方式,這大大的影響你的格局,未來將要到來,我將不斷奔跑!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM