*本文的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)(图片引用自互联网)
- 前言
- 工作中的一些记录
- 破解快手直播间的webSocket的连接
- 快手「反」反爬虫的研究记录
- HTML AND CSS
- 遇到的一些还行的css笔试题
- css常见面试题
- JavaScript 深度剖析
- ES6到ESNext新特性
- 关于http与缓存
- 关于页面性能
- 关于浏览器的重排(reflow、layout)与重绘
- 手写函数节流
- 手写promise
- 手写函数防抖
- 手写图片懒加载
- 手写jsonp
- 手写深拷贝
- 手写new
- 数据结构和算法
- 前言
- 时间复杂度
- 栈
- 队列
- 集合
- 字典
- 链表
- 树
- 图
- 堆
- 排序
- 搜索
- Webpack
- Webpack原理与实践
- Vue
- Vuejs的Virtual Dom的源码实现
- minVue
- Vuex实现原理
- 一道关于diff算法的面试题
- Vue2源码笔记:源码目录设计
- vue-router源码分析(v4.x)
- React及周边
- 深入理解redux(一步步实现一个 redux)
- React常见面试题汇总
- Taro、小程序等
- TypeScript
- CI/CD
- docker踩坑笔记
- jenkins
- 最后