一、虛擬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
其中有兩個關鍵函數:
二、Diff演演算法:
diff 比對兩個新舊vnode的過程主要是在 patch 函數(patchVnode函數)中進行
正常情況下兩棵樹之間作比對,那麼第一遍歷tree1,第二遍歷tree2,第三排序,三次遍歷,時間複雜度為 O(n ^ 3)節點太多,演演算法就不可用
框架中diff演演算法的優化:
h 函數用來生成vnode,vnode函數如下:
返回一個js物件結構的虛擬DOM(vnode):
1.children和text是不能共存的,要麼裡面是純text文字,要麼是子元素
2.elm 就是vnode對應的那個DOM元素
3.key 就相當於 v-for 裡面的 key,是我們在使用 v-for 的時候需要自己手動加上
初始化:第一次執行patch,patch(container,vnode),建立空的vnode,關聯傳入的dom
更新:判斷是否是相同的vnode,tag(sel選擇器)和key相同,則認為是相同節點,執行patchVnode函數進行比對
否則刪除重建,不做深度比對
原理:
針對新舊 children
定義四個index, oldStartIdx
, oldEndIdx
, newStartIdx
, newEndIdx
,然後進行一個迴圈,在迴圈過程中
idx會一邊累加或者一邊累減,startIdx會累加,endIdx會累減,在這個過程中,指標會慢慢地往中間去移動,當指標重合的時候,說明遍歷結束了,迴圈結束。
在每一輪迴圈過程中的具體的對比過程是:
如果出現下面情況中的一種:開始和開始節點去對比,結束和結束節點對比,結束和開始節點對比,那麼就執行 patchVnode()
函數,進行遞迴比較,
並且指標累加或者累減,往中間移動。 進行下一輪迴圈的時候,指標就指到下一個了 children
key
。sel
是否相等,如果sel
不相等,那還是沒對應上,說明節點是新的,那也找地方插入新的。sel
相等,key
相等,那麼繼續對這兩個相同的節點執行 patchVnode
方法,遞迴比較。
如果檢測出新節點中的 key 在舊節點上有對應的 key ,在進行交換位置的操作時,就沒有必要銷燬,由此提升效能
三、模板編譯
零、前置知識點: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))])}
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}})])
}
// 條件 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)}
//事件 const template = ` <button @click="clickHandler">submit</button> ` with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
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 函數形式
四、初次渲染與更新過程
五、非同步渲染--this$nextTick()
vue元件是非同步渲染的。程式碼沒執行完,DOM不會立即渲染。this.$nextTick 會在DOM渲染完成時回撥
頁面渲染時會將 data 的修改做一個整合,多次 data 的修改 最後只會渲染一個最終值
六、元件化
對於雙向繫結的理解,就是使用者更新了View,Model的資料也自動被更新了,這種情況就是雙向繫結。
再說細點,就是在單向繫結的基礎上給可輸入元素input、textare等新增了change(input)事件,(change事件觸發,View的狀態就被更新了)來動態修改model。
MVC和MVVM的區別並不是VM完全取代了C,ViewModel存在目的在於抽離Controller中展示的業務邏輯,而不是替代Controller
其它檢視操作業務等還是應該放在Controller中實現。也就是說MVVM實現的是業務邏輯元件的重用。
MVC中Controller演變成MVVM中的ViewModel
MVVM通過資料來顯示檢視層而不是節點操作
MVVM主要解決了MVC中大量的dom操作使頁面渲染效能降低,載入速度變慢,影響使用者體驗等問題。
七、響應式
參照:
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