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

相关推荐
七灵微23 分钟前
【后端】单点登录
服务器·前端
持久的棒棒君4 小时前
npm安装electron下载太慢,导致报错
前端·electron·npm
crary,记忆6 小时前
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
前端·webpack·angular·angular.js
漂流瓶jz7 小时前
让数据"流动"起来!Node.js实现流式渲染/流式传输与背后的HTTP原理
前端·javascript·node.js
SamHou07 小时前
手把手 CSS 盒子模型——从零开始的奶奶级 Web 开发教程2
前端·css·web
我不吃饼干7 小时前
从 Vue3 源码中了解你所不知道的 never
前端·typescript
开航母的李大8 小时前
【中间件】Web服务、消息队列、缓存与微服务治理:Nginx、Kafka、Redis、Nacos 详解
前端·redis·nginx·缓存·微服务·kafka
Bruk.Liu8 小时前
《Minio 分片上传实现(基于Spring Boot)》
前端·spring boot·minio
鱼樱前端8 小时前
Vue3+d3-cloud+d3-scale+d3-scale-chromatic实现词云组件
前端·javascript·vue.js
zhangxingchao8 小时前
Flutter入门:Flutter开发必备Dart基础
前端