關(guān)于Virtual DOM理解和Snabbdom源碼淺析
什么是Virtual DOM
Virtual DOM(虛擬DOM),在形態(tài)上表現(xiàn)為一個(gè)能夠描述DOM結(jié)構(gòu)及其屬性信息的普通的JS對(duì)象,因?yàn)椴皇钦鎸?shí)的DOM對(duì)象,所以叫虛擬DOM。
<div></div>
{
sel: 'div',
data: {},
chidren:undefined,
elm:undefined,
key:undefined,
}
Virtual DOM 本質(zhì)上JS和DOM之間的一個(gè)映射緩存??梢灶?lèi)比 CPU 和硬盤(pán),既然硬盤(pán)這么慢,我們就在它們之間加個(gè)緩存:既然 DOM 這么慢,我們就在它們 JS 和 DOM 之間加個(gè)緩存。CPU(JS)只操作內(nèi)存(Virtual DOM),最后的時(shí)候再把變更寫(xiě)入硬盤(pán)(DOM)。
為什么需要Virtual DOM
手動(dòng)操作DOM比較麻煩,還需要考慮瀏覽器兼容性問(wèn)題,雖然有jquery等庫(kù)簡(jiǎn)化DOM操作,但是隨著項(xiàng)目的復(fù)雜DOM操作復(fù)雜提升。 為了簡(jiǎn)化DOM的復(fù)雜操作于是出現(xiàn)了MVVM框架,MVVM框架解決了視圖和狀態(tài)的同步問(wèn)題。 為了簡(jiǎn)化視圖的操作我們可以使用模板引擎,但是模板引擎沒(méi)有解決跟蹤狀態(tài)變化的問(wèn)題,于是Virtual DOM出現(xiàn)了。 Virtual DOM的好處是當(dāng)狀態(tài)改變時(shí)不需要立即更新DOM,只需要?jiǎng)?chuàng)建一個(gè)虛擬樹(shù)來(lái)描述DOM,Virtual DOM內(nèi)部將弄清除如何有效(diff)的更新DOM。 虛擬DOM可以維護(hù)程序的狀態(tài),跟蹤上一次的狀態(tài),通過(guò)比較前后兩次狀態(tài)的差異更新真實(shí)DOM。
Virtual DOM的作用
1、減少對(duì)真實(shí)DOM的操作
真實(shí)DOM 因?yàn)闉g覽器廠商需要實(shí)現(xiàn)眾多的規(guī)范(各種 HTML5 屬性、DOM事件),即使創(chuàng)建一個(gè)空的 div 也要付出昂貴的代價(jià)。如以下代碼,打印空的div屬性一共298個(gè)。而這僅僅是第一層。真正的 DOM 元素非常龐大。直接操作DOM可能會(huì)導(dǎo)致頻繁的回流和重繪。
const div = document.createElement('div');
const arr = [];
for(key in div){arr.push(key)};
console.log(arr.length); // 298
對(duì)復(fù)雜的文檔DOM結(jié)構(gòu)(復(fù)雜視圖情況下提升渲染性能),提供一種方便的工具,進(jìn)行最小化地DOM操作。既然我們可以用JS對(duì)象表示DOM結(jié)構(gòu),那么當(dāng)數(shù)據(jù)狀態(tài)發(fā)生變化而需要改變DOM結(jié)構(gòu)時(shí),我們先通過(guò)JS對(duì)象表示的虛擬DOM計(jì)算出實(shí)際DOM需要做的最小變動(dòng)(Virtual DOM會(huì)使用diff算法計(jì)算出如果有效的更新dom,只更新?tīng)顟B(tài)改變的DOM),然后再操作實(shí)際DOM,從而避免了粗放式的DOM操作帶來(lái)的性能問(wèn)題,減少對(duì)真實(shí)DOM的操作。
2、無(wú)需手動(dòng)操作 DOM,維護(hù)視圖和狀態(tài)的關(guān)系
我們不再需要手動(dòng)去操作 DOM,只需要寫(xiě)好 View-Model 的代碼邏輯,MVVM框架會(huì)根據(jù)虛擬 DOM 和 數(shù)據(jù)雙向綁定,幫我們以可預(yù)期的方式更新視圖,極大提高我們的開(kāi)發(fā)效率。
3、跨平臺(tái)
虛擬DOM是對(duì)真實(shí)的渲染內(nèi)容的一層抽象,是真實(shí)DOM的描述,因此,它可以實(shí)現(xiàn)“一次編碼,多端運(yùn)行”,可以實(shí)現(xiàn)SSR(Nuxt.js/Next.js)、原生應(yīng)用(Weex/React Native)、小程序(mpvue/uni-app)等。
Virtual DOM有什么不足
上面我們也說(shuō)到了在復(fù)雜視圖情況下提升渲染性能。雖然虛擬 DOM + 合理的優(yōu)化,足以應(yīng)對(duì)絕大部分應(yīng)用的性能需求,但在一些性能要求極高的應(yīng)用中虛擬DOM 無(wú)法進(jìn)行針對(duì)性的極致優(yōu)化。首次渲染大量DOM時(shí),由于多了一層虛擬DOM的計(jì)算,會(huì)比innerHTML插入慢。
下方是尤大自己的見(jiàn)解。https://www.zhihu.com/question/31809713/answer/53544875
Virtual DOM庫(kù)
virtual-dom
一個(gè)JavaScript DOM模型,支持元素創(chuàng)建,差異計(jì)算和補(bǔ)丁操作,以實(shí)現(xiàn)高效的重新渲染。
源代碼庫(kù)地址:https://github.com/Matt-Esch/virtual-dom.git 已經(jīng)有五六年沒(méi)有維護(hù)了
snabbdom
一個(gè)虛擬DOM庫(kù),重點(diǎn)放在簡(jiǎn)單性,模塊化,強(qiáng)大的功能和性能上。
源代碼庫(kù)地址:https://github.com/snabbdom/snabbdom.git 最近一直在維護(hù)
為什么要介紹Virtual DOM庫(kù)Snabbdom
Vue2.x內(nèi)部使用的Virtual DOM就是改造的Snabbdom; 核心代碼大約200行; 通過(guò)模塊可擴(kuò)展; 源碼使用TypeScript開(kāi)發(fā); 最快的Virtual DOM之一; 最近在維護(hù)
Snabbdom核心
使用 h()函數(shù)創(chuàng)建 JavaScript 對(duì)象(Vnode)描述真實(shí) DOM init()設(shè)置模塊,創(chuàng)建 patch() patch()比較新舊兩個(gè) Vnode 把變化的內(nèi)容更新到真實(shí) DOM 樹(shù)上
Snabbdom搭建項(xiàng)目
第一步,初始化項(xiàng)目
npm init -y
or
yarn init -y
第二步,安裝依賴(lài)
安裝snabbdom
npm install snabbdom
or
yarn add snabbdom
安裝parcel-bundler
npm install parcel-bundler
or
yarn add parcel-bundler
第三步,創(chuàng)建文件夾/文件,編輯文件
在根目錄下創(chuàng)建一個(gè)名為src的文件目錄,然后在里面創(chuàng)建一個(gè)main.js文件。最后,在根目錄下創(chuàng)建一個(gè)index.html文件。
package.json文件可以編輯如下,更利于操作。
"scripts": {
"serve": "parcel index.html --open",
"build": "parcel build index.html"
},
第四步,編輯文件內(nèi)容
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>snabbdomApp</title>
</head>
<body>
<div id="app"></div>
<script src="src/main.js"></script>
</body>
</html>
main.js
主要介紹snabbdom中兩個(gè)核心init()、h()。
init() 是一個(gè)高階函數(shù),返回patch()(對(duì)比兩個(gè)VNode的差異更新到真實(shí)DOM); h() 返回虛擬節(jié)點(diǎn)VNode;
示例1
import { h, init } from 'snabbdom';
// init函數(shù)參數(shù):數(shù)組(模塊)
// 返回值:patch函數(shù):作用是對(duì)比兩個(gè)Vnode的差異更新到真實(shí)dom
const patch = init([]);
// h函數(shù)
// 第一個(gè)參數(shù):標(biāo)簽+選擇器;
// 第二個(gè)參數(shù):如果是字符串則是標(biāo)簽的內(nèi)容
let vnode = h('div#container', 'Hello World');
const app = document.querySelector('#app');
// patch函數(shù)
// 第一個(gè)參數(shù):可以是DOM元素(內(nèi)部會(huì)把DOM元素轉(zhuǎn)化為Vnode),也可以是Vnode;
// 第二個(gè)參數(shù):Vnode
// 返回值:Vnode
let oldVnode = patch(app, vnode);
// 新舊Vnode對(duì)比
vnode = h('div', 'Hello Snabbdom');
patch(oldVnode, vnode);
示例2
import { h, init } from 'snabbdom';
const patch = init([]);
// 可放置子元素
let vnode = h('div#container', [h('h1', '1'), h('h2', '2')]);
const app = document.querySelector('#app');
const oldVnode = patch(app, vnode);
vnode = h('div', 'Hello Snabbdom');
patch(oldVnode, vnode);
setInterval(() => {
// 清除頁(yè)面元素
patch(oldVnode, h('!'));
}, 3000);
示例3
常用模塊
The attributes module
設(shè)置DOM元素的特性,使用setAttribute添加和更新特性。
The props module
允許設(shè)置DOM元素的屬性。
The class module
提供了一種動(dòng)態(tài)切換元素上的類(lèi)的簡(jiǎn)單方法。
The style module
允許在元素上設(shè)置CSS屬性。請(qǐng)注意,如果樣式屬性作為樣式對(duì)象的屬性被移除,樣式模塊并不會(huì)移除它們。為了移除一個(gè)樣式,應(yīng)該將其設(shè)置為空字符串。
The dataset module
允許在DOM元素上設(shè)置自定義數(shù)據(jù)屬性(data-*)。
The eventlisteners module
提供了附加事件監(jiān)聽(tīng)器的強(qiáng)大功能。
import {
h,
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
} from 'snabbdom';
const patch = init([
styleModule,
classModule,
propsModule,
eventListenersModule,
]);
let vnode = h(
'div#container',
{
style: {
color: '#000',
},
on: {
click: eventHandler,
},
},
[
h('p', 'p1', h('a', { class: { active: true, selected: true } }, 'Toggle')),
h('p', 'p2'),
h('a', { props: { href: '/' } }, 'Go to'),
]
);
function eventHandler() {
console.log('1');
}
const app = document.querySelector('#app');
patch(app, vnode);
snabbdom源碼淺析
源碼地址:https://github.com/snabbdom/snabbdom.git以下分析snabbdom版本3.0.1。
源碼核心文件目錄及其文件
核心文件夾是**src目錄。**里面包含了如下文件夾及其目錄:
helpers:里面只有一個(gè)文件attachto.ts,這個(gè)文件主要作用是定義了幾個(gè)類(lèi)型在vnode.ts文件中使用。 modules:里面存放著snabbdom模塊,分別是attributes.ts、class.ts、dataset.ts、eventlisteners.ts、props.ts、style.ts這6個(gè)模塊。另外一個(gè)module.ts這個(gè)文件為它們提供了鉤子函數(shù)。 h.ts:創(chuàng)建Vnode。 hook.ts:提供鉤子函數(shù)。 htmldomapi:提供了DOM API。 index.ts:snabbdom 入口文件。 init.ts:導(dǎo)出了patch函數(shù)。 is.ts:導(dǎo)出了兩個(gè)方法。一個(gè)方法是判斷是否是數(shù)組,另一個(gè)判斷是否是字符串或數(shù)字。 jsx.ts:與jsx相關(guān)。 thunk.ts:與優(yōu)化key相關(guān)。 tovnode.ts:真實(shí)DOM 轉(zhuǎn)化為 虛擬DOM。 vnode.ts:定義了Vnode的結(jié)構(gòu)。
核心文件淺析
h.ts
h 函數(shù)最早見(jiàn)于 hyperscript,使用 JavaScript 創(chuàng)建超文本,Snabbdom 中的 h 函數(shù)不是用來(lái)創(chuàng)建超文本,而是創(chuàng)建 Vnode。 在使用 Vue2.x 的時(shí)候見(jiàn)過(guò) h 函數(shù),它的參數(shù)就是h函數(shù),但是Vue加強(qiáng)了h函數(shù),使其支持組件機(jī)制。
new Vue({
router,
store,
render:h => h(App)
}).$mount('#app)
以上是h.ts文件中的內(nèi)容,可以看到它導(dǎo)出了多個(gè)h方法,這種方式叫做函數(shù)重載。在JS中暫時(shí)沒(méi)有,目前TS支持這種機(jī)制(但也只是通過(guò)調(diào)整代碼參數(shù)層面上,因?yàn)樽罱KTS還是要轉(zhuǎn)換為JS)。方法名相同,參數(shù)個(gè)數(shù)或類(lèi)型不同的方法叫做函數(shù)重載。所以通過(guò)參數(shù)個(gè)數(shù)或類(lèi)型不同來(lái)區(qū)分它們。
// 這里通過(guò)參數(shù)不同來(lái)區(qū)分不同的函數(shù)
function add(a, b) {
console.log(a + b);
}
function add(a, b, c) {
console.log(a + b + c);
}
add(1, 2);
add(1, 2, 3);
從上面代碼層面上我們知道了通過(guò)函數(shù)重載這種方法可以在通過(guò)參數(shù)個(gè)數(shù)或類(lèi)型不同輕松地實(shí)現(xiàn)了相應(yīng)情況調(diào)用相應(yīng)參數(shù)的方法。
那么,我們來(lái)具體看下源碼是怎么實(shí)現(xiàn)函數(shù)重載的。
通過(guò)源碼我們看到,通過(guò)傳入不同的類(lèi)型的參數(shù)調(diào)用對(duì)應(yīng)的代碼,最后將將參數(shù)傳入到vnode方法中,創(chuàng)建一個(gè)Vnode,并返回這個(gè)方法。
那么接下來(lái),我們看下vnode方法的實(shí)現(xiàn)。
vnode.ts
我們打開(kāi)vnode.ts這個(gè)文件,這個(gè)文件主要是導(dǎo)出了一個(gè)vnode方法,并且定義了幾個(gè)接口。我們看到以下代碼中vnode中的參數(shù)含義就知道在h.ts文件中函數(shù)參數(shù)的意思,是相對(duì)應(yīng)的。

init.ts
在介紹init.ts文件之前的,我們需要知道這樣的一個(gè)概念:
init()是一個(gè)高階函數(shù),返回patch() patch(oldVnode,newVnode) 把新節(jié)點(diǎn)中的變化的內(nèi)容渲染到真實(shí)DOM,最后返回新節(jié)點(diǎn)作為下一次處理的舊節(jié)點(diǎn)
這個(gè)概念我們?cè)谏厦嬉呀?jīng)闡述了。**init()**就是通過(guò)這個(gè)文件導(dǎo)出的。
在看init.ts源碼之前,我們還需要了解Vnode是渲染到真實(shí)DOM的整體流程。這樣,看源碼才不會(huì)有誤解。
整體流程:
對(duì)比新舊Vnode是否相同節(jié)點(diǎn)(節(jié)點(diǎn)數(shù)據(jù)中的key、sel、is相同) 如果不是相同節(jié)點(diǎn),刪除之前的內(nèi)容,重新渲染 如果是相同節(jié)點(diǎn),再判斷新的Vnode是否有text,如果有并且和oldVnode 的text 不同,直接更新文本內(nèi)容 如果新的Vnode 有children,判斷子節(jié)點(diǎn)是否有變化,判斷子節(jié)點(diǎn)的過(guò)程就是diff 算法 diff 算法過(guò)程只進(jìn)行同層級(jí)節(jié)點(diǎn)比較
Diff算法的作用是用來(lái)計(jì)算出 Virtual DOM 中被改變的部分,然后針對(duì)該部分進(jìn)行原生DOM操作,而不用重新渲染整個(gè)頁(yè)面。
同級(jí)對(duì)比
對(duì)比的時(shí)候,只針對(duì)同級(jí)的對(duì)比,減少算法復(fù)雜度。
就近復(fù)用
為了盡可能不發(fā)生 DOM 的移動(dòng),會(huì)就近復(fù)用相同的 DOM 節(jié)點(diǎn),復(fù)用的依據(jù)是判斷是否是同類(lèi)型的 dom 元素。 看到這里你可能就會(huì)想到Vue中列表渲染為什么推薦加上key,我們需要使用key來(lái)給每個(gè)節(jié)點(diǎn)做一個(gè)唯一標(biāo)識(shí),Diff算法就可以正確的識(shí)別此節(jié)點(diǎn),找到正確的位置區(qū)插入新的節(jié)點(diǎn)。key的作用主要是為了高效的更新虛擬DOM。
我們先看下init.ts中的大體源碼。
我們先簡(jiǎn)單地來(lái)看下sameVnode方法。判斷是否是相同的虛擬節(jié)點(diǎn)。
// 是否是相同節(jié)點(diǎn)
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
const isSameKey = vnode1.key === vnode2.key;
const isSameIs = vnode1.data?.is === vnode2.data?.is;
const isSameSel = vnode1.sel === vnode2.sel;
return isSameSel && isSameKey && isSameIs;
}
是否是Vnode。
// 是否是vnode
function isVnode(vnode: any): vnode is VNode {
return vnode.sel !== undefined;
}
注冊(cè)一系列的鉤子,在不同的階段觸發(fā)。
// 定義一些鉤子函數(shù)
const hooks: Array<keyof Module> = [
"create",
"update",
"remove",
"destroy",
"pre",
"post",
];
下面呢,主要看下導(dǎo)出的init方法。也是init.ts中最主要的部分,從68行到472行。
// 導(dǎo)出init函數(shù)
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number;
let j: number;
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
// 初始化轉(zhuǎn)化成虛擬節(jié)點(diǎn)的api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 把傳入的所有模塊的鉤子函數(shù),統(tǒng)一存儲(chǔ)到cbs對(duì)象中
// 最終構(gòu)建的cbs對(duì)象的形式cbs = {create:[fn1,fn2],update:[],....}
for (i = 0; i < hooks.length; ++i) {
// cbs.create= [], cbs.update = []...
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
// modules 傳入的模塊數(shù)組
// 獲取模塊中的hook函數(shù)
// hook = modules[0][create]...
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
// 把獲取到的hook函數(shù)放入到cbs 對(duì)應(yīng)的鉤子函數(shù)數(shù)組中
(cbs[hooks[i]] as any[]).push(hook);
}
}
}
function emptyNodeAt(elm: Element) {
const id = elm.id ? "#" + elm.id : "";
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
return vnode(
api.tagName(elm).toLowerCase() + id + c,
{},
[],
undefined,
elm
);
}
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm) as Node;
api.removeChild(parent, childElm);
}
};
}
/*
1.觸發(fā)鉤子函數(shù)init
2.把vnode轉(zhuǎn)換為DOM對(duì)象,存儲(chǔ)到vnode.elm中
- sel是!--》創(chuàng)建注釋節(jié)點(diǎn)
- sel不為空 --》創(chuàng)建對(duì)應(yīng)的DOM對(duì)象;觸發(fā)模塊的鉤子函數(shù)create;創(chuàng)建所有子節(jié)點(diǎn)對(duì)應(yīng)的DOM對(duì)象;觸發(fā)鉤子函數(shù)create;如果是vnode有inset鉤子函數(shù),追加到隊(duì)列
- sel為空 --》創(chuàng)建文本節(jié)點(diǎn)
3.返回vnode.elm
*/
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any;
let data = vnode.data;
if (data !== undefined) {
// 執(zhí)行init鉤子函數(shù)
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
// 把vnode轉(zhuǎn)換成真實(shí)dom對(duì)象(沒(méi)有渲染到頁(yè)面)
const children = vnode.children;
const sel = vnode.sel;
if (sel === "!") {
// 如果選擇器是!,創(chuàng)建注釋節(jié)點(diǎn)
if (isUndef(vnode.text)) {
vnode.text = "";
}
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// 如果選擇器不為空
// 解析選擇器
// Parse selector
const hashIdx = sel.indexOf("#");
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag, data)
: api.createElement(tag, data));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
// 執(zhí)行模塊的create鉤子函數(shù)
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
// 如果vnode中有子節(jié)點(diǎn),創(chuàng)建子Vnode對(duì)應(yīng)的DOM元素并追加到DOM樹(shù)上
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
// 如果vode的text值是string/number,創(chuàng)建文本節(jié)點(diǎn)并追加到DOM樹(shù)
api.appendChild(elm, api.createTextNode(vnode.text));
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
// 執(zhí)行傳入的鉤子 create
hook.create?.(emptyNode, vnode);
if (hook.insert) {
insertedVnodeQueue.push(vnode);
}
}
} else {
// 如果選擇器為空,創(chuàng)建文本節(jié)點(diǎn)
vnode.elm = api.createTextNode(vnode.text!);
}
// 返回新創(chuàng)建的DOM
return vnode.elm;
}
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
function invokeDestroyHook(vnode: VNode) {
const data = vnode.data;
if (data !== undefined) {
// 執(zhí)行的destroy 鉤子函數(shù)
data?.hook?.destroy?.(vnode);
// 調(diào)用模塊的destroy鉤子函數(shù)
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
// 執(zhí)行子節(jié)點(diǎn)的destroy鉤子函數(shù)
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j];
if (child != null && typeof child !== "string") {
invokeDestroyHook(child);
}
}
}
}
}
function removeVnodes(
parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number
): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number;
let rm: () => void;
const ch = vnodes[startIdx];
if (ch != null) {
// 如果sel 有值
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
// 防止重復(fù)調(diào)用
listeners = cbs.remove.length + 1;
// 創(chuàng)建刪除的回調(diào)函數(shù)
rm = createRmCb(ch.elm!, listeners);
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
// 執(zhí)行remove鉤子函數(shù)
const removeHook = ch?.data?.hook?.remove;
if (isDef(removeHook)) {
removeHook(ch, rm);
} else {
// 如果沒(méi)有鉤子函數(shù),直接調(diào)用刪除元素的方法
rm();
}
} else {
// Text node
// 如果是文本節(jié)點(diǎn),直接是調(diào)用刪除元素的方法
api.removeChild(parentElm, ch.elm!);
}
}
}
}
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
/*
對(duì)比兩個(gè)新舊節(jié)點(diǎn),然后找到差異并更新DOM
第一部分
1.觸發(fā)prepatch鉤子函數(shù)
2.觸發(fā)update鉤子函數(shù)
第二部分
1.新節(jié)點(diǎn)有text屬性,且不等于舊節(jié)點(diǎn)的text屬性 -》如果舊節(jié)點(diǎn)有children,移除舊節(jié)點(diǎn)children對(duì)應(yīng)的DOM元素;設(shè)置新節(jié)點(diǎn)對(duì)應(yīng)的DOM元素的textContent
2.新舊節(jié)點(diǎn)都有children,且不相等-》調(diào)用updateChildren();對(duì)比子節(jié)點(diǎn),并且更新子節(jié)點(diǎn)的差異
3.只有新節(jié)點(diǎn)有children屬性-》如果舊節(jié)點(diǎn)有text屬性,清空對(duì)應(yīng)DOM元素的textContent;添加所有的子節(jié)點(diǎn)
4.只有舊節(jié)點(diǎn)有children屬性-》移除所有舊節(jié)點(diǎn)
5.只有舊節(jié)點(diǎn)有text屬性=》清空對(duì)應(yīng)的DOM元素的textContent
第三部分
1.觸發(fā)postpatch鉤子函數(shù)
*/
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
const hook = vnode.data?.hook;
// 首先執(zhí)行prepatch鉤子函數(shù)
hook?.prepatch?.(oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm)!;
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
// 如果新舊vnode相同返回
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
// 執(zhí)行模塊的update鉤子函數(shù)
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
// 執(zhí)行update鉤子函數(shù)
vnode.data.hook?.update?.(oldVnode, vnode);
}
// 如果是vnode.text 未定義
if (isUndef(vnode.text)) {
// 如果是新舊節(jié)點(diǎn)都有 children
if (isDef(oldCh) && isDef(ch)) {
// 使用diff算法對(duì)比子節(jié)點(diǎn),更新子節(jié)點(diǎn)
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) {
// 如果新節(jié)點(diǎn)有children,舊節(jié)點(diǎn)沒(méi)有children
// 如果舊節(jié)點(diǎn)有text,清空dom 元素的內(nèi)容
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
// 批量添加子節(jié)點(diǎn)
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 如果舊節(jié)點(diǎn)有children,新節(jié)點(diǎn)沒(méi)有children
// 批量移除子節(jié)點(diǎn)
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 如果舊節(jié)點(diǎn)有 text,清空 DOM 元素
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
// 如果沒(méi)有設(shè)置 vnode.text
if (isDef(oldCh)) {
// 如果舊節(jié)點(diǎn)有children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 設(shè)置 DOM 元素的textContent為 vnode.text
api.setTextContent(elm, vnode.text!);
}
// 最后執(zhí)行postpatch鉤子函數(shù)
hook?.postpatch?.(oldVnode, vnode);
}
// init 內(nèi)部返回 patch 函數(shù),把vnode渲染成真實(shí)dom,并返回vnode
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 保存新插入的節(jié)點(diǎn)的隊(duì)列,為了觸發(fā)鉤子函數(shù)
const insertedVnodeQueue: VNodeQueue = [];
// 執(zhí)行模塊的pre 鉤子函數(shù)
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果oldVnode不是Vnode,創(chuàng)建Vnode并設(shè)置elm
if (!isVnode(oldVnode)) {
// 把Dom元素轉(zhuǎn)化成空的Vnode
oldVnode = emptyNodeAt(oldVnode);
}
// 如果新舊節(jié)點(diǎn)是相同節(jié)點(diǎn)
if (sameVnode(oldVnode, vnode)) {
// 找節(jié)點(diǎn)的差異并更新DOM,這里的原理就是diff算法
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 如果新舊節(jié)點(diǎn)不同,vnode創(chuàng)建對(duì)應(yīng)的DOM
// 獲取當(dāng)前的DOM元素
elm = oldVnode.elm!;
// 獲取父元素
parent = api.parentNode(elm) as Node;
// 創(chuàng)建Vnode對(duì)應(yīng)的DOM元素,并觸發(fā)init/create 鉤子函數(shù)
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 如果父元素不為空,把vnode對(duì)應(yīng)的DOM插入到父元素中
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
// 移除舊節(jié)點(diǎn)
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 執(zhí)行insert 鉤子函數(shù)
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
// 執(zhí)行模塊的post 鉤子函數(shù)
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
// 返回vnode 作為下次更新的舊節(jié)點(diǎn)
return vnode;
};
}
接下來(lái),我們分開(kāi)介紹init方法中的內(nèi)容。
將各個(gè)模塊的鉤子方法,掛到統(tǒng)一的鉤子上
初始化的時(shí)候,將每個(gè) modules 下的相應(yīng)的鉤子都追加都一個(gè)數(shù)組里面 在進(jìn)行 patch 的各個(gè)階段,觸發(fā)對(duì)應(yīng)的鉤子去處理對(duì)應(yīng)的事情 這種方式比較方便擴(kuò)展。新增鉤子的時(shí)候,不需要更改到主要的流程 這些模塊的鉤子,主要用在更新節(jié)點(diǎn)的時(shí)候,會(huì)在不同的生命周期里面去觸發(fā)對(duì)應(yīng)的鉤子,從而更新這些模塊。
let i: number;
let j: number;
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
// 把傳入的所有模塊的鉤子函數(shù),統(tǒng)一存儲(chǔ)到cbs對(duì)象中
// 最終構(gòu)建的cbs對(duì)象的形式cbs = {create:[fn1,fn2],update:[],....}
for (i = 0; i < hooks.length; ++i) {
// cbs.create= [], cbs.update = []...
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
// modules 傳入的模塊數(shù)組
// 獲取模塊中的hook函數(shù)
// hook = modules[0][create]...
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
// 把獲取到的hook函數(shù)放入到cbs 對(duì)應(yīng)的鉤子函數(shù)數(shù)組中
(cbs[hooks[i]] as any[]).push(hook);
}
}
}
patch方法
init 方法最后返回一個(gè) patch 方法 。
主要的邏輯如下 :
觸發(fā) pre 鉤子 如果舊節(jié)點(diǎn)非 vnode, 則新創(chuàng)建空的 vnode 新舊節(jié)點(diǎn)為 sameVnode 的話(huà),則調(diào)用 patchVnode 更新 vnode , 否則創(chuàng)建新節(jié)點(diǎn) 觸發(fā)收集到的新元素 insert 鉤子 觸發(fā) post 鉤子
// init 內(nèi)部返回 patch 函數(shù),把vnode渲染成真實(shí)dom,并返回vnode
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 保存新插入的節(jié)點(diǎn)的隊(duì)列,為了觸發(fā)鉤子函數(shù)
const insertedVnodeQueue: VNodeQueue = [];
// 執(zhí)行模塊的pre 鉤子函數(shù)
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果oldVnode不是Vnode,創(chuàng)建Vnode并設(shè)置elm
if (!isVnode(oldVnode)) {
// 把Dom元素轉(zhuǎn)化成空的Vnode
oldVnode = emptyNodeAt(oldVnode);
}
// 如果新舊節(jié)點(diǎn)是相同節(jié)點(diǎn)
if (sameVnode(oldVnode, vnode)) {
// 找節(jié)點(diǎn)的差異并更新DOM,這里的原理就是diff算法
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 如果新舊節(jié)點(diǎn)不同,vnode創(chuàng)建對(duì)應(yīng)的DOM
// 獲取當(dāng)前的DOM元素
elm = oldVnode.elm!;
// 獲取父元素
parent = api.parentNode(elm) as Node;
// 創(chuàng)建Vnode對(duì)應(yīng)的DOM元素,并觸發(fā)init/create 鉤子函數(shù)
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 如果父元素不為空,把vnode對(duì)應(yīng)的DOM插入到父元素中
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
// 移除舊節(jié)點(diǎn)
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 執(zhí)行insert 鉤子函數(shù)
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
// 執(zhí)行模塊的post 鉤子函數(shù)
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
// 返回vnode 作為下次更新的舊節(jié)點(diǎn)
return vnode;
};
patchVnode方法
主要的邏輯如下 :
觸發(fā) prepatch 鉤子 觸發(fā) update 鉤子, 這里主要為了更新對(duì)應(yīng)的 module 內(nèi)容 非文本節(jié)點(diǎn)的情況 , 調(diào)用 updateChildren 更新所有子節(jié)點(diǎn) 文本節(jié)點(diǎn)的情況 , 直接 api.setTextContent(elm, vnode.text as string)
這里在對(duì)比的時(shí)候,就會(huì)直接更新元素內(nèi)容了。并不會(huì)等到對(duì)比完才更新 DOM 元素。
/*
對(duì)比兩個(gè)新舊節(jié)點(diǎn),然后找到差異并更新DOM
第一部分
1.觸發(fā)prepatch鉤子函數(shù)
2.觸發(fā)update鉤子函數(shù)
第二部分
1.新節(jié)點(diǎn)有text屬性,且不等于舊節(jié)點(diǎn)的text屬性 -》如果舊節(jié)點(diǎn)有children,移除舊節(jié)點(diǎn)children對(duì)應(yīng)的DOM元素;設(shè)置新節(jié)點(diǎn)對(duì)應(yīng)的DOM元素的textContent
2.新舊節(jié)點(diǎn)都有children,且不相等-》調(diào)用updateChildren();對(duì)比子節(jié)點(diǎn),并且更新子節(jié)點(diǎn)的差異
3.只有新節(jié)點(diǎn)有children屬性-》如果舊節(jié)點(diǎn)有text屬性,清空對(duì)應(yīng)DOM元素的textContent;添加所有的子節(jié)點(diǎn)
4.只有舊節(jié)點(diǎn)有children屬性-》移除所有舊節(jié)點(diǎn)
5.只有舊節(jié)點(diǎn)有text屬性=》清空對(duì)應(yīng)的DOM元素的textContent
第三部分
1.觸發(fā)postpatch鉤子函數(shù)
*/
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
const hook = vnode.data?.hook;
// 首先執(zhí)行prepatch鉤子函數(shù)
hook?.prepatch?.(oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm)!;
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
// 如果新舊vnode相同返回
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
// 執(zhí)行模塊的update鉤子函數(shù)
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
// 執(zhí)行update鉤子函數(shù)
vnode.data.hook?.update?.(oldVnode, vnode);
}
// 如果是vnode.text 未定義
if (isUndef(vnode.text)) {
// 如果是新舊節(jié)點(diǎn)都有 children
if (isDef(oldCh) && isDef(ch)) {
// 使用diff算法對(duì)比子節(jié)點(diǎn),更新子節(jié)點(diǎn)
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) {
// 如果新節(jié)點(diǎn)有children,舊節(jié)點(diǎn)沒(méi)有children
// 如果舊節(jié)點(diǎn)有text,清空dom 元素的內(nèi)容
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
// 批量添加子節(jié)點(diǎn)
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 如果舊節(jié)點(diǎn)有children,新節(jié)點(diǎn)沒(méi)有children
// 批量移除子節(jié)點(diǎn)
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 如果舊節(jié)點(diǎn)有 text,清空 DOM 元素
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
// 如果沒(méi)有設(shè)置 vnode.text
if (isDef(oldCh)) {
// 如果舊節(jié)點(diǎn)有children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 設(shè)置 DOM 元素的textContent為 vnode.text
api.setTextContent(elm, vnode.text!);
}
// 最后執(zhí)行postpatch鉤子函數(shù)
hook?.postpatch?.(oldVnode, vnode);
}
updateChildren 方法
patchVnode 里面最重要的方法,也是整個(gè) diff 里面的最核心方法。
主要的邏輯如下:
優(yōu)先處理特殊場(chǎng)景,先對(duì)比兩端。也就是
舊 vnode 頭 vs 新 vnode 頭 舊 vnode 尾 vs 新 vnode 尾 舊 vnode 頭 vs 新 vnode 尾 舊 vnode 尾 vs 新 vnode 頭
首尾不一樣的情況,尋找 key 相同的節(jié)點(diǎn),找不到則新建元素 如果找到 key,但是,元素選擇器變化了,也新建元素 如果找到 key,并且元素選擇沒(méi)變, 則移動(dòng)元素 兩個(gè)列表對(duì)比完之后,清理多余的元素,新增添加的元素
不提供 key 的情況下,如果只是順序改變的情況,例如第一個(gè)移動(dòng)到末尾。這個(gè)時(shí)候,會(huì)導(dǎo)致其實(shí)更新了后面的所有元素。
// 更新子節(jié)點(diǎn)
function updateChildren(
parentElm: Node,
oldCh: Array<VNode>,
newCh: Array<VNode>,
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0,
newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: any;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
// 移動(dòng)索引,因?yàn)楣?jié)點(diǎn)處理過(guò)了會(huì)置空,所以這里向右移
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
// 原理同上
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
// 原理同上
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
// 原理同上
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 從左對(duì)比
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 從右對(duì)比
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 最左側(cè) 對(duì)比 最右側(cè)
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
// 移動(dòng)元素到右側(cè)指針的后面
api.insertBefore(
parentElm,
oldStartVnode.elm as Node,
api.nextSibling(oldEndVnode.elm as Node)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 最右側(cè)對(duì)比最左側(cè)
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
// 移動(dòng)元素到左側(cè)指針的后面
api.insertBefore(
parentElm,
oldEndVnode.elm as Node,
oldStartVnode.elm as Node
);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 首尾都不一樣的情況,尋找相同 key 的節(jié)點(diǎn),所以使用的時(shí)候加上key可以調(diào)高效率
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(
oldCh,
oldStartIdx,
oldEndIdx
);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
// 如果找不到 key 對(duì)應(yīng)的元素,就新建元素
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
newStartVnode = newCh[++newStartIdx];
} else {
// 如果找到 key 對(duì)應(yīng)的元素,就移動(dòng)元素
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
} else {
patchVnode(
elmToMove,
newStartVnode,
insertedVnodeQueue
);
oldCh[idxInOld] = undefined as any;
api.insertBefore(
parentElm,
elmToMove.elm as Node,
oldStartVnode.elm as Node
);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
// 新舊數(shù)組其中一個(gè)到達(dá)末尾
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
// 如果舊數(shù)組先到達(dá)末尾,說(shuō)明新數(shù)組還有更多的元素,這些元素都是新增的,說(shuō)以一次性插入
before =
newCh[newEndIdx + 1] == null
? null
: newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else {
// 如果新數(shù)組先到達(dá)末尾,說(shuō)明新數(shù)組比舊數(shù)組少了一些元素,所以一次性刪除
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
addVnodes方法
主要功能就是添加 Vnodes 到 真實(shí) DOM 中。
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
removeVnodes方法
主要邏輯如下:
循環(huán)觸發(fā) destroy 鉤子,遞歸觸發(fā)子節(jié)點(diǎn)的鉤子 觸發(fā) remove 鉤子,利用 createRmCb , 在所有監(jiān)聽(tīng)器執(zhí)行后,才調(diào)用 api.removeChild,刪除真正的 DOM 節(jié)點(diǎn)
function invokeDestroyHook(vnode: VNode) {
const data = vnode.data;
if (data !== undefined) {
// 執(zhí)行的destroy 鉤子函數(shù)
data?.hook?.destroy?.(vnode);
// 調(diào)用模塊的destroy鉤子函數(shù)
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
// 執(zhí)行子節(jié)點(diǎn)的destroy鉤子函數(shù)
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j];
if (child != null && typeof child !== "string") {
invokeDestroyHook(child);
}
}
}
}
}
//創(chuàng)建一個(gè)刪除的回調(diào),多次調(diào)用這個(gè)回調(diào),直到監(jiān)聽(tīng)器都沒(méi)了,就刪除元素
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm) as Node;
api.removeChild(parent, childElm);
}
};
}
function removeVnodes(
parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number
): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number;
let rm: () => void;
const ch = vnodes[startIdx];
if (ch != null) {
// 如果sel 有值
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
// 防止重復(fù)調(diào)用
listeners = cbs.remove.length + 1;
// 創(chuàng)建刪除的回調(diào)函數(shù)
rm = createRmCb(ch.elm!, listeners);
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
// 執(zhí)行remove鉤子函數(shù)
const removeHook = ch?.data?.hook?.remove;
if (isDef(removeHook)) {
removeHook(ch, rm);
} else {
// 如果沒(méi)有鉤子函數(shù),直接調(diào)用刪除元素的方法
rm();
}
} else {
// Text node
// 如果是文本節(jié)點(diǎn),直接是調(diào)用刪除元素的方法
api.removeChild(parentElm, ch.elm!);
}
}
}
}
createElm方法
將 vnode 轉(zhuǎn)換成真正的 DOM 元素。
主要邏輯如下:
觸發(fā) init 鉤子 處理注釋節(jié)點(diǎn) 創(chuàng)建元素并設(shè)置 id , class 觸發(fā)模塊 create 鉤子 。 處理子節(jié)點(diǎn) 處理文本節(jié)點(diǎn) 觸發(fā) vnodeData 的 create 鉤子
/*
1.觸發(fā)鉤子函數(shù)init
2.把vnode轉(zhuǎn)換為DOM對(duì)象,存儲(chǔ)到vnode.elm中
- sel是!--》創(chuàng)建注釋節(jié)點(diǎn)
- sel不為空 --》創(chuàng)建對(duì)應(yīng)的DOM對(duì)象;觸發(fā)模塊的鉤子函數(shù)create;創(chuàng)建所有子節(jié)點(diǎn)對(duì)應(yīng)的DOM對(duì)象;觸發(fā)鉤子函數(shù)create;如果是vnode有inset鉤子函數(shù),追加到隊(duì)列
- sel為空 --》創(chuàng)建文本節(jié)點(diǎn)
3.返回vnode.elm
*/
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any;
let data = vnode.data;
if (data !== undefined) {
// 執(zhí)行init鉤子函數(shù)
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
// 把vnode轉(zhuǎn)換成真實(shí)dom對(duì)象(沒(méi)有渲染到頁(yè)面)
const children = vnode.children;
const sel = vnode.sel;
if (sel === "!") {
// 如果選擇器是!,創(chuàng)建注釋節(jié)點(diǎn)
if (isUndef(vnode.text)) {
vnode.text = "";
}
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// 如果選擇器不為空
// 解析選擇器
// Parse selector
const hashIdx = sel.indexOf("#");
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag, data)
: api.createElement(tag, data));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
// 執(zhí)行模塊的create鉤子函數(shù)
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
// 如果vnode中有子節(jié)點(diǎn),創(chuàng)建子Vnode對(duì)應(yīng)的DOM元素并追加到DOM樹(shù)上
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
// 如果vode的text值是string/number,創(chuàng)建文本節(jié)點(diǎn)并追加到DOM樹(shù)
api.appendChild(elm, api.createTextNode(vnode.text));
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
// 執(zhí)行傳入的鉤子 create
hook.create?.(emptyNode, vnode);
if (hook.insert) {
insertedVnodeQueue.push(vnode);
}
}
} else {
// 如果選擇器為空,創(chuàng)建文本節(jié)點(diǎn)
vnode.elm = api.createTextNode(vnode.text!);
}
// 返回新創(chuàng)建的DOM
return vnode.elm;
}
參考資料
https://foio.github.io/virtual-dom/;https://tech.tuya.com/xu-ni-dom/;https://github.com/snabbdom/snabbdom/;https://segmentfault.com/a/1190000017519084/;https://qastack.cn/programming/21965738/what-is-virtual-dom
結(jié)語(yǔ)
謝謝閱讀,如果你覺(jué)得對(duì)你有幫助,歡迎一鍵三連,另外,我自己創(chuàng)辦了一個(gè)公眾號(hào),你可以關(guān)注它。 VX搜索:前端歷劫之路 。關(guān)注后,我可以拉你進(jìn)學(xué)習(xí)交流群。掌握最新前端動(dòng)態(tài),一起學(xué)習(xí)進(jìn)步。
