Virtual DOM的实现原理总结超详细版(二)diff算法
5.10 diff算法
重温虚拟DOM
在说 Diff算法
前,先重温一下什么是 虚拟DOM
吧,有利于后面对 Diff算法
的理解加深。
虚拟DOM
是 **一个用来表示真实DOM的对象**
请看以下 真实DOM
:
<ul id="list">
<li class="item">Q7Long</li>
<li class="item">james</li>
<li class="item">23</li>
</ul>
对应的 虚拟DOM
为:
let oldVDOM = { // 旧虚拟DOM
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, children: ['Q7Long']
},
{
tagName: 'li', props: { class: 'item' }, children: ['james']
},
{
tagName: 'li', props: { class: 'item' }, children: ['23']
},
]
}
这里我们可以修改一个 li标签
的内容部分:
<ul id="list">
<li class="item">Q7Long</li>
<li class="item">james</li>
// 对第三个li的内容进行修改
<li class="item">张祺龙</li>
</ul>
这时候生成的 新的虚拟DOM
为:
let newVDOM = { // 新虚拟DOM
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, children: ['Q7Long']
},
{
tagName: 'li', props: { class: 'item' }, children: ['james']
},
{
tagName: 'li', props: { class: 'item' }, children: ['张祺龙']
},
]
}
虚拟DOM并不一定比真实DOM快:虚拟DOM算法操作真实DOM,性能高于直接操作真实DOM
虚拟DOM算法 = 虚拟DOM + Diff算法
总结: **Diff算法是一种对比算法**
。对比两者是 旧虚拟DOM和新虚拟DOM
,对比出是哪个 虚拟节点
更改了,找出这个 虚拟节点
,并只更新这个虚拟节点所对应的 真实节点
,而不用更新其他数据没发生改变的节点,实现 精准
地更新真实DOM,进而 提高效率
。
使用虚拟DOM算法的损耗计算
: 总损耗 = 虚拟DOM增删改+(与Diff算法效率有关)真实DOM差异增删改+(较少的节点)排版与重绘
直接操作真实DOM的损耗计算
: 总损耗 = 真实DOM完全增删改+(可能较多的节点)排版与重绘
diff算法的执行流程
// updateChildren()是虚拟DOM的一个核心,作用是:对比新旧节点的children子节点的差异,更新对应的DOM
// 如果我们更新一个数据,直接修改到DOM上,直接渲染到真实的DOM上,就会引起整个DOM树的重绘和重拍,那么DOM的更新和渲染开销就会非常大了
// 那么有没有办法值更新我们修改部分的DOM呢?diff算法就是帮助我们完成这个操作的
首先我们会根据真实的DOM生成一个虚拟DOM,当我们虚拟DOM某个节点中数据发生改变之后,就会生成一个vnode,那么这个vnode就会和旧的oldVnode进行对比,如果发现有不一样,那么就直接修改对应的数据,然后把数据更新到对应的DOM上,整个diff算法的过程中,不断调用patchVnode这个方法,通过调用patchVnode方法,它回去不断比较新旧节点,然后去进行更新操作
// 在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较
如果要对比两棵树的差异,我们可以先取第一棵树的每一个节点,依次和第二棵树的每一个节点进行对比,虽然这种办法也可以解决问题,但是效率太低了。
因为我们在DOM中很少对一个父节点进行移动,那么我们一般都是找同级别的结点进行比较,然后再进行下一个节点的比较,同级比较如下图:
两组同级别节点比较流程图:
在新节点和旧节点的开始和结尾都设置了一些索引值,在比较的过程中,需要移动索引值进行比较
// 四种情况
1. 比较 旧的开始节点 和 新的开始节点
2. 比较 旧的结束的结点 和 新的结束节点 进行比较
3. 旧的 开始节点 和 新的结束节点 进行比较
4. 旧的 结束节点 和 新的开始节点 进行比较
-
第一种情况:新的开始节点和旧的开始节点进行比较
-
如果是
sameVnode
- 进行
patchVnode
更新 - 同时
oldstartIndex
,newStartIndex
各自右移一位
- 进行
-
如果不是
sameVnode
,则进入下一步条件
-
1. 如果说旧的开始节点和新的开始节点进行比较的时候,通过sameVnode()方法(比较key和sel属性是否相同),如果发现这两个是相同的Vnode,也就是说相同的节点,那么就会调用 patchVnode方法 对比新旧节点的差异,完成节点的更新
更新完之后,索引值就开是往后 +1,然后进行下一个节点的比较
2. 假如说新的开始节点和旧的开始节点不是同一个节点,那么就会将 旧的结束节点和新的结束节点进行比较,如果这个时候相同的话,那么将索引值-1,往前移动,进行下一个节点的比较
/*
对比新旧节点的子节点中的差异
parentElm 父节点
oldCh 对比的旧节点
newCh 对比的新节点 */
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) {
// 判断这些节点是否为null 如果是对节点进行重新赋值
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];
//1. 比较老的开始节点和新的开始节点是不是相同节点(key 和 sel 进行比较),这里是同一个节点的操作
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果两个节点是相同节点那么调用patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
// 在这个地方更改它们的索引值,然后进入下一次循环,这里是对新老节点 +1
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
//2. 如果说上面的老的开始节点和新的开始节点不是相同节点
// 那么就会在这里比较老的结束节点和新的结束节点,看看是不是同一节点
} else if(旧的结束节点和新的结束节点进行比较){
....
}else if(旧的结束节点和新的结束节点进行比较){
....
}
}else if(旧的开始节点和新的结束节点进行比较){
....
}else if(旧的结束节点和新的开始节点进行比较){
....
}
}
}
-
旧的结束节点和新的结束节点进行比较
-
如果是
sameVnode
- 进行
patchVnode
更新 - 同时
oldstartIndex
,newStartIndex
各自左移一位
- 进行
-
如果不是
sameVnode
,则进入下一步条件
-
else if (sameVnode(oldEndVnode, newEndVnode)) {
// 调用 patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
// 修改它们的索引值,与上面的索引值做法相反,这里是-1获取前面的一个节点,然后进入下次循环
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
//3. 比较老的开始节点和新的结束节点是不是相同节点
}
-
旧的开始节点和新的结束节点进行比较
-
如果是
sameVnode
- 进行
patchVnode
更新 - 以
oldEndVnode.elm.nextSibling
为锚点,将oldStartVnode.elm
插入到oldEndVnode.elm.nextSibling
之前(因为这个时候对比的是新children
的尾部,所以如果此时这个条件,旧children
的头部可以移动到上个确定的旧尾节点之前,那从上图就可以看出来,{key = 9}
的旧尾节点是上个确定的旧尾节点,从已有的变量中,可以得到是oldEndVnode
的下一个相邻节点) - 同时
newEndIndex
左移一位,oldStartIndex
右移一位
- 进行
-
如果不是
sameVnode
,则进入下一步条件
// 比较过程 首先将旧的开始节点和新的结束节点进行比较,如果是相同节点的话,那么就会调用patchVnode()进行一个节点的更新,并且会把旧的开始节点对用的DOM元素移动到最右侧,并且会更新对应的索引值,旧的开始节点就会向右移动一位,相当于索引值++,对应的新节点的结束节点向左移动一位,相当于索引值--
-
```
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 调用 patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
// 调用 insertBefore方法,将老的开始节点移动到老的结束节点之后
api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
// 然后这里将老的开始节点+1
oldStartVnode = oldCh[++oldStartIdx];
// 然后这里将新的结束节点-1 进入下次循环
newEndVnode = newCh[--newEndIdx];
// 4. 这里比较的是老的结束节点和新的开始节点是不是相同节点
}
```
3. 旧的结束节点和新的开始节点进行比较
对旧 `children` 的尾部和新 `children` 的头部进行对比
- 如果是 `sameVnode`
- 进行 `patchVnode` 更新
- 以 `oldStartVnode.elm` 为锚点,将 `oldEndVnode.elm` 插入到 `oldStartVnode.elm` 之前(因为此时对比的是 `**新children的头部**` ,所以对比成功时,说明 `oldEndVnode.elm` 可以复用,那么可以将其插入到当前旧children头部已经确定对比过的最右面一个节点之后,图中看就是要插入到 `oldStartVnode.elm` 之前)
- 将 `oldEndIndex` 左移一位, `newStartIndex` 右移一位
- 如果不是 `sameVnode` ,进入下一步条件
```
// 过程与上面类似
把旧的结束节点和新的开始节点进行比较,如果相同的话,那么就会调用patchVnode()进行一个节点的更新。并且把旧的结束节点对应的DOM元素移动到最左侧,更新相应的索引值
```
```
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 调用 patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
// 更新完DOM之后,这里将老的结束节点插入到老的开始节点之前
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
// 这里将老的结束节点-1
oldEndVnode = oldCh[--oldEndIdx];
// 这里将新的开始节点+1
newStartVnode = newCh[++newStartIdx];
}
```
3. 如果都不符合的话,第五种情况
由于前四步的对比都无法成立,接下来就是未知序列的处理,首先大体说下原理吧
0. 我们可以想到的是,我需要尽最大力来完成旧节点的复用,所以我们遍历当前的 `newStartVnode`的时候,需要看在oldChildren中的 `[oldStartIndex, oldEndIndex]`区间内是否有相同节点可以复用
0. 具体实现方式,Vue2是这样处理的:对 `oldChildren`中的 `[oldStartIndex, oldEndIndex]`区间内所有旧节点生成 `key --> oldIndex`的映射哈希表,也就是我们最一开始初始化时声明的变量 `oldKeyToIdx`
0. 当生成 `oldKeyToIdx`之后,就可以开始对当前的 `newStartVnode`进行查找了,而 `idxInOld`这个变量就是每次 `newStartVnode`在 `oldChildren`中的可以复用的旧节点的索引位置
- 如果 `idxInOld`存在,则说明 `oldChildren`存在可以复用的节点
- 进行 `patchVnode`更新
- 重置 `oldChildren`中对应 `idxInOld`的值为 `undefined`
- 进行移动,将找到的 `oldVnode.elm`移动到 `oldStartVnode.elm`之前(具体原因与第四步中一致,因为 `oldStartVnode`没有被比对,但是它之前的节点已经被比对过了,而第五步中是遍历 `newStartVnode`,也就是 `newChildren`的未知序列头部节点)
- 如果 `idxInOld`不存在,就说明 `oldChildren`中没有可以复用的节点
- 新建 `newStartVnode`这个vnode对应的elm,锚点为 `oldStartVnode.elm`
0. `newStartIndex`右移一位
```
遍历新节点,使用新节点的开始节点的key,在老节点的数组中进行查找,看看有没有具有相同key值的结点,
1.如果没有找到,那么说明这个新的开始节点是一个新节点,只需要把这个节点的DOM元素创建出来,插入到DOM树的最前面即可
2.如果说找到了key值相同的节点,还要对比两个节点的 sel,对比它们的选择器是否相同,如果不相同
说明节点被修改了,那么需要重新创建对应的DOM元素,插入到DOM树的最前面
3.如果说找到了相同key值的结点,并且两个节点的选择器sel也是相同的话,就会给节点起一个名字叫做elmToMove,把它对应的DOM元素,给它移动到最左侧
// 这个情况会一直循环下去,结束有两种情况
1. 老节点对应的 所有的子节点先遍历完,这个循环会结束
2. 新节点对应的 所有的子节点先遍历完,这个循环也会结束
//1. 如果说 老节点对应的 所有的子节点的数组先遍历完,那么说明新节点对应的子节点有剩余,那么这些节点就是我们要新增节点,我们把这些节点批量插入到老节点的最右侧即可
//2. 如果说 新节点对应的 所有的子节点的数组先遍历完,那么说明老节点对应的子节点有剩余,那么把老节点的剩余节点给批量删除掉
```
// 如果以上四种情况都不满足的话,进入 else
// 使用newStarNode的key在老数组节点中查找相同的节点
} else {
if (oldKeyToIdx === undefined) {
// 调用createKeyToOldIdx()方法 将 老节点数组、老节点开始索引,老节点结束索引都传入方法中
// 这个方法返回的是一个对象,这个对象中有一个属性就是老节点的 key
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 通过新节点的开始节点newStartVnode的key,从对象中查找对应的内容
// 如果找到了,那么返回的就是从老节点中找到相同 key 的老节点的索引值(绕口)
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果没有找到响应的索引值的话,执行这一层if里面的内容
if (isUndef(idxInOld)) { // New element
// 如果没有找到说明新的开始节点是没有在老节点的数组中的,说明新的开始节点就是一个全新的元素
// 有个全新的元素,那么就是用createElm创建,创建好之后插入到了老的开始节点之前
// 并且重新给newStartVnode赋值,看看能不能找到循环,如果找不到跟上次是一样的
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
newStartVnode = newCh[++newStartIdx];
} else {
// 这里说明是找到相同key的老节点,并且将这个老节点取出来存储到 elmToMove 数组中
elmToMove = oldCh[idxInOld];
// 然后再判断一下这两者之前的选择器是否相等
if (elmToMove.sel !== newStartVnode.sel) {
// 如果sel是不相等,说明这个节点被修改过了,那么修改过就需要调用createElm()创建新的开始节点对应的DOM元素,插入到老的开始节点之前
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
} else {
// 如果相等的话调用patchVnode更新DOM
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 将移走的老节点的这个位置设置为undefined
oldCh[idxInOld] = undefined as any;
// 然后把我们找到的这个老的节点移动到,老的开始节点之前
api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
}
// 当前这个新的开始节点处理完之后,那么将这个下标+1,切换到下一个节点 newStarIdx指针右移一位
newStartVnode = newCh[++newStartIdx];
}
}
5.11 虚拟DOM核心-updateChildren()函数源码查看
// updateChildren() 整体代码 上面是拆分代码
/* 对比新旧节点的子节点中的差异
parentElm 父节点
oldCh 对比的旧节点
newCh 对比的新节点
*/
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) {
// 判断这些节点是否为null 如果是对节点进行重新赋值
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];
//1. 比较老的开始节点和新的开始节点是不是相同节点(key 和 sel 进行比较),这里是同一个节点的操作
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果两个节点是相同节点那么调用patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
// 在这个地方更改它们的索引值,然后进入下一次循环,这里是对新老节点 +1
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
//2. 如果说上面的老的开始节点和新的开始节点不是相同节点
// 那么就会在这里比较老的结束节点和新的结束节点,看看是不是同一节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 调用 patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
// 修改它们的索引值,与上面的索引值做法相反,这里是-1获取前面的一个节点,然后进入下次循环
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
//3. 比较老的开始节点和新的结束节点是不是相同节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 调用 patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
// 调用 insertBefore方法,将老的开始节点移动到老的结束节点之后
api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
// 然后这里将老的开始节点+1
oldStartVnode = oldCh[++oldStartIdx];
// 然后这里将新的结束节点-1 进入下次循环
newEndVnode = newCh[--newEndIdx];
// 4. 这里比较的是老的结束节点和新的开始节点是不是相同节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 调用 patchVnode方法,更新节点到DOM树,更新DOM元素
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
// 更新完DOM之后,这里将老的结束节点插入到老的开始节点之前
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
// 这里将老的结束节点-1
oldEndVnode = oldCh[--oldEndIdx];
// 这里将新的开始节点+1
newStartVnode = newCh[++newStartIdx];
// 如果以上四种情况都不满足的话,进入 else
// 使用newStarNode的key在老数组节点中查找相同的节点
} else {
if (oldKeyToIdx === undefined) {
// 调用createKeyToOldIdx()方法 将 老节点数组、老节点开始索引,老节点结束索引都传入方法中
// 这个方法返回的是一个对象,这个对象中有一个属性就是老节点的 key
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 通过新节点的开始节点newStartVnode的key,从对象中查找对应的内容
// 如果找到了,那么返回的就是从老节点中找到相同 key 的老节点的索引值(绕口)
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果没有找到响应的索引值的话,执行这一层if里面的内容
if (isUndef(idxInOld)) { // New element
// 如果没有找到说明新的开始节点是没有在老节点的数组中的,说明新的开始节点就是一个全新的元素
// 有个全新的元素,那么就是用createElm创建,创建好之后插入到了老的开始节点之前
// 并且重新给newStartVnode赋值,看看能不能找到循环,如果找不到跟上次是一样的
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
newStartVnode = newCh[++newStartIdx];
} else {
// 这里说明是找到相同key的老节点,并且将这个老节点取出来存储到 elmToMove 数组中
elmToMove = oldCh[idxInOld];
// 然后再判断一下这两者之前的选择器是否相等
if (elmToMove.sel !== newStartVnode.sel) {
// 如果sel是不相等,说明这个节点被修改过了,那么修改过就需要调用createElm()创建新的开始节点对应的DOM元素,插入到老的开始节点之前
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
} else {
// 如果相等的话调用patchVnode更新DOM
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 将移走的老节点的这个位置设置为undefined
oldCh[idxInOld] = undefined as any;
// 然后把我们找到的这个老的节点移动到,老的开始节点之前
api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
}
// 当前这个新的开始节点处理完之后,那么将这个下标+1,切换到下一个节点
newStartVnode = newCh[++newStartIdx];
}
}
}
// 表明循环结束的条件 老节点数组遍历完成 新节点数组便利完成 任其一都可以
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
// 说明老节点数组遍历完成,新节点数组有剩余
if (oldStartIdx > oldEndIdx) {
// 把新节点插入到 before 的位置
before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
//调用addVnodes方法, parentElm 父节点 before插入到的位置 newCh新插入节点的数组 newStartIdx newEndIdx记录是哪个新节点要插入到before的位置
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
//调用removeVnodes()方法删除老节点中剩余的内容, parentElm 父节点 oldCh老节点的children数组oldStartIdx oldEndIdx 记录了要删除的老节点的开始位置和结束位置
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
5.12 钩子函数介绍
源码位置:node_modules/snabbdom/src/hooks.ts
import {VNode} from './vnode';
export type PreHook = () => any;
export type InitHook = (vNode: VNode) => any;
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any;
export type InsertHook = (vNode: VNode) => any;
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any;
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any;
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any;
export type DestroyHook = (vNode: VNode) => any;
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any;
export type PostHook = () => any;
export interface Hooks {
// pretch函数开始执行时触发
pre?: PreHook;
// 在createElement开始执行之前触发,即Vnode转成真实DOM之前触发
init?: InitHook;
// 在createElement之后触发,即创建完真实DOM之后触发
create?: CreateHook;
// 在patch的末尾执行,即真实DOM添加到DOM树中触发
insert?: InsertHook;
// patchVnode调用之前触发,即对比两个Vnode差异之前触发
prepatch?: PrePatchHook;
// update是两个Vnode对比过程中触发,比prepatch稍晚一点
update?: UpdateHook;
// 是在整个patchVnode末尾调用,说明两个Vnode对比结束
postpatch?: PostPatchHook;
// 回收,就是在删除元素之前触发
destroy?: DestroyHook;
// 移除,在元素被删除的时候触发
remove?: RemoveHook;
// 在整个patch函数最后被调用触发的
post?: PostHook;
}
// 重点关注create 和 update
// 由于create是在createElement之后触发,即创建完真实DOM之后触发,可以在create钩子函数中,对DOM进行一些操作,比如给DOM元素注册事件,给DOM元素设置属性
5.13 模块源码分析
源码位置:node_modules/snabbdom/src/modules/attributes.ts
// 由于模块中实现都是类似的,所以我们这里抽出一个来看,比如我们看attributes.ts处理元素属性的模块
import {VNode, VNodeData} from '../vnode';
// 导入module,那我们就可以拿到常用的钩子函数
import {Module} from './module';
// because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237
declare global {
interface Element {
setAttribute(name: string, value: string | number | boolean): void;
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string | number | boolean): void;
}
}
export type Attrs = Record<string, string | number | boolean>
const xlinkNS = 'http://www.w3.org/1999/xlink';
const xmlNS = 'http://www.w3.org/XML/1998/namespace';
const colonChar = 58;
const xChar = 120;
// 定义一个函数 updateAttrs 参数:oldVnode旧节点 vnode新节点
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
var key: string, elm: Element = vnode.elm as Element,
// 拿到旧节点中的属性,存储到 oldAttrs 中
oldAttrs = (oldVnode.data as VNodeData).attrs,
// 拿到新节点中的属性,存储到 attrs 中
attrs = (vnode.data as VNodeData).attrs;
// 看看能不能拿到旧节点中的属性和新节点中的属性,如果新老节点都没有 attrs 属性的话,直接返回
if (!oldAttrs && !attrs) return;
// 看看老节点中的属性和新节点中的属性是否相同,如果相同那么直接返回
if (oldAttrs === attrs) return;
oldAttrs = oldAttrs || {};
attrs = attrs || {};
// update modified attributes, add new attributes
// attrs是新节点中的属性,对新节点中的属性进行遍历
for (key in attrs) {
// 拿到新节点中属性对应的值
const cur = attrs[key];
// 拿到老节点中属性对应的值
const old = oldAttrs[key];
// 判断新老节点中的值是否相同
if (old !== cur) {
// 先判断新节点中的值,是否为Boolean类型的值
if (cur === true) {
// 如果是的话,那么这个属性值就是一个Boolean类型的属性
// (比较有特点的就是 checked selected)
// 上面说的 vnode.elm as Element
//eml就是新节点中的DOM元素 给新节点调用setAttribute()方法,把属性设置给DOM元素,并且这个值是空字符串
elm.setAttribute(key, "");
// 如果条件不成立,如果是个false的话
} else if (cur === false) {
// vnode.elm as Element
// 直接将属性从元素节点上移除
elm.removeAttribute(key);
} else {
// xChar 在上面是被设置了120也就是x,把这个属性的首字符取到,
// 如果首字母是x的话,处理响应的命名空间的形式,上面15 16 行
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur);
// else if 都是设置命名空间的情况 star
} else if (key.charCodeAt(3) === colonChar) {
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur);
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur);
// else if 都是设置命名空间的情况 end
// 如果处理命名空间都不满足,else说明不需要设置命名空间
// 正常设置DOM元素的属性和其对应的值
} else {
elm.setAttribute(key, cur);
}
}
}
}
// 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
// 循环老节点中属性进行遍历循环,取出来的就是老节点的属性
for (key in oldAttrs) {
// 判断老节点中的属性是否在新节点中的属性中是否存在
if (!(key in attrs)) {
// 如果不存在直接删除就可以了,因为老节点是要被删除的,新节点中没有就用不到了
elm.removeAttribute(key);
}
}
}
// 指定两个重要的钩子函数,update和cerate
export const attributesModule = {create: updateAttrs, update: updateAttrs} as Module;
export default attributesModule;
5.14 模块调用的时机分析
源码位置:node_modules/snabbdom/src/snabbdom.ts
在这里面有两个重要的对象 cbs
和 htmlDomApi
//4. hooks就是一个数组,数组里面有一些内容,这里面都是模块中的钩子函数,hooks里面存储的就是所有模块中钩子函数的名称
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
export {h} from './h';
export {thunk} from './thunk';
// 当我们调用函数的时候,我们会传过来一个数组,
// 第二个参数有个? 代表是可有可无的参数
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
// cbs = {create:[fn1,fn2],update:[fn1]} 取值对应的是数组
// 这里面存放的就是钩子函数触发之后需要调用的钩子函数放入数组中
let i: number, j: number, cbs = ({} as ModuleHooks);
//1. 如果不传入第二个参数,那么这里 domApi 就是 undefined 条件成立,拿到的结果是 htmlDomApi,ctrl进入htmlDomApi
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
//3. 这里是对hooks进行遍历 ctrl找到hooks
//4. 最终钩子函数名称就存入了 cbs 中
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []; //7. cbs.create=[],cbs.update=[]
for (j = 0; j < modules.length; ++j) {
//8. 每循环一次将模块中的钩子函数拿出来放入 hook 常量里面暂存
const hook = modules[j][hooks[i]];
//9. 这里判断 hooks 不等 undefined 的话,直接取出来放入cbd对应的钩子函数名称里面的数组中
if (hook !== undefined) {
//5. 这里说明 钩子函数的名称对应的是数组,值就是为钩子函数添加的一个具体的函数,比如 fn1 fn2
(cbs[hooks[i]] as Array<any>).push(hook);
//10. cbs最后的格式就是 cbs={create:[fn1,fn2],update:[fn]},存放完之后在patch()函数里面调用
}
}
}
源码位置:node_modules/snabbdom/src/htmldomapi.ts
第291行
// patch函数
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
// 这里对cbs数组进行遍历,取出里面的钩子函数进行执行就可以了
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
....
}
源码位置:node_modules/snabbdom/src/htmldomapi.ts
第79行
//2. 这里面都是对DOM操作的一些方法,虚拟DOM最终都是通过htmlDomApi中提供的方法,将虚拟DOM转成了真实的DOM
export const htmlDomApi = {
createElement,
createElementNS,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment,
} as DOMAPI;
export default htmlDomApi;
Vue虚拟DOM Over made by Q7Long 2022-10-28 20:26
转载自:https://juejin.cn/post/7159551634194825253