🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
*本文的snabbdom库是基于:"snabbdom": "^0.7.4"* *本文的对应git仓库地址分支:[git仓库链接](https://github.com/fujiazhang/vue/tree/snabbdom-use) 建议down下来,因为给源码挨个写了中文注释😁* ***** 这篇文章virtual dom 库 snabbdom的阅读笔记,Virtual DOM 也越来越火,snabbdom 是其中一种实现,他号称是最快的,哈哈哈哈,因为最近在看vue源码,而vue 2.0版本的 Virtual DOM 部分也是基于snabbdom,而且关键的是他的代码不多,很适合源码的学习,而且还是用的typescript的,ts阅读起来就挺舒服。 [TOC] # snabbdom使用 ## snabbdom基本api ![](https://img.kancloud.cn/82/71/8271dd1eb3d9421ecfba30070d409a33_841x738.png) ## snabbdom的带样式插件、事件插件的使用 ![](https://img.kancloud.cn/d7/62/d762780cb30f2b2da34265de047118e4_912x503.png) # 源码解析 ## snabbdom的核心 * h 函数创建vnode(js对象)用来描述真实dom * init函数,挂载模块,创建patch()函数 * patch()函数 对比新旧vnode节点 * 更新真实dom 官方源码地址:https://github.com/snabbdom/snabbdom (注意本文0.7.4版本),down下来后,目录如下: ![](https://img.kancloud.cn/4d/28/4d28b7cda7336339fea60d813d02f5a3_418x609.png) 我会用care src目录和examples目录即可。 ## h( )函数 关于h( ) ,我们知道是用来创建vnode的(对象)。 其实我假设你没看过vue源码,那么你也应该眼熟h()函数,vue在初始化时会用到,当然,在你没有了解源码前,应该不会关注vue初始化为什么要传一个这样的参数: ![](https://img.kancloud.cn/a9/25/a92527781d72cfafee4fb03ab70c2237_455x178.png) > 在了解h函数前需要先知道重载,js是没有重载的,ts通过传入参数不同,代码调整参数,模拟实现了重载。 我们来看看h.ts的关键代码:![](https://img.kancloud.cn/f2/74/f27413af7b888118b0fa65d612ef4cd8_1484x801.png) 关键的都写在注释中,可以发现h()函数就是经过一系列重载参数判断,然后对`children`属性做处理,将可能不是`vnode`的项转成`vnode`,然后调用vnode函数,返回vnode. ## vnode函数 在上面h函数中,我们调用了一个vnode函数 返回一个vnode,我们看看他是怎么实现的。 先把调用的参数放一边好看 ![](https://img.kancloud.cn/ab/5d/ab5d7e81f2c58e966d603d38cb0dad82_557x344.png) 带注释的源码如下: ![](https://img.kancloud.cn/38/fb/38fba853802717870e94227bae67f79b_997x607.png) ## init() 代码结构如下: ![](https://img.kancloud.cn/4a/19/4a1965e165849c8d80134af74eefa52e_1067x883.png) 这里调用init函数,返回patch函数,使用的是高阶函数,形成闭包,好处是patch数据可以访问init传入参数domapi,modules,这样不用每次都去传入,好处是非常明显和巧妙的~ 。通过参数可以知道,这里有接受一个 `modules` 数组,另外有一个可选的参数 `domApi`,如果没传递会使用浏览器中和 `dom` 相关的 api,这样的设计也很有好处,它可以让用户自定义平台相关的 api,这里会对 `module` 中的 `hook` 进行收集,保存到 `cbs` 中。然后定义了各种函数,这里可以先不管,接着就是返回一个 `patch` 函数了,这里也先不分析它的具体逻辑。这样 `init` 就结束了。这里先按住不表,后面会展开讲。 ## patch patch函数是init函数返回的,patch的作用就是对比新旧节点,更新真实dom,patch内部整体过程如下: * ptach(oldVnode,newVnode) * 打补丁,把新节点中华变得内容渲染到真实dom,最后返回新节点作为下一次patch的旧节点 * 对比新旧vnode是否相同节点(key 、sel对比) * 如果不是相同 节点,删除之前节点,重新渲染 * 如果是相同的,再判断新的VNode是否由text, 如果有并且和oldVnode的text不同,直接更新文本内容 * 如果新的Vnode有childern, 判断子节点是否变化,判断子节点的过程即使用diff算法 * diff过程只进行同级比较 ![](https://img.kancloud.cn/3e/f6/3ef65eae1aa434186067b247a141e527_1337x753.png) 首先会调用 `module` 的 `pre hook`,你可能会有疑惑,为什么没有调用来自各个元素的 `pre hook`,这是因为元素上不支持 `pre hook`,也有一些 `hook` 不支持在 `module` 中,具体可以查看[这里的文档](https://github.com/snabbdom/snabbdom#overview)。然后会判断传入的第一个参数是否为 `vnode` 类型,如果不是,会调用 `emptyNodeAt` 然后将其转换成一个 `vnode`,`emptyNodeAt` 的具体实现也很简单,注意这里只是保留了 `class` 和 `style`,这个和 `toVnode` 的实现有些区别,因为这里并不需要保存很多信息,比如 `prop` `attribute` 等。接着调用 `sameVnode` 来判断是否为相同的 `vnode` 节点,具体实现也很简单,这里只是判断了 `key` 和 `sel` 是否相同。如果相同,调用 `patchVnode`,如果不相同,会调用 `createElm` 来创建一个新的 `dom` 节点,然后如果存在父节点,便将其插入到 dom 上,然后移除旧的 `dom` 节点来完成更新。最后调用元素上的 `insert hook` 和 `module` 上的 `post hook`。 这里的重点是 `patchVnode` 和 `createElm` 函数,我们先看 `createElm` 函数,看看是如何来创建 `dom` 节点的。 ### createElm 函数 ![](https://img.kancloud.cn/2a/6b/2a6b51df98046a6c42bf084a59c0533d_1299x930.png) 这里的逻辑也很清晰,首先会调用元素的 `init hook`,接着这里会存在三种情况: * 如果当前元素是注释节点,会调用 `createComment` 来创建一个注释节点,然后挂载到 `vnode.elm` * 如果不存在选择器,只是单纯的文本,调用 `createTextNode` 来创建文本,然后挂载到 `vnode.elm` * 如果存在选择器,会对这个选择器做解析,得到 `tag`、`id` 和 `class`,然后调用 `createElement` 或 `createElementNS` 来生成节点,并挂载到 `vnode.elm`。接着调用 `module` 上的 `create hook`,如果存在 `children`,遍历所有子节点并递归调用 `createElm` 创建 `dom`,通过 `appendChild` 挂载到当前的 `elm` 上,不存在 `children` 但存在 `text`,便使用 `createTextNode` 来创建文本。最后调用调用元素上的 `create hook` 和保存存在 `insert hook` 的 `vnode`,因为 `insert hook` 需要等 `dom` 真正挂载到 `document` 上才会调用,这里用个数组来保存可以避免真正需要调用时需要对 `vnode` 树做遍历。 接着我们来看看 `snabbdom` 是如何做 `vnode` 的 `diff` 的,这部分是 `Virtual DOM` 的核心。 ### patchVnode 函数 这个函数做的事情是对传入的两个`vnode`做`diff`,如果存在更新,将其反馈到`dom`上。 patchVnode过程: ![](https://img.kancloud.cn/a4/83/a48373612d1fa227637d0be953a235b6_1077x658.png) ![](https://img.kancloud.cn/37/e4/37e495452ae6b5acb84c5ca3e346c5d1_1344x788.png) ### updateChildren函数 diff对比逻辑如下(这部分来源于网络,迅雷前端): ![](https://img.kancloud.cn/a7/49/a749aa005e0ab28dc9a105d0efa3b350_1343x851.png) ![](https://img.kancloud.cn/ac/60/ac60d836dec88a12040d03137cd72e64_1369x882.png) ![](https://img.kancloud.cn/cc/81/cc8147b0c7ba5b7d7576ed7ac2ae18db_1341x909.png) 整个过程简单来说,对两个数组进行对比,找到相同的部分进行复用,并更新。整个逻辑可能看起来有点懵,可以结合下面这个例子理解下: 1. 假设旧节点顺序为\[A, B, C, D\],新节点为\[B, A, C, D, E\] ![](https://img.kancloud.cn/5c/f0/5cf0984a8c0c10b87a807f03b9ee1846_1280x747.png) 2.第一轮比较:开始结束节点两两并不相等,于是看 newStartVnode 在旧节点中是否存在,最后找到了在第二个位置,调用 patchVnode 进行更新,将 oldCh\[1\] 至空,将 dom 插入到 oldStartVnode 前面,newStartIdx 向中间移动,状态更新如下 ![](https://img.kancloud.cn/f3/8d/f38de27a15a7e577d80d9fbfaf89cce0_1280x770.png) 3. 第二轮比较:oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下 ![](https://img.kancloud.cn/fb/f9/fbf94b03e979611a3c9960b5108cef44_1280x779.png) 4. 第三轮比较:oldStartVnode 为空,oldStartIdx 向中间移动,进入下轮比较,状态更新如下 ![](https://img.kancloud.cn/5a/b6/5ab66cc8a2c6f5d062dd0298411b2ed1_1216x786.png) 5. 第四轮比较:oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下 ![](https://img.kancloud.cn/91/a4/91a436b03a92fdd6996edc010b2dd053_1276x864.png) 6. oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下 ![](https://img.kancloud.cn/70/18/7018e8c23ea47b3405813201ceb2bfe5_1232x942.png) 7. oldStartIdx 已经大于 oldEndIdx,循环结束,由于是旧节点先结束循环而且还有没处理的新节点,调用 addVnodes 处理剩下的新节点 ![](https://img.kancloud.cn/7a/2a/7a2a26135f03ad294919ae9f33cc9189_1320x864.png)(图片引用自互联网)