一、虛擬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不能同時存在)
- 如果新舊vnode都有 children ,呼叫 updateChildren() ,再繼續進行更新
- 如果新vnode有 children ,舊vnode無 children,呼叫 addVnodes() 新增 children 到 elm 上
- 如果新vnode無 children ,舊vnode有 children,呼叫 removeVnodes() 移除 children
- 如果新舊vnode都無 childre且舊vnode有 text,則把elm的 text 設定為空
- 如果新Vnode沒有 children,只有 text且值也不同,就移除舊vnode的children
- updateChildren函式:
原理:
針對新舊 children
定義四個index, oldStartIdx
, oldEndIdx
, newStartIdx
, newEndIdx
,然後進行一個迴圈,在迴圈過程中
idx會一邊累加或者一邊累減,startIdx會累加,endIdx會累減,在這個過程中,指標會慢慢地往中間去移動,當指標重合的時候,說明遍歷結束了,迴圈結束。
在每一輪迴圈過程中的具體的對比過程是:
如果出現下面情況中的一種:開始和開始節點去對比,結束和結束節點對比,結束和開始節點對比,那麼就執行 patchVnode()
函式,進行遞迴比較,
並且指標累加或者累減,往中間移動。 進行下一輪迴圈的時候,指標就指到下一個了 children
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 執行 patch 和 diff 演算法
注意:使用webpack,vue-loader,會在開發環境編譯模板,所以最後打包出來產生的程式碼就沒有模板程式碼,全部都是 render 函式形式
- render 函式:在vue元件中可以使用 render 代替 template,在某些複雜情況下,可以考慮使用render
四、初次渲染與更新過程
- 初次渲染:
- 首先解析模板為 render 函式(模板編譯)
- 觸發響應式,監聽data屬性,設定getter、setter
- 執行 render 函式,生成 vnode,patch(elm,vnode)
- 更新過程:
- 修改 data,觸發 setter(此前在getter中已被監聽)
- 重新執行 render 函式,生成newVnode
- 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