vue原理:diff、模板編譯、渲染過程等

2023-02-14 06:02:34

一、虛擬DOM:

因為DOM操作非常消耗效能,在操作DOM時,會出現DOM的迴流(Reflow:元素大小或者位置發生改變)重繪(元素樣式的改變)使DOM重新渲染。

現在的框架Vue和React很少直接操作DOM,因為兩者都是資料驅動檢視,只會對資料進行增刪改的操作

因此,二者使用虛擬DOM(vdom)來解決控制DOM操作的問題:

原理:使用Js模擬DOM結構,把DOM的計算轉移為Js的計算,使用diff演演算法計算出最小的變更,然後根據變更操作DOM

學習diff演演算法需要藉助snabbdom這個vdom庫的原始碼,vue也是參考它實現的

import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";

const patch = init([
  // Init patch function with chosen modules
  classModule, // makes it easy to toggle classes
  propsModule, // for setting properties on DOM elements
  styleModule, // handles styling on elements with support for animations
  eventListenersModule, // attaches event listeners
]);

const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
  h("span", { style: { fontWeight: "bold" } }, "This is bold"),
  " and this is just normal text",
  h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);

// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);

const newVnode = h(
  "div#container.two.classes",
  { on: { click: anotherEventHandler } },
  [
    h(
      "span",
      { style: { fontWeight: "normal", fontStyle: "italic" } },
      "This is now italic type"
    ),
    " and this is still just normal text",
    h("a", { props: { href: "/bar" } }, "I'll take you places!"),
  ]
);

// Second  ` patch `  invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

其中有兩個關鍵函數:

  • h  函數返回一個vnode,他是使用js物件表示的虛擬DOM結構。接收 sel(選擇器)、data(對DOM的js描述)、children(這個虛擬DOM的子vnode元素)
  • patch  函數的作用:一是將vnode渲染為真實的DOM掛載至頁面;二是使用diff演演算法比對兩個vnode的不同,然後重新渲染

二、Diff演演算法:

  • 概述:

diff 比對兩個新舊vnode的過程主要是在 patch 函數(patchVnode函數)中進行

 

正常情況下兩棵樹之間作比對,那麼第一遍歷tree1,第二遍歷tree2,第三排序,三次遍歷,時間複雜度為 O(n ^ 3)節點太多,演演算法就不可用

框架中diff演演算法的優化:

  • 同級比對,不跨級
  • tag不相同,則直接刪除重建,不再深度比對(有可能tag不相同但是tag下面的子元素還是相同的,但是我們不管了,只要tag不相同就刪掉,因為深度比較複雜度太高)
  • tag和key,兩者都相同,則認為是相同節點,不再深度比對,時間複雜度優化至 O(n)
  • 生成vnode:

    h 函數用來生成vnode,vnode函數如下:

    

     返回一個js物件結構的虛擬DOM(vnode):

     1.children和text是不能共存的,要麼裡面是純text文字,要麼是子元素

     2.elm 就是vnode對應的那個DOM元素

     3.key 就相當於 v-for 裡面的 key,是我們在使用 v-for 的時候需要自己手動加上

  • patch函數:

    初始化:第一次執行patch,patch(container,vnode),建立空的vnode,關聯傳入的dom

    更新:判斷是否是相同的vnode,tag(sel選擇器)和key相同,則認為是相同節點,執行patchVnode函數進行比對

    否則刪除重建,不做深度比對

       

  • patchVnode函數(比對):
    • 如果新Vnode有 children,沒有 text(vnode.text === undefined)(text和children不能同時存在)
    1. 如果新舊vnode都有 children ,呼叫 updateChildren() ,再繼續進行更新
    2. 如果新vnode有 children ,舊vnode無 children,呼叫  addVnodes()  新增 children 到 elm 上
    3. 如果新vnode無 children ,舊vnode有 children,呼叫 removeVnodes() 移除 children
    4. 如果新舊vnode都無 childre且舊vnode有 text,則把elm的 text 設定為空
    • 如果新Vnode沒有 children,只有 text且值也不同,就移除舊vnode的children

       

  • updateChildren函數:

    原理:

    針對新舊 children  定義四個index, oldStartIdxoldEndIdxnewStartIdxnewEndIdx ,然後進行一個迴圈,在迴圈過程中

    idx會一邊累加或者一邊累減,startIdx會累加,endIdx會累減,在這個過程中,指標會慢慢地往中間去移動,當指標重合的時候,說明遍歷結束了,迴圈結束。

     在每一輪迴圈過程中的具體的對比過程是:

    如果出現下面情況中的一種:開始和開始節點去對比,結束和結束節點對比,結束和開始節點對比,那麼就執行 patchVnode() 函數,進行遞迴比較,

    並且指標累加或者累減,往中間移動。 進行下一輪迴圈的時候,指標就指到下一個了 children

    如果都沒有上面的四種情況,首先會拿新節點 key,能否對應上 oldChildren 中的某個節點的 key 。
    • 如果沒有對應上,說明這個節點是新的,找個地方插入進去新的就好。
    • 如果對應上了,還要判斷sel是否相等,如果sel不相等,那還是沒對應上,說明節點是新的,那也找地方插入新的。
    • 如果sel相等,key相等,那麼繼續對這兩個相同的節點執行 patchVnode 方法,遞迴比較。

     

  • v-for中key的作用
    • 如果不使用 key ,diff 演演算法中 沒有 key值能夠對應上,會認為節點更新,之後會銷燬對應的vnode,重新渲染元素
    • 如果檢測出新節點中的 key 在舊節點上有對應的 key ,在進行交換位置的操作時,就沒有必要銷燬,由此提升效能

    • key值需要唯一值,如果使用 index,例如在一個 li 陣列中 頭部插入某些dom元素,index值遞增了,但對應的內容卻錯誤了

  

三、模板編譯

零、前置知識點:JS的 with 語法

with語法:改變 {} 內自由變數的查詢規則,,將 {} 內自由變數,當作 obj 的屬性來查詢
如果找不到匹配的 obj 屬性,就會報錯
with 要慎用, 它打破了作用域的規則,易讀性變差

vue模板編譯成什麼?

模板不是html , 有指令、插值、JS 表示式,能實現判斷、迴圈

html是標籤語言,只有JS才能實現判斷、迴圈(圖靈完備的)

因此,模板一定是轉換為某種JS程式碼,模板怎麼轉成js程式碼的過程就是模板編譯

安裝 vue template complier 這個庫,檢視編譯輸出值:

// 引入
const
compiler = require('vue-template-compiler') // 插值 // const template = ` <p>{{message}}</p> ` // 編譯 const res = compiler.compile(template) console.log(res.render)

列印結果:

with(this){return _c('p',[_v(_s(message))])}

其中 this 在vue中就是 vm 範例,所以 _c、_v、_s 就是vue原始碼中的一些函數

// 從 vue 原始碼中找到縮寫函數的含義
function installRenderHelpers (target) {
    target._c = createElement//建立vnode
    target._o = markOnce;
    target._n = toNumber;
    target._s = toString;
    target._l = renderList;
    target._t = renderSlot;
    target._q = looseEqual;
    target._i = looseIndexOf;
    target._m = renderStatic;
    target._f = resolveFilter;
    target._k = checkKeyCodes;
    target._b = bindObjectProps;
    target._v = createTextVNode;
    target._e = createEmptyVNode;
    target._u = resolveScopedSlots;
    target._g = bindObjectListeners;
    target._d = bindDynamicKeys;
    target._p = prependModifier;
}

轉化後:createElement 函數的作用是建立一個 vnode

with(this){return createElement('p',[createTextVNode(toString(message))])}
  • 表示式編譯:表示式會轉變為js程式碼,然後把結果放到vnode裡面去
const template =  ` <p>{{flag ? message : 'no message found'}}</p> ` 
with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
  • 動態屬性:同理
const template =  ` 
    <div id="div1" class="container">
        <img :src="imgUrl"/>
    </div>
 ` 

with(this){return _c('div',
     {staticClass:"container",attrs:{"id":"div1"}},
     [_c('img',{attrs:{"src":imgUrl}})])
}
  • 條件:使用三元表示式來建立不同的vnode節點
// 條件
const template =  ` 
    <div>
        <p v-if="flag === 'a'">A</p>
        <p v-else>B</p>
    </div>
 ` 
with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
  • 迴圈:通過 _l ( renderList )函數,傳入陣列或者物件,即可返回列表vnode
//迴圈
const template =  ` 
    <ul>
        <li v-for="item in list" :key="item.id">{{item.title}}</li>
    </ul>
 ` 
with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
  • 事件:on屬性包含所有的事件繫結
//事件
const template =  ` 
    <button @click="clickHandler">submit</button>
 ` 
with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
  • v-model:原理就是 value 的 attr 加 input 事件監聽的語法糖 最後執行 render 函數,生成vnode
//v-model
const template =  ` <input type="text" v-model="name"> ` 
//主要看 input 事件
with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
  • 總結

   模板編譯的過程:模板編譯為render函數,執行render函數後返回vnode

   之後再基於 vnode 執行 patchdiff 演演算法

   注意:使用webpack,vue-loader,會在開發環境編譯模板,所以最後打包出來產生的程式碼就沒有模板程式碼,全部都是 render 函數形式

  • render 函數:在vue元件中可以使用 render 代替 template,在某些複雜情況下,可以考慮使用render

   

四、初次渲染與更新過程

  • 初次渲染:
  1. 首先解析模板為 render 函數(模板編譯
  2. 觸發響應式,監聽data屬性,設定getter、setter
  3. 執行 render 函數,生成 vnode,patch(elm,vnode)

  

  • 更新過程:
  1. 修改 data,觸發 setter(此前在getter中已被監聽)
  2. 重新執行 render 函數,生成newVnode
  3. patch(vnode,newVnode) (diff演演算法)

   

五、非同步渲染--this$nextTick()

  vue元件是非同步渲染的。程式碼沒執行完,DOM不會立即渲染。this.$nextTick 會在DOM渲染完成時回撥

  頁面渲染時會將 data 的修改做一個整合,多次 data 的修改 最後只會渲染一個最終值    

  

六、元件化

  

  • MVC模式:單向繫結,即Model繫結到View,使用JS程式碼更新Model時,View就會自動更新

  

  • MVVM模式:雙向繫結,實現了View的變動,自動反映在VM,反之亦然。

    對於雙向繫結的理解,就是使用者更新了View,Model的資料也自動被更新了,這種情況就是雙向繫結。

    再說細點,就是在單向繫結的基礎上給可輸入元素input、textare等新增了change(input)事件,(change事件觸發,View的狀態就被更新了)來動態修改model。

  

  • MVC與MVVM的區別 

   MVC和MVVM的區別並不是VM完全取代了C,ViewModel存在目的在於抽離Controller中展示的業務邏輯而不是替代Controller

    其它檢視操作業務等還是應該放在Controller中實現。也就是說MVVM實現的是業務邏輯元件的重用

    MVC中Controller演變成MVVM中的ViewModel

    MVVM通過資料來顯示檢視層而不是節點操作

    MVVM主要解決了MVC中大量的dom操作使頁面渲染效能降低,載入速度變慢,影響使用者體驗等問題。

七、響應式

  • vue2:object.defineProperty()

  

  

  

  • vue3:proxy

   

  

 

參照: 

https://www.shouxicto.com/article/3298.html

https://juejin.cn/post/6995232345749979172#heading-2

https://juejin.cn/post/6995204870114377741

https://blog.csdn.net/gxll499294075/article/details/123667632