上一篇《虚拟 DOM 和 diff 算法 -01》我们手写实现了 h 函数,本篇着重介绍 diff 算法。
diff 算法的特点
- 如果是往数组的最后面添加节点,那么前面的节点不会改动
比如有如下新(vnode2)旧(vnode1)两个节点,那么执行 patch(vnode1, vnode2)
会发现仅仅浏览器只是追加了一个节点 <div>东风破</div>
, 不会改变前两个。
const vnode1 = h("div", {}, [
h("div", "七里香"),
h("div", "东风破")
])
const vnode2 = h("div", {}, [
h("div", "七里香"),
h("div", "东风破"),
h("div", "兰亭序")
])
可以通过在浏览器调试工具里直接将“七里香”改成“七里不香”,然后通过点击按钮执行 patch(vnode1, vnode2) 会发现“七里不香”依旧没变。
- key 很重要,key 作为节点的标识,告诉 diff 算法在更改前后节点是否为同一个
如果是往数组的开头添加节点,则所有的节点都会被改动,想要做到最小化更新,需要给每个节点添加 key 属性,这样 <div>七里香</div>
和 <div>东风破</div>
两个节点就不会被改动了
const vnode1 = h("div", {}, [
h("div", { key: 1 }, "七里香"),
h("div", { key: 2 }, "东风破")
])
const vnode2 = h("div", {}, [
h("div", { key: 3 }, "兰亭序"),
h("div", { key: 1 }, "七里香"),
h("div", { key: 2 }, "东风破")
])
- 只有是同一个虚拟节点,才进行精细化比较,否则直接删除旧节点,插入新节点
判断两个节点是否为同一个,是根据比较选择器,也就是 sel 的值和 key 的值是否都相同,都相等则判断为同一个虚拟节点
- 只进行同层比较
新旧节点的层级要相同,比如下面的例子里新节点比旧节点多了层 div,则不会进行精细化比较,直接删除旧节点插入新节点
const vnode2 = h("div", {}, [
h('div', [
h("div", { key: 3 }, "兰亭序"),
h("div", { key: 1 }, "七里香"),
h("div", { key: 2 }, "东风破")
])
])
手写 patch 函数
diff 算法是通过 patch 函数实现的,在开始手写之前,我们先来理清 patch 函数做了什么
函数功能分析
可以通过之前下载到 node_modules 里的 snabbdom 查看源码,patch 函数被定义在了 snabbdom 下的 src 目录下的 init.ts 里
// init.ts
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
// ...忽略部分代码
// 通过 isVnode 函数判断旧节点是否为虚拟节点
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode); // 不是则通过 emptyNodeAt 包装为虚拟节点
}
// 通过 sameVnode 函数判断新旧节点是否为为同一个节点
if (sameVnode(oldVnode, vnode)) {
// 相同...
} else {
// 不同...
}
// ...忽略部分代码
};
根据源码得到如下流程图
手写第一次上树
新建 patch.js 文件,引入 vnode 函数用于将非虚拟节点的 oldVnode 包装为虚拟节点
// patch.js
import vnode from './vnode.js'
import creatElement from './creatElement.js'
export default (oldVnode, newVnode) => {
// 判断 oldVnode 是否为虚拟节点
if (oldVnode.sel === undefined) {
// oldVnode 不是虚拟节点,则包装成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase, {}, [], undefined, oldVnode)
}
// 判断 oldVnode, newVnode 是否为同一节点
if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
// 同一节点
} else {
// 不是同一节点
const domNode = creatElement(newVnode)
// 将新节点上树
oldVnode.elm.parentNode?.insertBefore(domNode, oldVnode.elm)
// 删除旧节点
oldVnode.elm.parentNode?.removeChild(oldVnode.elm)
}
}
新建 creatElement.js 并在 patch.js 引入 creatElement 函数,用于创建新节点,并将对应的虚拟节点的 elm 属性赋值为创建出的新节点
/**
* creatElement.js
* 将 vnode 创建为真正的 DOM 节点(但是没上树的孤儿节点)
*/
export default function createElement (vnode) {
const domNode = document.createElement(vnode.sel)
vnode.elm = domNode
// 判断 vnode 有子节点(children)还是文本(text)
if (vnode.children !== undefined && vnode.children.length && vnode.text === undefined) {
// 有子节点
vnode.children.forEach(item => {
// 调用 createElement 意味着创建出了 DOM,并且将改虚拟节点的 elm 属性指向了这个 DOM,
// 但这个 DOM 是个孤儿节点,还没上树
const childNode = createElement(item)
item.elm = childNode
domNode.appendChild(childNode)
})
} else {
// 内部为文本
domNode.innerText = vnode.text
vnode.elm = domNode
}
return domNode
}
至此,我们已经完成了上面 patch 函数流程图中除了“精细化比较”之外的内容。接下来就开始着手当 oldVnode 和 newVnode 是同一节点的情况下的精细化比较的内容,这部分将有较多的图示,写在本篇难免会导致页面过长,我将在下篇继续分享~
One More Thing
本文有用到一些插入节点的方法,现在就此做一个扩展总结
- innerHTML(属性):获取标签内部的HTML内容
- outerHTML(属性):获取包括目标标签在内,以及内部HTML的内容
- appendChild(函数):向目标标签末尾添加子节点,返回参数节点
- insertBefore(函数):向目标节点的第二个参数位置添加第一个参数为子节点,返回第一个参数
- insertAdjacentHTML(函数):向目标节点的指定位置添加节点
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!