站在巨人的肩膀上看vue,来自霍春阳的vue设计与实现。作者以问题的形式一步步解读vue3底层的实现。思路非常的巧妙,这里会将书中的主要逻辑进行串联,也是自己读后的记录,希望通过这种形式可以和大家一起交流学习。
开篇
简单Diff算法利用虚拟节点的key属性,尽可能的复用DOM元素,通过移动DOM的方式来完成更新,从而减少不断的创建和销毁DOM元素带来的性能开销。
简单Diff算法主要思想是寻找旧节点最大索引值,因为旧节点索引值都是呈递增趋势。如果当前新节点的索引比最大索引值小则需要移动,否则更新最大索引。
当遍历新节点的时候,如果在旧节点中没有找到,表明这个新节点是需要新增的,当新节点更新完毕后,后需要单独遍历旧节点,如果在新节点中没有出现则表明这个旧节点是需要删除的。
这就是简单的Diff算法过程。
9.1、减少DOM操作的性能开销
diff算法:当新旧vnode的字节点都是同一组节点时,为了以最小的性能开销完成更新操作,需要比较两组子节点,找到可复用的节点,减少操作DOM带来的性能消耗。
情况1:节点都相同只是文本节点的内容不同
只需要遍历其中oldChildren、newChildren一个,然后调用patch函数进行更新,patch函数在执行更新是发现新旧字节点只有文本内容不同,因此只会更新其文本节点的内容
function patchChildren(newChildren, oldChildren) {
for (let i = 0; i < oldChildren.length; i++) {
patch(newChildren[i], oldChildren[i])
}
}
情况2:新旧节点的数量不相同
在遍历的时候不应该总是遍历旧的节点或新的节点,而是应该遍历其中长度最短的那一组,新节点数量比旧节点数量多需要mount挂在,反之需要unmount卸载。这样无论两组字节点的数量关系如何,渲染器都能正确的挂在或卸载他们
function patchChildren(newChildren, oldChildren) {
let newLen = newChildren.length
let oldLen = oldChildren.length
const commonLength = Math.min(newLen, oldLen)
for (let i = 0; i < commonLength; i++) {
patch(newChildren[i], oldChildren[i])
}
if (newLen > oldLen) {
for (let i = commonLength; i < newLen; i++) {
patch(null, newChildren[i])
}
} else if (oldLen < newLen) {
for (let i = commonLength; i < oldLen; i++) {
unmount(oldChildren[i])
}
}
}
9.2、DOM 复用与 Key 的作用
情况3:新旧两个节点内容完全相同,但位置不同
这种情况通过DOM的移动来完成字节点的更新,如何判断新的节点是否出现在旧节点中?
这时,需要引入额外的key来作为vnode的标识,只要两个虚拟节点的type属性值和key都相同,那么认为他们是相同的,即可以进行DOM的复用
[
{
type: 'p', children: '1', key: 1
}
]
function patchChildren(newChildren, oldChildren) {
let newLen = newChildren.length
let oldLen = oldChildren.length
for (let i = 0; i < newLen; i++) {
const newVnode = newChildren[i]
for (let j = 0; j < oldLen; j++) {
const oldVnode = oldChildren[j]
if (newVnode.key === oldVnode.key) {
patch(oldVnode, newVnode)
break
}
}
}
}
9.3、找到需要移动的元素
情况4:如何判断一个节点是否需要移动
编译找到旧节点的索引值,索引值应该是一个递增的顺序,比如0,1,2,当新节点遍历寻找相同key的时候,发现索引值递增顺序被打破了,这表明改节点是需要移动的。
在旧节点中寻找具有相同节点的过程中,遇到最大索引值,如果在后续寻找的过程中,存在索引值比当前遇到的最大索引值还要小的节点,则意味着该节点需要移动
function patchChildren(newChildren, oldChildren) {
let newLen = newChildren.length
let oldLen = oldChildren.length
let lastIndex = 0 // 存储过程中遇到的最大索引值
for (let i = 0; i < newLen; i++) {
const newVnode = newChildren[i]
for (let j = 0; j < oldLen; j++) {
const oldVnode = oldChildren[j]
if (newVnode.key === oldVnode.key) {
patch(oldVnode, newVnode)
if (j < lastIndex) {
// 当前找到的节点在就节点中的索引值小于lastIndex
// 需要移动
} else {
lastIndex = j
}
break
}
}
}
}
9.4、如何移动元素
情况5:如何移动元素
移动元素指的是移动一个虚拟节点对应的真实DOM节点,需要移动真实的DOM节点,需要取得对它的引用。
- 如果 小于lastIndex,则说明当前的节点所对应的节点需要移动
- 获取当前节点的前一个虚拟节点,newChildren[i - 1]
- 如果上面步骤不存在,则说明是第一个节点,不需要移动
- 如果存在,获取但钱节点的下一个兄弟节点作为锚点,利用 insertBefore 将其插入到锚点的前面
function patchChildren(newChildren, oldChildren) {
let newLen = newChildren.length
let oldLen = oldChildren.length
let lastIndex = 0 // 存储过程中遇到的最大索引值
for (let i = 0; i < newLen; i++) {
const newVnode = newChildren[i]
for (let j = 0; j < oldLen; j++) {
const oldVnode = oldChildren[j]
if (newVnode.key === oldVnode.key) {
patch(oldVnode, newVnode)
if (j < lastIndex) {
// 获取newVnode的前一个节点
const prevVNode = newChildren[i -1]
if (prevVNode) {
// 获取prevnode 对应的真实的dom的下一个兄弟子节点,将其作为锚点
const anchor = prevVNode.el.nextSibling
// 对应的真实dom插入到锚点的前面
insert(newVnode.el, anchor)
}
} else {
lastIndex = j
}
break
}
}
}
}
9.5、添加元素
情况6:当新节点元素比旧节点元素多时如何添加元素
遍历新节点将新增的元素挂载在上一个节点之下,定义一个find的变量记录当前节点是否在旧节点中能否找到,如何找到了可复用的节点,将变量find设为true。如果没有找到说明是需要新增的节点,这时寻找当前节点的上一个节点的兄弟节点作为锚点,如果没有找到说明是第一个元素,相反将节点插入到锚点之前,最后挂载节点。
function patchChildren(newChildren, oldChildren) {
let newLen = newChildren.length
let oldLen = oldChildren.length
let lastIndex = 0 // 存储过程中遇到的最大索引值
for (let i = 0; i < newLen; i++) {
const newVnode = newChildren[i]
let j = 0
let find = false
for (j; j < oldLen; j++) {
const oldVnode = oldChildren[j]
if (newVnode.key === oldVnode.key) {
find = true
patch(oldVnode, newVnode)
if (j < lastIndex) {
// 获取newVnode的前一个节点
const prevVNode = newChildren[i -1]
if (prevVNode) {
// 获取prevnode 对应的真实的dom的下一个兄弟子节点,将其作为锚点
const anchor = prevVNode.el.nextSibling
// 对应的真实dom插入到锚点的前面
insert(newVnode.el, anchor)
}
} else {
lastIndex = j
}
break
}
if (!find) {
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
anchor = prevVNode.el.nextSibling
} else {
anchor = container.firstChild
}
// 挂载 newVnode
patch(null, newVnode, container, anchor)
}
}
}
}
9.6、移除不存在的元素
情况7:当新节点元素比旧节点少,如何移除
当新节点基本的更新结束之后,需要遍历旧的一组节点,然后去和新的一组节点中寻找具有相同key值的点,如果找不到,则说明应该删除该节点
....省略
for (let i = 0; i < oldLen; i++) {
const oldVNode = oldChildren[i]
const has = newChildren.find(vnode => vnode.key === oldVNode.key)
if (!has) {
// 卸载该节点
unmount(oldVNode)
}
}