>[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)`
![](https://img.kancloud.cn/b5/79/b579c580b83dd523f6a3caee2ad27d81_599x280.png)
* 实际采用的方法**利用指针进行找同级别的子节点依次比较**,然后再找下一级别的节点比较,这样算法的 时间复 杂度为 `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://img.kancloud.cn/7d/ae/7dae086ded79545de9f358d61a10677d_2600x4182.png)
[参考视频动画链接](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 -- 案例
- 待续