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 才会删除元素。

相关推荐
Cyclo-1 小时前
PDFJS 在React中的引入 使用组件打开文件流PDF
前端·react.js·pdf
椒盐螺丝钉4 小时前
Vue Router应用:组件跳转
前端·javascript·vue.js
顾安r4 小时前
11.20 开源APP
服务器·前端·javascript·python·css3
敲敲了个代码4 小时前
CSS 像素≠物理像素:0.5px 效果的核心密码是什么?
前端·javascript·css·学习·面试
少云清4 小时前
【软件测试】5_基础知识 _CSS
前端·css·tensorflow
倔强青铜三5 小时前
AI编程革命:React + shadcn/ui 将终结前端框架之战
前端·人工智能·ai编程
天外飞雨道沧桑5 小时前
前端开发 Cursor MCP 提效工具配置
前端·vscode·ai编程·开发工具·cursor
朱哈哈O_o5 小时前
前端通用包的作用——jquery篇
前端
葡萄城技术团队5 小时前
纯前端驱动:在线 Excel 工具的技术革新与实践方案
前端·excel
芳草萋萋鹦鹉洲哦5 小时前
【vue】调用同页面其他组件方法几种新思路
前端·javascript·vue.js