Snabbdom 源码 - patchVnode调试

调试不带 key 的列表渲染

js 复制代码
let patch = init([])

let vnode = h('ul', [ h('li', '首页'), h('li', '视频'), h('li', '微博') ])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

vnode = h('ul', [ h('li', '首页'), h('li', '微博'), h('li', '视频') ])
patch(oldVnode, vnode)

patch(app, vnode)

进入 patch 方法,由于 app 不是 vnode,所以要将这个 dom 元素转换成 vnode,即 oldVnode 为:

js 复制代码
{ "sel": "div#app", "data": {},"children": [],"elm": {} }

传入的 vnode 为:

js 复制代码
{ "sel": "ul", "data": {}, 
  "children": [
        { "sel": "li", "data": {}, "text": "首页" },
        { "sel": "li", "data": {}, "text": "视频" },
        { "sel": "li", "data": {}, "text": "微博" }
    ]
}

通过 sameVnode 根据 key 和 sel 判断 vnode 和 oldVnode 是否是相同节点,这里 oldVnode 和 vnode 的 key 都是 undefined,但是他们的 sel 不同,所以不是相同节点:

  1. 找到 oldVnode.elm 的父节点 parent
  2. 调用 createElm 生成 vnode 对应的 dom 元素,并设置为 vnode.elm
  3. 将 vnode.elm 插入到 parent 上
  4. 移除 oldVnode.elm

patch(oldVnode, vnode)

传入的 oldvnode 是:

js 复制代码
{
    "sel": "ul", "data": {},
    "children": [
        { "sel": "li", "data": {}, "text": "首页", "elm": {} },
        { "sel": "li", "data": {}, "text": "视频", "elm": {} },
        { "sel": "li", "data": {}, "text": "微博", "elm": {} }
    ],
    "elm": {<ul><li>首页</li><li>视频</li><li>微博</li></ul>...}
}

传入的 vnode 为:

js 复制代码
{
    "sel": "ul", "data": {},
    "children": [
        { "sel": "li", "data": {}, "text": "首页" },
        { "sel": "li", "data": {}, "text": "微博" },
        { "sel": "li", "data": {}, "text": "视频" }
    ]
}

oldVnode 和 vnode 会被认为是 sameVnode,会调用 patchVnode 进行更新和比较。

patchVnode

  1. 将 oldVnode.elm 赋值给 vnode.elm 和 elm
  2. 获取 oldVnode 的子元素 oldCh 和 vnode 的子元素 ch
  3. oldCh 和 ch 不相等,所以会执行 updateChildren(elm, oldCh, ch,...)

updateChildren

进入 updateChildren 后就会使用 diff 算法进行比较,遍历 oldCh 和 ch 直到其中一个数组循环结束。

  1. oldCh 中的第一个 li 和 ch 中的第一个 li 是 sameVnode
js 复制代码
{ "sel": "li", "data": {}, "text": "首页", "elm": {...} }

再次调用 patchVnode 比较更新这两个 li 元素,但由于这两个 li 的 text 都是 '首页',所以不会进行 DOM 操作。

  1. 将新旧子节点的索引都向后++,使得开始节点都指向第 2 个 li 节点
  2. 开始比较第 2 个节点,分别是:
js 复制代码
{ "sel": "li", "data": {}, "text": "视频", elm": {...} }  // old
{ "sel": "li", "data": {}, "text": "微博" }   // new

根据 key 和 sel 判断,他们依旧是 sameVnode,所以继续调用 patchVnode 进行比较更新。此时,新节点的 text 和旧节点的 text 不同,所以要将新节点的 text 设置到 elm 上修改 DOM,界面在此时发生改变。

  1. 将新旧子节点的索引都向后++,使得开始节点都指向第 3 个 li 节点
  2. 第 3 个节点的比较过程和第 2 个节点一样,也是更新 elm 的 text

调试带 key 的列表渲染

js 复制代码
let patch = init([])

let vnode = h('ul', [h('li', { key: 'a' }, '首页'),h('li', { key: 'b' }, '视频'),h('li', { key: 'c' }, '微博')])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

vnode = h('ul', [h('li', { key: 'a' }, '首页'),h('li', { key: 'c' }, '微博'),h('li', { key: 'b' }, '视频')])
patch(oldVnode, vnode)

开始的执行过程和上个案例一致,直到在 updateChildren 中比较第 2 个元素开始,因为设置了 key 值使 DOM 的更新有了变化。

  1. 开始比较第 2 个节点,分别是:
js 复制代码
{ "key": "b", "sel": "li", "data": {}, "text": "视频", elm": {...} }  // oldStartVnode
{ "key": "c", "sel": "li", "data": {}, "text": "微博" }   // newStartVnode

由于 key 不相同,所以他们不是 sameVnode 了,那么就要进入下一个分支。 2. 比较新旧子节点的结束节点,分别是:

js 复制代码
{ "key": "c", "sel": "li", "text": "微博", "elm": {...} }  // oldEndVnode
{ "key": "b", "sel": "li", "data": {}, "text": "视频" }   // newEndVnode

也不是 sameVnode 了,继续进入下一个分支。 3. 比较旧子节点的开始节点和新子节点的结束节点,分别是:

js 复制代码
{ "key": "b", "sel": "li", "data": {}, "text": "视频", elm": {...} }  // oldStartVnode
{ "key": "b", "sel": "li", "data": {}, "text": "视频" }   // newEndVnode

他们是 sameVnode,可以调用 patchVnode 进行比较更新。在 patchVnode 中,由于新旧节点的 text 都是 '视频',所以是无需进行 DOM 操作的。但是调用 patchVnode 之后,我们需要执行:

js 复制代码
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))

将旧子节点的开始节点对应的 DOM 元素移动到父元素的最后。

最后将旧的开始索引 ++ ,使得旧子节点的开始节点指向 key:c 的 vnode。新的结束节点 --,使得新子节点的结束节点指向 key:c 的 vnode。

  1. 比较第 3 个节点,都是 key:c 的 vnode,无需再操作 DOM

总结

是否设置 key 还是会对渲染的过程和性能造成影响的,但是什么时候设置 key,还是要根据具体情况。

相关推荐
excel6 分钟前
为什么相同卷积代码在不同层学到的特征完全不同——基于 tfjs-node 猫图像识别示例的逐层解析
前端
知识分享小能手7 分钟前
React学习教程,从入门到精通,React 使用属性(Props)创建组件语法知识点与案例详解(15)
前端·javascript·vue.js·学习·react.js·前端框架·vue
用户21411832636029 分钟前
dify案例分享-免费玩转即梦 4.0 多图生成!Dify 工作流从搭建到使用全攻略,附案例效果
前端
CodeSheep9 分钟前
稚晖君又开始摇人了,有点猛啊!
前端·后端·程序员
JarvanMo12 分钟前
Flutter Web vs Mobile:主要区别以及如何调整你的UI
前端
IT_陈寒31 分钟前
Java性能优化:从这8个关键指标开始,让你的应用提速50%
前端·人工智能·后端
天生我材必有用_吴用33 分钟前
Vue3+Node.js 实现大文件上传:断点续传、秒传、分片上传完整教程(含源码)
前端
摸鱼的春哥1 小时前
前端程序员最讨厌的10件事
前端·javascript·后端
牧羊狼的狼5 小时前
React 中的 HOC 和 Hooks
前端·javascript·react.js·hooks·高阶组件·hoc
知识分享小能手6 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react