>[success] # patch 函數
1. `patch` 函數 是通過調用`init`閉包返回的函數,會傳入新老的`Vnode`,進行比較后進行視圖渲染,返回新的` VNode`,作為下一次 **patch() 的 oldVnod**
2. 代碼的執行過程:
2.1. 首先執行模塊中的**鉤子函數 pre**
2.2. 執行`sameVnode(oldVnode, vnode)`比較`oldVnode 和 Vnode`,返回`true`調用 `patchVnode()`,找節點的`差異并更新 DOM`,返回`false`,從`oldVnode`中`elm `獲取**真實渲染dom**,在通過`createElm` 根據新的**Vnode創建出對應dom**,獲取**老dom**位置,將**新dom**插入原來老的位置**并刪除老的dom**
~~~
// init 內部返回 patch函數,把vnode渲染成真實dom,并返回vnode
// 不需要額外傳遞modules domapi
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 保存新插入節點的隊列,為了觸發鉤子函數
const insertedVnodeQueue: VNodeQueue = [];
// 執行模塊的 pre 鉤子函數
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果 oldVnode 不是 VNode,創建 VNode 并設置 elm
if (!isVnode(oldVnode)) {
// 把 DOM 元素轉換成空的 VNode
oldVnode = emptyNodeAt(oldVnode);
}
// 如果新舊節點是相同節點(key 和 sel 相同)
if (sameVnode(oldVnode, vnode)) {
// 找節點的差異并更新 DOM
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 如果新舊節點不同,vnode 創建對應的 DOM
// 獲取當前的 DOM 元素
elm = oldVnode.elm!;
parent = api.parentNode(elm);
// 觸發 init/create 鉤子函數,創建 DOM
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 如果父節點不為空,把 vnode 對應的 DOM 插入到文檔中
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
// 移除老節點
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 執行用戶設置的 insert 鉤子函數
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
// 執行模塊的 post 鉤子函數
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
// 返回 vnode
return vnode;
};
~~~
* 簡化這個模型代碼,實際只需要做的`patch` 的主要功能是比對兩個 `VNode `節點,**將「差異」更新到視圖上**
~~~
function patch (oldVnode, vnode, parentElm) {
if (!oldVnode) {
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
} else if (!vnode) {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
} else {
if (sameVnode(oldVNode, vnode)) {
// 相同開始diff 算法比較
patchVnode(oldVNode, vnode);
} else {
// 不同直接刪除舊的,增加新節點。
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
}
}
}
~~~
>[danger] ##### 幾個工具函數說明
* `sameVnode`函數比較新老虛擬dom(Vnode)是否相同,比較的幾個點,`key`,`sel`即選擇器比較創**建元素節點**,`is`是**string**類型作為判斷是否是自定義元素
~~~
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;
const isSameTextOrFragment =
!vnode1.sel && vnode1.sel === vnode2.sel
? typeof vnode1.text === typeof vnode2.text
: true;
return isSameSel && isSameKey && isSameIs && isSameTextOrFragment;
}
~~~
* `emptyNodeAt` 函數將真實dom 轉換為Vnode節點,為了進行Vnode 新老比較,回顧一下`Vnode`結構`sel,data,children,text,elm`,elm記錄的就是真實dom,將真實dom 屬性依次對應傳入到Vnode 函數中
~~~
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)
}
~~~
* `createElm`函數作用,將虛擬 dom 轉變為真實dom元素,插入回`Vnode`對象的`elm`屬性,此時`Vnode`既有**js對象表現形式**,并且表現形式中`elm `屬性記錄了**真實dom**
整段代碼執行順序
1. 首先觸發 `vnode` 中 `init` 鉤子函數
~~~
h(
'div',
{
hook: {
init() {
console.log(1111111111) // 執行后控制臺打印
},
},
},
[(h('span', '子節點'), 1, 23, 34)]
)
~~~
2. 開始解析 `h` 函數中`sel `參數,sel參數設置可能是**選擇器字符串**`div#container.cls`可能是**單純的標簽**`div`,有可能是 `!`**評論節點**,或者是沒有`sel`參數 只是**單純的文本節點**,因此接下來就是對這些標簽分析轉換成對應真實`dom`
~~~
// 整體代碼流程
// 解析選擇器,設置標簽的 id 和 class 屬性
// 執行模塊的 create 鉤子函數
// 如果 vnode 有 children,創建子 vnode 對應的 DOM,追加到 DOM 樹
// 如果 vnode 的 text 值是 string/number,創建文本節點并追擊到 DOM 樹
// 行用戶設置的 create 鉤子函數
// 如果有用戶設置的 insert 鉤子函數,把 vnode 添加到隊列中
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any, data = vnode.data;
if (data !== undefined) {
// 執行用戶設置的 init 鉤子函數
const init = data.hook ? .init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
let children = vnode.children,
sel = vnode.sel;
if (sel === '!') {
// 如果選擇器是!,創建評論節點
if (isUndef(vnode.text)) {
vnode.text = '';
}
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// 如果選擇器不為空
// 解析選擇器
// Parse selector 如果是選擇器字符串div#container.cls,需要獲取sel 標簽(div) let vnode = h('div#container.cls', 'hello word')
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) :
api.createElement(tag);
// 在這之前都是對虛擬dom 的sel屬性進行解析,為了對提供的sel 選擇器而創建dom準備的
// 注意創建真實dom 有兩種一種是createElementNS這是創建svg,一種是createElement 創建普通dom
// ---------------------------------------------
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot +
1).replace(/\./g, ' '));
// 執行模塊的 create 鉤子函數
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode,
vnode);
// 如果 vnode 中有子節點,創建子 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,創建文本節點并追加到 DOM 樹
api.appendChild(elm, api.createTextNode(vnode.text));
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
// 執行用戶傳入的鉤子 create
hook.create ? .(emptyNode, vnode);
if (hook.insert) {
// 把 vnode 添加到隊列中,為后續執行 insert 鉤子做準備
insertedVnodeQueue.push(vnode);
}
}
} else {
// 如果選擇器為空,創建文本節點
vnode.elm = api.createTextNode(vnode.text!);
}
// 返回新創建的 DOM
return vnode.elm;
}
~~~
* 其他幫助函數
~~~
function addVnodes(parentElm: Node,
before: Node | null,
vnodes: Array<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 removeVnodes(parentElm: Node,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number): void {
for (; startIdx <= endIdx; ++startIdx) {
let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
if (ch != null) {
// 如果 sel 有值
if (isDef(ch.sel)) {
// 執行 destroy 鉤子函數(會執行所有子節點的 destroy 鉤子函數)
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
// 創建刪除的回調函數
rm = createRmCb(ch.elm as Node, listeners);
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
// 執行用戶設置的 remove 鉤子函數
if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
i(ch, rm);
} else {
// 如果沒有用戶鉤子函數,直接調用刪除元素的方法
rm();
}
} else { // Text node
// 如果是文本節點,直接調用刪除元素的方法
api.removeChild(parentElm, ch.elm as Node);
}
}
}
}
function invokeDestroyHook(vnode: VNode) {
let i: any, j: number, data = vnode.data;
if (data !== undefined) {
// 執行用戶設置的 destroy 鉤子函數
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
// 調用模塊的 distroy 鉤子函數
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
// 執行子節點的 distroy 鉤子函數
if (vnode.children !== undefined) {
for (j = 0; j < vnode.children.length; ++j) {
i = vnode.children[j];
if (i != null && typeof i !== "string") {
invokeDestroyHook(i);
}
}
}
}
}
function createRmCb(childElm: Node, listeners: number) {
// 返回刪除元素的回調函數
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
~~~
>[info] ## patchVnode
`patchVnode` 函數作為`patch`函數中新老`Vnode`進行比較依靠的是屬性的`標簽和key`比較相同可以觸發 `patchVnode` 函數,找節點的**差異并更新 DOM**
~~~
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)
const elm = (vnode.elm = oldVnode.elm)!
if (oldVnode === vnode) return
if (
vnode.data !== undefined ||
(isDef(vnode.text) && vnode.text !== oldVnode.text)
) {
vnode.data ??= {}
oldVnode.data ??= {}
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode)
vnode.data?.hook?.update?.(oldVnode, vnode)
}
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
// 如果 vnode.text 未定義,有text 就沒有children
if (isUndef(vnode.text)) {
// 如果新老節點都有 children
if (isDef(oldCh) && isDef(ch)) {
// 使用 diff 算法對比子節點,更新子節點
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {
// 如果新節點有 children,老節點沒有 children
// 如果老節點有text,清空dom 元素的內容
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
// 批量添加子節點
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果老節點有children,新節點沒有children
// 批量移除子節點
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果老節點有 text,清空 DOM 元素
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果沒有設置 vnode.text
if (isDef(oldCh)) {
// 如果老節點有 children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 設置 DOM 元素的 textContent 為 vnode.text
api.setTextContent(elm, vnode.text!)
}
hook?.postpatch?.(oldVnode, vnode)
}
~~~
代碼中在開始時有兩行代碼,`elm`之前說過是`Vnode `一個屬性,用來記錄`Vnode `真實`dom`的對象,先將老`Vnode`已將創建出來的真實`dom`賦值給新`Vnode `雖然此時并不確認新老相等但先進行賦值操作,如果后續中發現新老Vnode 相等此時不需要做任何改變了,直接 return 掉
~~~
const elm = (vnode.elm = oldVnode.elm)!;
if (oldVnode === vnode) return;
~~~
* 如果不想等進入比較階段,比較其實分兩個情況,就是新`vnode`是文本節點(文本節點沒有什么特點比較情況,相同不變,不同更改為新的 `vnode` 文本),新`vnode`是非文本類型
~~~
// 如果 vnode.text 未定義,有text 就沒有children
if (isUndef(vnode.text)) {
// ... 非文本情況
} else if (oldVnode.text !== vnode.text) {
// .... 是文本的情況
// 如果沒有設置 vnode.text
if (isDef(oldCh)) {
// 如果老節點有 children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 設置 DOM 元素的 textContent 為 vnode.text
api.setTextContent(elm, vnode.text!)
}
~~~
* `oldCh`與`ch`都存在且不相同時,使用`updateChildren`函數來更新子節點,使用 `diff` 算法對比子節點,更新子節點
* 如果只有`ch`存在的時候,如果老節點是文本節點則先將節點的文本清除,然后將`ch`批量插入插入到節點elm下
* 同理當只有`oldch`存在時,說明需要將老節點通過`removeVnodes`全部清除
* 只有老節點是文本節點的時候,清除其節點文本內容
~~~
if (oldCh && ch && (oldCh !== ch)) {
updateChildren(elm, oldCh, ch);
} else if (ch) {
if (oldVnode.text) nodeOps.setTextContent(elm, '');
addVnodes(elm, null, ch, 0, ch.length - 1);
} else if (oldCh) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (oldVnode.text) {
nodeOps.setTextContent(elm, '')
}
~~~
>[danger] ##### updateChildren -- diff
`updateChildren ` 是真正的`diff`發生區域,能進入`patchVnode `函數都是新老父節點相同的,接下來要比較的其實是新老子節點,如果新老子節點形式不同(例如我是文本你是dom元素),但如果新老子節點形式相同要做就是對應位置比較,即如何性能最優的去比較
* 最笨的方法拿每一個老節點虛擬`dom`和新節點的去比較,如下圖嵌套三層的情況下時間復雜度為 `O(n^3)`

* 實際采用的方法**利用指針進行找同級別的子節點依次比較**,然后再找下一級別的節點比較,這樣算法的 時間復 雜度為 `O(n)`,考慮的就是dom復用,有時候重新排序只是新老dom節點位置發生變化,如果能**復用相同的dom而不是重新創建dom**這樣就又可以減少一部分性能開銷
`oldStartVnode / newStartVnode` (舊開始節點 / 新開始節點)
`oldEndVnode / newEndVnode` (舊結束節點 / 新結束節點)
`oldStartVnode / oldEndVnode` (舊開始節點 / 新結束節點)
`oldEndVnode / newStartVnode` (舊結束節點 / 新開始節點)
`oldStartIdx`、`newStartIdx`、`oldEndIdx`以及`newEndIdx`兩兩比對的過程,一共會出現 2\*2=4
* **這四種其實對應下圖四種情況他們找到能復用的vnode節點**,當在相同時候會在調用`patchVnode`函數 然后觸發`if (oldVnode === vnode) return` 這里
~~~
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
~~~

[參考視頻動畫鏈接](https://www.bilibili.com/video/BV1b5411V7i3?t=479)
第五種情況最復雜,第五種沒有規律,但是存在可以復用的節點,使用新開始節點的key值在【舊開始節點-舊結束節點】的子數組中找相同key的節點。若沒有在子數組中找到相同key節點則新開始節點是新節點,創建一個該節點的真實DOM節點插入到舊開始節點指向的真實DOM節點之前;若找到相同key的節點獲取該舊節點,比較新、舊節點的選擇器是否相同,選擇器不同則創建一個該節點的真實DOM節點插入到舊開始節點指向的真實DOM節點之前,選擇器相同則對比兩個節點的差異執行相應的操作(如更新DOM節點),并將子數組中key節點指向到真實DOM節點移動到到舊開始索引指向的真實DOM節點之前(移動的是真實DOM而不是虛擬DOM,虛擬DOM無變化)。最后將新開始索引指向下一個節點
~~~
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)) {
// 1. 比較老開始節點和新開始節點
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 2.比較老結束節點和新的結束節點
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 3.比較老開始節點和新的結束節點
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
// 4.比較老結束節點和新的開始節點
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 開始節點和結束節點都不相同
// 使用newStartNode 的key在老節點數組中找相同節點
// 先設置記錄 key 和index對象
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 遍歷 newStartVnode,從舊節點中找相同key的oldVnode的索引
idxInOld = oldKeyToIdx[newStartVnode.key as string]
// 如果是新的vnode
if (isUndef(idxInOld)) { // New element
// 如果沒找到,newStartNode 是新節點
// 創建元素插入DOM樹
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 如果找到相同key的老節點,記錄到elmToMove遍歷
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
// 如果新舊節點的選擇器不同
// 創建新開始節點對應的DOM元素,插入到DOM樹種
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 如果相同,patchVnode()
// 把elmToMove 對應的DOM元素,移動到左邊
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
// 重新給newStartVnode 復制,指向下一個新節點
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)
}
}
}
~~~
>[info] ## key 作用
`Key `一般用于生成一列同類型節點時使用,這種情況下,當修改這些同類型節點的某個內容、變更位置、刪除、添加等時,此時界面需要更新視圖,調用 `patch `方法通過對比**新、舊節點的變化來更新視圖**。其從根節點開始若**新、舊 VNode 相同**,則調用`patchVnode`
`patchVnode `中若新節點沒有文本,且新節點和舊節點都有有子點,則需對子節點進行 `Diff `操作,即調用` updateChildren`,`Key` 就在 `updateChildren `起了大作用
`updateChildren `中會遍歷對比上步中的新、舊節點的子節點,并按 Diff 算法通過 sameVnode 來判斷要對比的節點是否相同
**若這里的子節點未設置 Key**,則此時的每個新、舊子節點在執行`sameVnode `時會判定相同,然后再次執行一次 patchVnode 來對比這些子節點的子節點
**若設置了 Key**,當執行 sameVnode
**若 Key 不同 sameVnode 返回 false**,然后執行后續判斷;
**若 Key 相同 sameVnode 返回 true**,然后再執行 patchVnode 來對比這些子節點的子節點
即,使用了 Key 后,可以優化新、舊節點的對比判斷,減少了遍歷子節點的層次,少使用很多次 `patchVnode`,在第五種情況最明顯,會在老Vnode中找到新Vnode 能復用的節點直接使用,這樣就避免了重新渲染真實dom的開銷
- 工程化 -- Node
- vscode -- 插件
- vscode -- 代碼片段
- 前端學會調試
- 谷歌瀏覽器調試技巧
- 權限驗證
- 包管理工具 -- npm
- 常見的 npm ci 指令
- npm -- npm install安裝包
- npm -- package.json
- npm -- 查看包版本信息
- npm - package-lock.json
- npm -- node_modules 層級
- npm -- 依賴包規則
- npm -- install 安裝流程
- npx
- npm -- 發布自己的包
- 包管理工具 -- pnpm
- 模擬數據 -- Mock
- 頁面渲染
- 渲染分析
- core.js && babel
- core.js -- 到底是什么
- 編譯器那些術語
- 詞法解析 -- tokenize
- 語法解析 -- ast
- 遍歷節點 -- traverser
- 轉換階段、生成階段略
- babel
- babel -- 初步上手之了解
- babel -- 初步上手之各種配置(preset-env)
- babel -- 初步上手之各種配置@babel/helpers
- babel -- 初步上手之各種配置@babel/runtime
- babel -- 初步上手之各種配置@babel/plugin-transform-runtime
- babel -- 初步上手之各種配置(babel-polyfills )(未來)
- babel -- 初步上手之各種配置 polyfill-service
- babel -- 初步上手之各種配置(@babel/polyfill )(過去式)
- babel -- 總結
- 各種工具
- 前端 -- 工程化
- 了解 -- Yeoman
- 使用 -- Yeoman
- 了解 -- Plop
- node cli -- 開發自己的腳手架工具
- 自動化構建工具
- Gulp
- 模塊化打包工具為什么出現
- 模塊化打包工具(新) -- webpack
- 簡單使用 -- webpack
- 了解配置 -- webpack.config.js
- webpack -- loader 淺解
- loader -- 配置css模塊解析
- loader -- 圖片和字體(4.x)
- loader -- 圖片和字體(5.x)
- loader -- 圖片優化loader
- loader -- 配置解析js/ts
- webpack -- plugins 淺解
- eslit
- plugins -- CleanWebpackPlugin(4.x)
- plugins -- CleanWebpackPlugin(5.x)
- plugin -- HtmlWebpackPlugin
- plugin -- DefinePlugin 注入全局成員
- webapck -- 模塊解析配置
- webpack -- 文件指紋了解
- webpack -- 開發環境運行構建
- webpack -- 項目環境劃分
- 模塊化打包工具 -- webpack
- webpack -- 打包文件是個啥
- webpack -- 基礎配置項用法
- webpack4.x系列學習
- webpack -- 常見loader加載器
- webpack -- 移動端px轉rem處理
- 開發一個自己loader
- webpack -- plugin插件
- webpack -- 文件指紋
- webpack -- 壓縮css和html構建
- webpack -- 清里構建包
- webpack -- 復制靜態文件
- webpack -- 自定義插件
- wepack -- 關于靜態資源內聯
- webpack -- source map 對照包
- webpack -- 環境劃分構建
- webpack -- 項目構建控制臺輸出
- webpack -- 項目分析
- webpack -- 編譯提速優護體積
- 提速 -- 編譯階段
- webpack -- 項目優化
- webpack -- DefinePlugin 注入全局成員
- webpack -- 代碼分割
- webpack -- 頁面資源提取
- webpack -- import按需引入
- webpack -- 搖樹
- webpack -- 多頁面打包
- webpack -- eslint
- webpack -- srr打包后續看
- webpack -- 構建一個自己的配置后續看
- webpack -- 打包組件和基礎庫
- webpack -- 源碼
- webpack -- 啟動都做了什么
- webpack -- cli做了什么
- webpack - 5
- 模塊化打包工具 -- Rollup
- 工程化搭建代碼規范
- 規范化標準--Eslint
- eslint -- 擴展配置
- eslint -- 指令
- eslint -- vscode
- eslint -- 原理
- Prettier -- 格式化代碼工具
- EditorConfig -- 編輯器編碼風格
- 檢查提交代碼是否符合檢查配置
- 整體流程總結
- 微前端
- single-spa
- 簡單上手 -- single-spa
- 快速理解systemjs
- single-sap 不使用systemjs
- monorepo -- 工程
- Vue -- 響應式了解
- Vue2.x -- 源碼分析
- 發布訂閱和觀察者模式
- 簡單 -- 了解響應式模型(一)
- 簡單 -- 了解響應式模型(二)
- 簡單 --了解虛擬DOM(一)
- 簡單 --了解虛擬DOM(二)
- 簡單 --了解diff算法
- 簡單 --了解nextick
- Snabbdom -- 理解虛擬dom和diff算法
- Snabbdom -- h函數
- Snabbdom - Vnode 函數
- Snabbdom -- init 函數
- Snabbdom -- patch 函數
- 手寫 -- 虛擬dom渲染
- Vue -- minVue
- vue3.x -- 源碼分析
- 分析 -- reactivity
- 好文
- grpc -- 瀏覽器使用gRPC
- grcp-web -- 案例
- 待續