Study Notes
本博主會持續(xù)更新各種前端的技術灶似,如果各位道友喜歡肆汹,可以關注痰滋、收藏、點贊下本博主的文章站刑。
虛擬 DOM(Virtual DOM)
什么是 Virtual DOM
- Virtual DOM(虛擬 DOM)另伍,是由普通的 JS 對象來描述 DOM 對象,因為不是真實的 DOM 對象绞旅,所以叫 Virtual DOM
- 真實 DOM 成員
let element = document.querySelector('#app')
let s = ''
for (var key in element) {
s += key + ','
}
console.log(s)
// 打印結(jié)果
align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut
ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off
setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc
opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc
hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag
end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan
ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr
ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown
,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,
onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on
resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend
,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot
pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe
rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl
eave,onselectstart,onselectionchange,onanimationend,onanimationiteration
,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click
,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n
amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at
tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef
t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight
,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme
ntTiming,previousElementSibling,nextElementSibling,children,firstElement
Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen
error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture
,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames
,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute
,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib
uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt
ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement
sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE
lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien
tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr
ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re
move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web
kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina
tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_
NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME
NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION
_NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN
T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI
NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU
RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh
ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild
Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu
mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace
,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo
veEventListener,dispatchEvent
- 可以使用 Virtual DOM 來描述真實 DOM摆尝,示例
{
"sel": "div",
"data": {},
"children": undefined,
"text": "Hello Virtual DOM",
"elm": undefined,
"key": undefined
}
為什么使用 Virtual DOM
- 手動操作 DOM 比較麻煩愕宋,還需要考慮瀏覽器兼容性問題,雖然有 jQuery 等庫簡化 DOM 操作结榄,但是隨著項目的復雜 DOM 操作復雜提升
- 為了簡化 DOM 的復雜操作于是出現(xiàn)了各種 MVVM 框架中贝,MVVM 框架解決了視圖和狀態(tài)的同步問題
- 為了簡化視圖的操作我們可以使用模板引擎,但是模板引擎沒有解決跟蹤狀態(tài)變化的問題臼朗,于是 Virtual DOM 出現(xiàn)了
- Virtual DOM 的好處是當狀態(tài)改變時不需要立即更新 DOM邻寿,只需要創(chuàng)建一個虛擬樹來描述 DOM, Virtual DOM 內(nèi)部將弄清楚如何有效(diff)的更新 DOM
- 參考 github 上 virtual-dom 的描述
- 虛擬 DOM 可以維護程序的狀態(tài)视哑,跟蹤上一次的狀態(tài)
- 通過比較前后兩次狀態(tài)的差異更新真實 DOM
虛擬 DOM 的作用
- 維護視圖和狀態(tài)的關系
- 復雜視圖情況下提升渲染性能
- 除了渲染 DOM 以外绣否,還可以實現(xiàn) SSR(Nuxt.js/Next.js)、原生應用(Weex/React Native)挡毅、小程序(mpvue/uni-app)等
Virtual DOM 庫
-
Snabbdom
- Vue 2.x 內(nèi)部使用的 Virtual DOM 就是改造的 Snabbdom
- 大約 200 SLOC(single line of code)
- 通過模塊可擴展
- 源碼使用 TypeScript 開發(fā)
- 最快的 Virtual DOM 之一
- virtual-dom
案例演示
Snabbdom
Snabbdom 基本使用
為了方便使用parcel打包工具
安裝
npm i snabbdom -D
導入 Snabbdom
import { init } from 'snabbdom/init';
import { h } from 'snabbdom/h'; // helper function for creating vnodes
如果遇到下面的錯誤
Cannot resolve dependency 'snabbdom/init'
因為模塊路徑并不是 snabbdom/int蒜撮,這個路徑是作者在 package.json 中的 exports 字段設置的,而我們使用的打包工具不支持 exports 這個字段跪呈,webpack 4 也不支持段磨,webpack 5 beta 支持該字段。該字段在導入 snabbdom/init 的時候會補全路徑成 snabbdom/build/package/init.js耗绿。
{
"exports": {
"./init": "./build/package/init.js",
"./h": "./build/package/h.js",
"./helpers/attachto": "./build/package/helpers/attachto.js",
"./hooks": "./build/package/hooks.js",
"./htmldomapi": "./build/package/htmldomapi.js",
"./is": "./build/package/is.js",
"./jsx": "./build/package/jsx.js",
"./modules/attributes": "./build/package/modules/attributes.js",
"./modules/class": "./build/package/modules/class.js",
"./modules/dataset": "./build/package/modules/dataset.js",
"./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
"./modules/hero": "./build/package/modules/hero.js",
"./modules/module": "./build/package/modules/module.js",
"./modules/props": "./build/package/modules/props.js",
"./modules/style": "./build/package/modules/style.js",
"./thunk": "./build/package/thunk.js",
"./tovnode": "./build/package/tovnode.js",
"./vnode": "./build/package/vnode.js"
}
}
解決方法一:安裝 Snabbdom@v0.7.4 版本
解決方法二:導入 init苹支、h,以及模塊只要把把路徑補全即可误阻。
import { h } from 'snabbdom/build/package/h';
import { init } from 'snabbdom/build/package/init';
import { classModule } from 'snabbdom/build/package/modules/class';
使用
- init() 是一個高階函數(shù)债蜜,返回 patch()
- h() 返回虛擬節(jié)點 VNode,這個函數(shù)我們在使用 Vue.js 的時候見過
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
- thunk() 是一種優(yōu)化策略究反,可以在處理不可變數(shù)據(jù)時使用
demo
/**
* @author Wuner
* @date 2020/8/1 10:05
* @description
*/
import { h } from 'snabbdom/build/package/h';
import { init } from 'snabbdom/build/package/init';
// 使用init()函數(shù)創(chuàng)建patch()
// init()的參數(shù)是一個數(shù)組寻定,用于導入模塊,處理屬性/樣式/事件等
let patch = init([]);
// 使用h()函數(shù)創(chuàng)建Vnode
let vnode = h('div#second', [h('h1', '基本使用2'), h('p', 'hello world')]);
let appEl = document.querySelector('#app');
// 把vnode渲染到空的DOM元素(替換)
// 會返回新的vnode
let oldVnode = patch(appEl, vnode);
setTimeout(() => {
vnode = h('div#second', [h('h1', '基本使用2'), h('p', 'hello snabbdom')]);
// 把老的視圖更新到新的狀態(tài)
oldVnode = patch(oldVnode, vnode);
setTimeout(() => {
// 卸載DOM精耐,文檔中patch(oldVnode,null)有誤
// h('!')創(chuàng)建注釋
patch(oldVnode, h('!'));
}, 1000);
}, 2000);
Snabbdom 模塊使用
Snabbdom 的核心庫并不能處理元素的屬性/樣式/事件等狼速,如果需要處理的話,可以使用模塊
常用模塊
- 官方提供了 6 個模塊
- attributes
- 設置 DOM 元素的屬性黍氮,使用 setAttribute ()
- 處理布爾類型的屬性
- props
- 和 attributes 模塊相似唐含,設置 DOM 元素的屬性 element[attr] = value
- 不處理布爾類型的屬性
- class
- 切換類樣式
- 注意:給元素設置類樣式是通過 sel 選擇器
- dataset
- 設置 data-* 的自定義屬性
- eventListeners
- 注冊和移除事件
- style
- 設置行內(nèi)樣式,支持動畫
- delayed/remove/destroy
- attributes
demo
/**
* @author Wuner
* @date 2020/8/1 11:40
* @description
*/
import { init } from 'snabbdom/build/package/init';
import { h } from 'snabbdom/build/package/h';
// 導入需要的模塊
import { styleModule } from 'snabbdom/build/package/modules/style';
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners';
// 使用 init() 函數(shù)創(chuàng)建 patch()
// init() 的參數(shù)是數(shù)組沫浆,將來可以傳入模塊,處理屬性/樣式/事件等
let patch = init([
// 注冊模塊
styleModule,
eventListenersModule,
]);
// 使用 h() 函數(shù)創(chuàng)建 vnode
let vnode = h(
'div#third',
{
// 設置 DOM 元素的行內(nèi)樣式
style: {
backgroundColor: '#999',
},
// 注冊事件
on: {
click: clickHandel,
},
},
[h('h1', '模塊使用'), h('p', 'hello snabbdom module use')],
);
function clickHandel() {
// 此處的 this 指向?qū)?vnode
console.log('我點擊了', this.elm.innerHTML);
}
let appEl = document.querySelector('#app');
patch(appEl, vnode);
Snabbdom 源碼解析
如何學習源碼
- 先宏觀了解
- 帶著目標看源碼
- 看源碼的過程要不求甚解
- 調(diào)試
- 參考資料
Snabbdom 的核心
- 使用 h() 函數(shù)創(chuàng)建 JavaScript 對象(VNode)描述真實 DOM
- init() 設置模塊滚秩,創(chuàng)建 patch()
- patch() 比較新舊兩個 VNode
- 把變化的內(nèi)容更新到真實 DOM 樹上
Snabbdom 源碼結(jié)構
│-----h.ts h() -----------函數(shù)专执,用來創(chuàng)建 VNode
│-----hooks.ts -----------所有鉤子函數(shù)的定義
│-----htmldomapi.ts ------對 DOM API 的包裝
│-----init.ts ------------設置模塊,創(chuàng)建 patch()
│-----is.ts --------------判斷數(shù)組和原始值的函數(shù)
│-----jsx-global.d.ts ----jsx 的類型聲明文件
│-----jsx.ts -------------處理 jsx
│-----thunk.ts -----------優(yōu)化處理郁油,對復雜視圖不可變值得優(yōu)化
│-----tovnode.ts ---------DOM 轉(zhuǎn)換成 VNode
│-----vnode.ts -----------虛擬節(jié)點定義
│
|-----helpers
│----------attachto.ts ---定義了 vnode.ts 中 AttachData 的數(shù)據(jù)結(jié)構
│
|-----modules ------------所有模塊定義
|----------attributes.ts
|----------class.ts
|----------dataset.ts
|----------eventlisteners.ts
|----------hero.ts --------example 中使用到的自定義鉤子
|----------module.ts ------定義了模塊中用到的鉤子函數(shù)
|----------props.ts
|----------style.ts
h 函數(shù)
-
h() 函數(shù)介紹
-
在使用 Vue 的時候見過 h() 函數(shù)
new Vue({ router, store, render: (h) => h(App), }).$mount('#app');
h() 函數(shù)最早見于 hyperscript 本股,使用 JavaScript 創(chuàng)建超文本
Snabbdom 中的 h() 函數(shù)不是用來創(chuàng)建超文本攀痊,而是創(chuàng)建 VNode
-
-
函數(shù)重載
-
概念
- 參數(shù)個數(shù)或類型不同的函數(shù)
- JavaScript 中沒有重載的概念
- TypeScript 中有重載,不過重載的實現(xiàn)還是通過代碼調(diào)整參數(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);
-
源碼解析src/package/h.ts
import ...
export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(
data: any,
children: VNodes | undefined,
sel: string | undefined,
): void {...}
// h函數(shù)的重載
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
sel: string,
data: VNodeData | null,
children: VNodeChildren,
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {};
var children: any;
var text: any;
var i: number;
//處理參數(shù)拄显,實現(xiàn)重載的機制
if (c !== undefined) {
// 處理三個參數(shù)的情況
// sel苟径、data、children/text
if (b !== null) {
data = b;
}
// 如果c是數(shù)組躬审,則將c賦值給children
if (is.array(c)) {
children = c;
} else if (is.primitive(c)) {
// 如果c是數(shù)字或字符串類型棘街,則將c賦值給text
text = c;
} else if (c && c.sel) {
// 如果c是vnode,則將c放到數(shù)組里承边,賦值給children
children = [c];
}
} else if (b !== undefined && b !== null) {
// 處理兩個參數(shù)的情況
// 如果b是數(shù)組遭殉,則將b賦值給children
if (is.array(b)) {
children = b;
} else if (is.primitive(b)) {
// 如果b是數(shù)字或字符串類型,則將b賦值給text
text = b;
} else if (b && b.sel) {
// 如果b是vnode博助,則將b放到數(shù)組里险污,賦值給children
children = [b];
} else {
data = b;
}
}
if (children !== undefined) {
// 處理children里的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果children里的值是字符串或者數(shù)字(string/number)類型,則創(chuàng)建文本節(jié)點
if (is.primitive(children[i]))
children[i] = vnode(
undefined,
undefined,
undefined,
children[i],
undefined,
);
}
}
if (
sel[0] === 's' &&
sel[1] === 'v' &&
sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
// 如果是svg富岳,則添加命名空間
addNS(data, children, sel);
}
// 返回vnode(虛擬節(jié)點)
return vnode(sel, data, children, text, undefined);
}
VNode
一個 VNode 就是一個虛擬節(jié)點用來描述一個 DOM 元素蛔糯,如果這個 VNode 有 children 就是 Virtual DOM
源碼解析src/package/vnode.ts
import ...
export type Key = string | number;
export interface VNode {
// 選擇器
sel: string | undefined;
// 節(jié)點數(shù)據(jù),屬性窖式、樣式渤闷、事件機制等
data: VNodeData | undefined;
// 子節(jié)點,其和text屬性互斥脖镀,只能存在一個
children: Array<VNode | string> | undefined;
// 記錄vnode對應的真實DOM
elm: Node | undefined;
// 節(jié)點中的內(nèi)容飒箭,其和children屬性互斥,只能存在一個
text: string | undefined;
// 用于優(yōu)化
key: Key | undefined;
}
export interface VNodeData {...}
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined,
): VNode {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
init(src/package/init.ts)
- patch(oldVnode, newVnode)
- 打補丁蜒灰,把新節(jié)點中變化的內(nèi)容渲染到真實 DOM弦蹂,最后返回新節(jié)點作為下一次處理的舊節(jié)點
- 對比新舊 VNode 是否相同節(jié)點(節(jié)點的 key 和 sel 相同)
- 如果不是相同節(jié)點,刪除之前的內(nèi)容强窖,重新渲染
- 如果是相同節(jié)點凸椿,再判斷新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同翅溺,直接更新文本內(nèi)容
- 如果新的 VNode 有 children脑漫,判斷子節(jié)點是否有變化,判斷子節(jié)點的過程使用的就是 diff 算法
- diff 過程只進行同層級比較
init()
- 功能:init(modules, domApi)咙崎,返回 patch() 函數(shù)(高階函數(shù))
- 為什么要使用高階函數(shù)优幸?
- 因為 patch() 函數(shù)再外部會調(diào)用多次,每次調(diào)用依賴一些參數(shù)褪猛,比如:modules/domApi/cbs
- 通過高階函數(shù)讓 init() 內(nèi)部形成閉包网杆,返回的 patch() 可以訪問到 modules/domApi/cbs,而不需要重新創(chuàng)建
- init() 在返回 patch() 之前,首先收集了所有模塊中的鉤子函數(shù)存儲到 cbs 對象中
源碼解析
import ...
type NonUndefined<T> = T extends undefined ? never : T;
function isUndef(s: any): boolean {...}
function isDef<A>(s: A): s is NonUndefined<A> {...}
type VNodeQueue = VNode[];
const emptyNode = vnode('', {}, [], undefined, undefined);
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {...}
function isVnode(vnode: any): vnode is VNode {...}
type KeyToIndexMap = { [key: string]: number };
type ArraysOf<T> = {
[K in keyof T]: Array<T[K]>;
};
type ModuleHooks = ArraysOf<Required<Module>>;
function createKeyToOldIdx(
children: VNode[],
beginIdx: number,
endIdx: number,
): KeyToIndexMap {...}
const hooks: Array<keyof Module> = [
'create',
'update',
'remove',
'destroy',
'pre',
'post',
];
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é)點的api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 把傳入的所有模塊的鉤子函數(shù)碳却,統(tǒng)一存儲到cbs(callbacks)對象中
for (i = 0; i < hooks.length; ++i) {
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對應的鉤子函數(shù)數(shù)組中
(cbs[hooks[i]] as any[]).push(hook);
}
}
}
function emptyNodeAt(elm: Element) {...}
function createRmCb(childElm: Node, listeners: number) {...}
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {...}
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue,
) {...}
function invokeDestroyHook(vnode: VNode) {...}
function removeVnodes(
parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number,
): void {...}
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue,
) {...}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue,
) {...}
// init內(nèi)部返回patch函數(shù)队秩,把vnode渲染成真是的DOM,并返回vnode
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {...};
}
patch
- 功能:
- 傳入新舊 VNode昼浦,對比差異馍资,把差異渲染到 DOM
- 返回新的 VNode,作為下一次 patch() 的 oldVnode
- 執(zhí)行過程:
- 首先執(zhí)行模塊中的鉤子函數(shù) pre
- 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
- 調(diào)用 patchVnode()关噪,找節(jié)點的差異并更新 DOM
- 如果 oldVnode 是 DOM 元素
- 把 DOM 元素轉(zhuǎn)換成 oldVnode
- 調(diào)用 createElm() 把 vnode 轉(zhuǎn)換為真實 DOM鸟蟹,記錄到 vnode.elm
- 把剛創(chuàng)建的 DOM 元素插入到 parent 中
- 移除老節(jié)點
- 觸發(fā)用戶設置的 create 鉤子函數(shù)
源碼解析
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 保存新插入節(jié)點的隊列,為了觸發(fā)鉤子函數(shù)
const insertedVnodeQueue: VNodeQueue = [];
// 遍歷cbs(callbacks)執(zhí)行模塊中的pre鉤子函數(shù)
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果不是節(jié)點時色洞,為真實DOM創(chuàng)建空的虛擬節(jié)點
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// 如果老的虛擬節(jié)點和新的虛擬節(jié)點相同戏锹,則去尋找新舊節(jié)點的差異,并更新DOM
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 如果新舊節(jié)點不同火诸,則vnode創(chuàng)建對應的DOM
// 獲取當前的DOM元素
elm = oldVnode.elm!;
// 獲取當前DOM元素的父元素
parent = api.parentNode(elm) as Node;
// 觸發(fā)init/create鉤子函數(shù)锦针,創(chuàng)建DOM
createElm(vnode, insertedVnodeQueue);
// 如果父元素不為空,則把vnode對應的DOM插入到文檔中
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
// 移除老節(jié)點
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 遍歷insertedVnodeQueue置蜀,如果存在用戶設置的insert鉤子函數(shù)奈搜,則執(zhí)行該函數(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
return vnode;
};
createElm
- 功能:
- createElm(vnode, insertedVnodeQueue),返回創(chuàng)建的 DOM 元素
- 創(chuàng)建 vnode 對應的 DOM 元素
- 執(zhí)行過程:
- 首先觸發(fā)用戶設置的 init 鉤子函數(shù)
- 如果選擇器是!盯荤,創(chuàng)建注釋節(jié)點
- 如果選擇器為空馋吗,創(chuàng)建文本節(jié)點
- 如果選擇器不為空
- 解析選擇器,設置標簽的 id 和 class 屬性
- 執(zhí)行模塊的 create 鉤子函數(shù)
- 如果 vnode 有 children秋秤,創(chuàng)建子 vnode 對應的 DOM宏粤,追加到 DOM 樹
- 如果 vnode 的 text 值是 string/number,創(chuàng)建文本節(jié)點并追擊到 DOM 樹
- 執(zhí)行用戶設置的 create 鉤子函數(shù)
- 如果有用戶設置的 insert 鉤子函數(shù)灼卢,把 vnode 添加到隊列中
源碼解析
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any;
let data = vnode.data;
// 如果存在用戶設置的init鉤子函數(shù)绍哎,且不為undefined,則執(zhí)行該鉤子函數(shù)
if (data !== undefined) {
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
// 為什么重新賦值鞋真,是為了防止用戶設置新的
data = vnode.data;
}
}
const children = vnode.children;
const sel = vnode.sel;
if (sel === '!') {
if (isUndef(vnode.text)) {
vnode.text = '';
}
// 創(chuàng)建并返回一個注釋節(jié)點
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// 如果選擇器不為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;
// 如果data并且data.ns不為undefined時崇堰,創(chuàng)建一個具有指定的命名空間URI和限定名稱的元素
// 否則創(chuàng)建一個不指定命名空間URI的元素
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag)
: api.createElement(tag));
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
// 遍歷執(zhí)行cbs(callbacks)中的create鉤子函數(shù)
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
// 如果vnode中存在子節(jié)點,創(chuàng)建vnode對應的DOM元素涩咖,并追加到DOM樹上
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)) {
// 如果vnode中的text是string或者number類型的
// 則創(chuàng)建文本節(jié)點海诲,并追加到DOM樹上
api.appendChild(elm, api.createTextNode(vnode.text));
}
// 如果存在用戶設置的鉤子函數(shù),并且不為undefined時
const hook = vnode.data!.hook;
if (isDef(hook)) {
// 如果存在執(zhí)行create鉤子函數(shù)檩互,則執(zhí)行該鉤子函數(shù)
hook.create?.(emptyNode, vnode);
if (hook.insert) {
// 如果存在執(zhí)行insert鉤子函數(shù)特幔,則把vnode添加到隊列中,為后續(xù)執(zhí)行insert鉤子函數(shù)做準備
insertedVnodeQueue.push(vnode);
}
}
} else {
// 如果選擇器為undefined時盾似,創(chuàng)建文本節(jié)點
vnode.elm = api.createTextNode(vnode.text!);
}
// 返回新創(chuàng)建的DOM
return vnode.elm;
}
patchVnode
- 功能:
- patchVnode(oldVnode, vnode, insertedVnodeQueue)
- 對比 oldVnode 和 vnode 的差異敬辣,把差異渲染到 DOM
- 執(zhí)行過程:
- 首先執(zhí)行用戶設置的 prepatch 鉤子函數(shù)
- 執(zhí)行 create 鉤子函數(shù)
- 首先執(zhí)行模塊的 create 鉤子函數(shù)
- 然后執(zhí)行用戶設置的 create 鉤子函數(shù)
- 如果 vnode.text 未定義
- 如果 oldVnode.children 和 vnode.children 都有值
- 調(diào)用 updateChildren()
- 使用 diff 算法對比子節(jié)點雪标,更新子節(jié)點
- 如果 vnode.children 有值零院, oldVnode.children 無值
- 清空 DOM 元素
- 調(diào)用 addVnodes() 溉跃,批量添加子節(jié)點
- 如果 oldVnode.children 有值, vnode.children 無值
- 調(diào)用 removeVnodes() 告抄,批量移除子節(jié)點
- 如果 oldVnode.text 有值
- 清空 DOM 元素的內(nèi)容
- 如果 oldVnode.children 和 vnode.children 都有值
- 如果設置了 vnode.text 并且和 oldVnode.text 不同
- 如果老節(jié)點有子節(jié)點撰茎,全部移除
- 設置 DOM 元素的 textContent 為 vnode.text
- 最后執(zhí)行用戶設置的 postpatch 鉤子函數(shù)
源碼解析
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue,
) {
// 如果存在用戶設置的prepatch鉤子函數(shù),則執(zhí)行該鉤子函數(shù)
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm!);
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
// 如果新舊節(jié)點相同打洼,則直接返回
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
// 遍歷執(zhí)行cbs(callbacks)中的update鉤子函數(shù)
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
// 如果存在用戶設置的update鉤子函數(shù)龄糊,則執(zhí)行該鉤子函數(shù)
vnode.data.hook?.update?.(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
// vnode中的text為undefined時
if (isDef(oldCh) && isDef(ch)) {
// 舊虛擬節(jié)點存在子節(jié)點并且新虛擬節(jié)點也存在子節(jié)點時
// 如果新舊虛擬節(jié)點中的子節(jié)點不相同時,使用diff算法對比子節(jié)點募疮,更新子節(jié)點
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) {
// 舊虛擬節(jié)點不存在子節(jié)點并且新虛擬節(jié)點存在子節(jié)點時
// 如果舊虛擬節(jié)點中存在text炫惩,則清空DOM元素的內(nèi)容
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 批量添加子節(jié)點
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 舊虛擬節(jié)點存在子節(jié)點并且新虛擬節(jié)點不存在子節(jié)點時
// 批量刪除子節(jié)點
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 如果舊虛擬節(jié)點存在text,則清空DOM元素
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 舊虛擬節(jié)點的text與新虛擬節(jié)點的text不相同時
// 如果舊虛擬節(jié)點存在子節(jié)點阿浓,批量刪除子節(jié)點
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 將DOM元素的textContent設置為vnode.text
api.setTextContent(elm, vnode.text!);
}
// 如果存在用戶設置的postpatch鉤子函數(shù)他嚷,則執(zhí)行該鉤子函數(shù)
hook?.postpatch?.(oldVnode, vnode);
}
updateChildren
-
功能:
- diff 算法的核心,對比新舊節(jié)點的 children芭毙,更新 DOM
-
執(zhí)行過程:
- 要對比兩棵樹的差異筋蓖,我們可以取第一棵樹的每一個節(jié)點依次和第二課樹的每一個節(jié)點比較,但是這樣的時間復雜度為 O(n^3)
- 在 DOM 操作的時候我們很少很少會把一個父節(jié)點移動/更新到某一個子節(jié)點
- 因此只需要找同級別的子節(jié)點依次比較退敦,然后再找下一級別的節(jié)點比較粘咖,這樣算法的時間復雜度為 O(n)
- 在進行同級別節(jié)點比較的時候,首先會對新老節(jié)點數(shù)組的開始和結(jié)尾節(jié)點設置標記索引侈百,遍歷的過程中移動索引
- 在對開始和結(jié)束節(jié)點比較的時候瓮下,總共有四種情況
- oldStartVnode / newStartVnode (舊開始節(jié)點 / 新開始節(jié)點)
- oldEndVnode / newEndVnode (舊結(jié)束節(jié)點 / 新結(jié)束節(jié)點)
- oldStartVnode / oldEndVnode (舊開始節(jié)點 / 新結(jié)束節(jié)點)
- oldEndVnode / newStartVnode (舊結(jié)束節(jié)點 / 新開始節(jié)點)
- 開始節(jié)點和結(jié)束節(jié)點比較,這兩種情況類似
- oldStartVnode / newStartVnode (舊開始節(jié)點 / 新開始節(jié)點)
- oldEndVnode / newEndVnode (舊結(jié)束節(jié)點 / 新結(jié)束節(jié)點)
- 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
- 調(diào)用 patchVnode() 對比和更新節(jié)點
- 把舊開始和新開始索引往后移動 oldStartIdx++ / oldEndIdx++
- oldStartVnode / newEndVnode (舊開始節(jié)點 / 新結(jié)束節(jié)點) 相同
- 調(diào)用 patchVnode() 對比和更新節(jié)點
- 把 oldStartVnode 對應的 DOM 元素钝域,移動到右邊
- 更新索引
- oldEndVnode / newStartVnode (舊結(jié)束節(jié)點 / 新開始節(jié)點) 相同
- 調(diào)用 patchVnode() 對比和更新節(jié)點
- 把 oldEndVnode 對應的 DOM 元素讽坏,移動到左邊
- 更新索引
- 如果不是以上四種情況
- 遍歷新節(jié)點,使用 newStartNode 的 key 在老節(jié)點數(shù)組中找相同節(jié)點
- 如果沒有找到网梢,說明 newStartNode 是新節(jié)點
- 創(chuàng)建新節(jié)點對應的 DOM 元素震缭,插入到 DOM 樹中
- 如果找到了
- 判斷新節(jié)點和找到的老節(jié)點的 sel 選擇器是否相同
- 如果不相同,說明節(jié)點被修改了
- 重新創(chuàng)建對應的 DOM 元素战虏,插入到 DOM 樹中
- 如果相同拣宰,把 elmToMove 對應的 DOM 元素,移動到左邊
- 循環(huán)結(jié)束
- 當老節(jié)點的所有子節(jié)點先遍歷完 (oldStartIdx > oldEndIdx)烦感,循環(huán)結(jié)束
- 新節(jié)點的所有子節(jié)點先遍歷完 (newStartIdx > newEndIdx)巡社,循環(huán)結(jié)束
- 如果老節(jié)點的數(shù)組先遍歷完(oldStartIdx > oldEndIdx),說明新節(jié)點有剩余手趣,把剩余節(jié)點批量插入到右邊
- 如果新節(jié)點的數(shù)組先遍歷完(newStartIdx > newEndIdx)晌该,說明老節(jié)點有剩余肥荔,把剩余節(jié)點批量刪除
源碼解析
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) {
// 索引變化后,可能會把節(jié)點設置為null
if (oldStartVnode == null) {
// 當舊開始節(jié)點為null時朝群,移動舊開始索引
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
// 當舊結(jié)束節(jié)點為null時燕耿,移動舊結(jié)束索引
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
// 當新開始節(jié)點為null時,移動新開始索引
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
// 當新結(jié)束節(jié)點為null時姜胖,移動新結(jié)束索引
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 舊開始節(jié)點和新開始節(jié)點相同時
// 調(diào)用patchVnode()對比和更新節(jié)點
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
// 移動舊開始索引和移動新開始索引
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 舊結(jié)束節(jié)點和新結(jié)束節(jié)點相同時
// 調(diào)用patchVnode()對比和更新節(jié)點
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
// 移動舊結(jié)束索引和移動新結(jié)束索引
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 舊開始節(jié)點和新結(jié)束節(jié)點相同時
// 調(diào)用patchVnode()對比和更新節(jié)點
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
// 把舊開始節(jié)點對應的 DOM 元素誉帅,移動到右邊
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!),
);
// 移動舊開始索引和移動新結(jié)束索引
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 舊結(jié)束節(jié)點和新開始節(jié)點相同時
// 調(diào)用patchVnode()對比和更新節(jié)點
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
// 把舊結(jié)束節(jié)點對應的 DOM 元素,移動到左邊
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
// 移動舊結(jié)束索引和移動新開始索引
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 開始節(jié)點和結(jié)束節(jié)點都不同時
// 使用新開始節(jié)點的key在老節(jié)點數(shù)組中找相同節(jié)點
// 根據(jù)舊節(jié)點數(shù)組生成對應的key和index的map對象
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 遍歷開始節(jié)點右莱,從舊節(jié)點中找相同的key的舊節(jié)點索引
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
// 如果舊節(jié)點索引不存在蚜锨,則開始節(jié)點是一個新的節(jié)點
// 創(chuàng)建DOM元素并插入DOM樹
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!,
);
} else {
// 舊節(jié)點索引存在時,即找到了相同key的舊節(jié)點
// 將舊節(jié)點記錄到elmToMove中
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
// 如果新舊節(jié)點選擇器不同時慢蜓,創(chuàng)建新開始節(jié)點對應的DOM元素亚再,并插入到DOM樹上
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!,
);
} else {
// 新舊節(jié)點的選擇器相同時
// 調(diào)用patchVnode()對比和更新節(jié)點
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 將舊節(jié)點數(shù)組中的該索引位置的節(jié)點置為undefined
oldCh[idxInOld] = undefined as any;
// 把elmToMove對應的DOM元素,移到左邊
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
// 移動新開始索引
newStartVnode = newCh[++newStartIdx];
}
}
// 循環(huán)結(jié)束晨抡,舊節(jié)點數(shù)組先遍歷完成氛悬,或新節(jié)點數(shù)組先遍歷完成
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
// 如果舊節(jié)點數(shù)組先遍歷完成,說明有新節(jié)點剩余
// 把剩余節(jié)點批量插入到右邊
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue,
);
} else {
// 如果新節(jié)點數(shù)組先遍歷完成凄诞,說明有舊節(jié)點剩余
// 把剩余節(jié)點批量移除
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
Modules 源碼
- patch() -> patchVnode() -> updateChildren()
- Snabbdom 為了保證核心庫的精簡圆雁,把處理元素的屬性/事件/樣式等工作,放置到模塊中
- 模塊可以按照需要引入
- 模塊的使用可以查看官方文檔
- 模塊實現(xiàn)的核心是基于 Hooks
Hooks
鉤子是一種掛鉤到 DOM 節(jié)點生命周期的方法帆谍。Snabbdom 提供了豐富的鉤子可以選擇伪朽。模塊使用鉤子來擴展 Snabbdom,在普通代碼中汛蝙,鉤子用于在虛擬節(jié)點生命周期的期望點執(zhí)行任意代碼烈涮。
概覽
Name | Triggered when | Arguments to callback |
---|---|---|
pre | patch 過程開始 | none |
init | 一個虛擬節(jié)點被添加 | vnode |
create | 基于一個虛擬節(jié)點,一個 DOM 被創(chuàng)建 | emptyVnode, vnode |
insert | 一個元素被插入到 DOM 中 | vnode |
prepatch | 一個元素即將被修補(patched) | oldVnode, vnode |
update | 一個元素正在被更新 | oldVnode, vnode |
postpatch | 一個元素已經(jīng)被修補完成(patched) | oldVnode, vnode |
destroy | 元素被直接或間接刪除 | vnode |
remove | 元素將直接從 DOM 中刪除 | vnode, removeCallback |
post | 修補(patch)過程結(jié)束 | none |
以下鉤子可用于模塊:pre窖剑、create坚洽、update、destroy西土、remove讶舰、post。
以下鉤子可用于單個元素的鉤子屬性:init需了、create跳昼、insert、prepatch肋乍、update鹅颊、postpatch、destroy墓造、remove堪伍。
源碼解析src/package/hooks.ts
export interface Hooks {
// patch 函數(shù)開始執(zhí)行的時候觸發(fā)
pre?: PreHook;
// createElm 函數(shù)開始之前的時候觸發(fā)
// 在把 VNode 轉(zhuǎn)換成真實 DOM 之前觸發(fā)
init?: InitHook;
// createElm 函數(shù)末尾調(diào)用
// 創(chuàng)建完真實 DOM 后觸發(fā)
create?: CreateHook;
// patch 函數(shù)末尾執(zhí)行
// 真實 DOM 添加到 DOM 樹中觸發(fā)
insert?: InsertHook;
// patchVnode 函數(shù)開頭調(diào)用
// 開始對比兩個 VNode 的差異之前觸發(fā)
prepatch?: PrePatchHook;
// patchVnode 函數(shù)開頭調(diào)用
// 兩個 VNode 對比過程中觸發(fā)锚烦,比 prepatch 稍晚
update?: UpdateHook;
// patchVnode 的最末尾調(diào)用
// 兩個 VNode 對比結(jié)束執(zhí)行
postpatch?: PostPatchHook;
// removeVnodes -> invokeDestroyHook 中調(diào)用
// 在刪除元素之前觸發(fā),子節(jié)點的 destroy 也被觸發(fā)
destroy?: DestroyHook;
// removeVnodes 中調(diào)用
// 元素被刪除的時候觸發(fā)
remove?: RemoveHook;
// patch 函數(shù)的最后調(diào)用
// patch 全部執(zhí)行完畢觸發(fā)
post?: PostHook;
}
attributes
updateAttrs 函數(shù)功能
- 更新節(jié)點屬性
- 如果節(jié)點屬性值是 true 設置空置
- 如果節(jié)點屬性值是 false 移除屬性
源碼解析src/package/hooks.ts
帝雇,其他模塊類似
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
var key: string;
var elm: Element = vnode.elm as Element;
var oldAttrs = (oldVnode.data as VNodeData).attrs;
var attrs = (vnode.data as VNodeData).attrs;
// 新舊節(jié)點沒有 attrs 屬性涮俄,返回
if (!oldAttrs && !attrs) return;
// 新舊節(jié)點的 attrs 屬性相同,返回
if (oldAttrs === attrs) return;
oldAttrs = oldAttrs || {};
attrs = attrs || {};
// update modified attributes, add new attributes
// 遍歷新節(jié)點的屬性
for (key in attrs) {
const cur = attrs[key];
const old = oldAttrs[key];
// 如果新舊節(jié)點的屬性值不同
if (old !== cur) {
// 布爾類型值的處理
if (cur === true) {
elm.setAttribute(key, '');
} else if (cur === false) {
elm.removeAttribute(key);
} else {
// ascii 120 -> x
// <svg xmlns="http://www.w3.org/2000/svg">
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur as any);
} else if (key.charCodeAt(3) === colonChar) {
// ascii 58 -> :
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur as any);
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
// <svg xmlns="http://www.w3.org/2000/svg">
elm.setAttributeNS(xlinkNS, key, cur as any);
} else {
elm.setAttribute(key, cur as any);
}
}
}
}
// remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
// 如果舊節(jié)點的屬性在新節(jié)點中不存在摊求,移除
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key);
}
}
}
Diff 算法的執(zhí)行過程
- 循環(huán)結(jié)束
- 當老節(jié)點的所有子節(jié)點先遍歷完 (oldStartIdx > oldEndIdx)禽拔,循環(huán)結(jié)束
- 新節(jié)點的所有子節(jié)點先遍歷完 (newStartIdx > newEndIdx)刘离,循環(huán)結(jié)束
- 如果老節(jié)點的數(shù)組先遍歷完(oldStartIdx > oldEndIdx)室叉,說明新節(jié)點有剩余,把剩余節(jié)點批量插入到右邊
- 如果新節(jié)點的數(shù)組先遍歷完(newStartIdx > newEndIdx)硫惕,說明老節(jié)點有剩余茧痕,把剩余節(jié)點批量刪除