Snabbdom 源码 - patch

案例调试

Snabbdom 的使用 这一篇中,安装了 parcel 打包工具,在使用 npm run dev 的时候会在 dist 目录下生成对应的 .js 文件和 .js.map 文件。.js 文件就是我们打包之后的文件,同名的 .js.map 文件是记录打包之后的文件和源码文件之间的关系,目的是方便调试。

先来看一下当前源码里的代码:

js 复制代码
const patch = init([])
let vnode = h('div#container.cls', 'Hello World')
let app = document.querySelector('#app')
patch(app, vnode)

我们在浏览器上打开控制台,查看 source 栏下的内容,可以看到打包后的文件,在 src 下的文件是我们的源码文件。

patch

我们在源码的 js 文件的 patch 方法处打一个断点,查看一下 patch 的实现。

  1. f11 进入 patch 方法,继续按 f11 单步执行
  2. 由于我们 init 中没有传入模块,所以 cbs 中没有任何钩子函数
  3. 此时 vnode 不是一个 vnode 对象(dom对象没有sel属性),而是页面上的 #app 元素
  4. 调用 emptyNodeAt 转换成 vnode 对象:
js 复制代码
{ "sel": "div#app", "data": {}, "children": [], "elm": {} }
  1. 判断新旧节点是否为相同节点(keysel 是否相同),当前新旧节点不是,新节点为:
js 复制代码
vnode为:{ "sel": "div#container.cls", "data": {}, "text": "Hello World" }
  1. createElm 后为 vnode 添加了 key 和 children 属性为 undefined,添加了 elm 属性为 真实DOM
  2. 创建新节点的 dom 并插入,插入到旧元素的后面
  3. 移除旧节点的 dom,返回 vnode

createElm

vnode 节点转换成对应的 dom 元素,并将 dom 元素存储到 vnode 节点的 elm 属性当中,在调用 insertBefore 方法的时候才将 vnode 节点的 elm 属性插入到 DOM 树中。

为了更好的调试 createElm 方法,我们修改一下测试案例:

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

let vnode = h('div#container.cls', {
    // data.hook
    hook: {
        // init: 创建 DOM 之前执行的,其中获取不到 vnode 对应的 DOM 元素
        init (vnode) { console.log(vnode.elm) },
        // create:创建 DOM 完成之后执行的,其中可以获取 vnode 对应的 DOM 元素
        create (emptyNode, vnode) { console.log(vnode.elm) }
    }
}, 'Hello World')
let app = document.querySelector('#app')
patch(app, vnode)

createElm 的流程

  • 触发用户设置的 init 钩子函数
  • 获取 vnodesel 选择器,根据选择器不同执行不同的操作
  1. sel 为 '!',创建注释节点
  2. selundefined,创建文本节点
  3. sel 不为 '!' 或 undefined,创建对应的元素节点
  • 返回新创建的 Dom 元素

创建对应的元素节点

  • 解析 sel ,获取其中包含的 tag/id/class
  • 调用 createElement 方法创建相同 tagDom 元素 elm,并添加到 vnode.elm 属性上
  • elm 添加 idcalss
  • 触发模块中的 create 钩子函数,init 方法中传入的模块
  • 如果 vnode 有子节点,则递归调用 createElm 创建子节点对应的 Dom 元素,并追加到 elm
  • 否则如果 vnodetext 则创建文本节点,并追加到 elm
  • 触发用户设置的 create 钩子函数
  • 如果用户设置了 insert 钩子函数,会将其推入 insertedVnodeQueue 队列中,等 Dom 元素插入到 Dom 树之后执行

removeVnodes

removeVnodes 接收4个参数:父元素,要删除元素对应的 vnode 数组,要删除节点的开始和结束位置,此案例中就是要删除 parent 父元素中的 oldVnode

js 复制代码
removeVnodes(parent, [oldVnode], 0, 0)
  1. 首先根据 vnode 是否有 sel 属性,没有 sel 的认为是文本节点,是文本节点直接删除
  2. 此案例中的 seldiv#wrapper,为元素节点:
  • 触发 vnodedestroy 钩子函数
  • 调用 createRmCb 函数获取删除 DOM 元素的函数 rm
  • 调用模块 cbs 中的 remove 钩子函数,传入 rm
  • 如果用户传入了 remove 钩子函数,则在 remove 钩子函数中需要手动调用 rm
  • 如果用户没有传入 remove 钩子函数,则直接调用 rm

listeners

  1. 为了防止重复删除 DOM 元素,需要给 createRmCb 传递 listeners,值为 cbs.remove.length + 1
  2. 每执行一次 rm,先执行 --listeners,直到 listeners 为 0 才正在的调用 removeChild 删除元素
js 复制代码
   function rm () {
      if (--listeners === 0) {
        ...
        api.removeChild(parent, childElm)
      }
    }

也就是当在模块的 remove 钩子函数中执行 rm 的时候都不会真正删除节点,等执行完全部模块的 remove 钩子函数之后执行的 rm 才会删除元素。

相关推荐
新中地GIS开发老师36 分钟前
Cesium 军事标绘入门:用 Cesium-Plot-JS 快速实现标绘功能
前端·javascript·arcgis·cesium·gis开发·地理信息科学
Superxpang43 分钟前
前端性能优化
前端·javascript·vue.js·性能优化
Rysxt_1 小时前
Element Plus 入门教程:从零开始构建 Vue 3 界面
前端·javascript·vue.js
隐含1 小时前
对于el-table中自定义表头中添加el-popover会弹出两个的解决方案,分别针对固定列和非固定列来隐藏最后一个浮框。
前端·javascript·vue.js
大鱼前端1 小时前
Turbopack vs Webpack vs Vite:前端构建工具三分天下,谁将胜出?
前端·webpack·turbopack
你的人类朋友1 小时前
先用js快速开发,后续引入ts是否是一个好的实践?
前端·javascript·后端
知识分享小能手1 小时前
微信小程序入门学习教程,从入门到精通,微信小程序核心 API 详解与案例(13)
前端·javascript·学习·react.js·微信小程序·小程序·vue
子兮曰2 小时前
npm workspace 深度解析:与 pnpm workspace 和 Lerna 的全面对比
前端·javascript·npm
颜酱2 小时前
用搬家公司的例子来入门webpack
前端·javascript·webpack