学习snabbdom有助于拓宽我们对虚拟DOM的认知。当下最流行之一的Vue,它的虚拟DOM技术就源于snabbdom。
作者最近最近学习了snabbdom,并手写snabbdomd的核心部分,GitHub地址:study_snabbdom 不到300行
有兴趣可以拉下来看看,有详细注解和学习说明,将持续更新配图。
├─study_snabbdom
├─app
│ └─index.html # 静态的html模板
├─src # 项目源代码
│ ├─snabbdom # 手写源码的文件目录
│ │ ├─createElement.js # 虚拟dom对象定义
│ │ ├─h.js # 将表达式转换成以对象简单定义的dom
│ │ ├─htmldomapi.js # 操作DOM方法的简单封装
│ │ ├─patch.js # 开启diff的入口
│ │ ├─updateChildren.js # diff算法最核心的部分。
│ │ ├─vnode.js # 返回虚拟节点的模块
│ ├─index.js # 入口文件
└─package.json #
└─RMEADME.md #
└─webpack.config.js # webpack 配置
不过今天要讨论的不是如何手写snabbdom库的核心内容。而是想说说在它官方GitHub的文档上唯一的一个常见错误,“为什么不允许在patch时重用虚拟节点
”
由于Vue的虚拟DOM和diff算法核心和snabbdom几乎是一样的,所以讨论它就像是在讨论Vue。
一、为了说明什么这个问题,需要对虚拟DOM和diff算法有一个基本的了解。
(1)真实的DOM以li标签为例,它至少有200个属性
我知道就算你无所谓什么是virtual DOM和diff算法,也不想在调试栏中点开这个玩意儿。
(2)Virtual DOM最少只需要5个属性
virtual DOM就是一个用JS对象来表示一个真实DOM最重要的特征。
{
children: undefined // 一个数组 ,存放子虚拟DOM
data: {key: "A"} // 一个对象 ,用于存放一些属性和key,
elm: li // ⭐一个引用 , 对应自身的真实DOM引用(如果在DOM树上的话)?
key: "A" // 一个字符串 , 用于标识自己
sel: "li" // 一个字符串 , 用于表明自身的 tagName
text: "A2" // 一个字符串 , 通常是本文属性值
}
@以上就是构成一个虚拟DOM的最小可能。
(3)虚拟DOM中的diff算法是通过比对两个虚拟DOM,找出它们的差异再进行较少次数DOM操作的一个算法。
一般遵循以下原则
1. 同层比较 。 比如。
2. 同节点比较 。 比如。
二、回到正题,为什么不允许重用虚拟节点呢?
1、我们对patch()做一个实验。
(1)对html中那个容器进行patch。
开场一首苏轼的《水调歌头》window.onload完成调一个 patch()
window.onload = function (){
patch(container, newVnode1)
}
第1次patch
把《水调歌头》修改成了A B C D E。
(2)接下点击按钮2,对刚刚newVnode1进行patch
window.onload = function (){
btn.onclick = patch(newVnode1,newVnode2)
}
第2次patch
把 A B C D E 修改成了B2 A2 C2 E2 D2。
(3)接下点击按钮1,对刚刚newVnode2进行patch,并传入newVnode1作为第2参数
第3次patch
把 B2 A2 C2 E2 D2 修改成了A C D E。
这时候发现少了一个
B 。??
2、起初我以为这是我的代码逻辑有问题。但是使用源码也是一样
通过在代码中输出信息,发现无论是源码还是我手写的实现,输出的位置时间和内容都是一致的。
3、答案就在于 diff核心算法,updateChildren函数中。
4中常规命中方式失败之后,会通过查找的方式,来搜寻旧节点中是否有新节点,如果正好有,而且是同 key 同 sel
就会继续对这两个节点调用patch(),调用完成后会为这个旧子节点的elm打上 undefined
的标识 。
因此,当重用虚拟节点去patch的时候,就可能出现某一个虚拟节点的elm
属性已经是undefined
了。
重点来了,由于网络上diff算法都用文字说明,看着太费解了作者自己做了动图,并且在图里找到了答案。
本图是仓库的配图,虽然和例子不一样,但是足以说明。
(这里把旧子节点列表同时当成真实DOM来演示,可以把它们看成是一致的。)
看看第2次patch和第3次patch后,虚拟节点的情况。
可以看到第2次patch
之后,newVnode1虚拟节点列表中下标为1
的元素已经是undefined
了,第3次patch
之后,newVnode2中下标 1
2
也是undefined
了。因此当你再使用这个虚拟节点调用patch的时候,肯定会发生意外。
一些心得
DOM中的diff是算法为了减少DOM操作的次数。它在比较的时候,是对虚拟DOM进行比较,而将真实DOM的引用保存在虚拟DOM的elm属性中,并在发生差异时才操作DOM。
官方的解释
常见错误
Uncaught NotFoundError: Failed to execute 'insertBefore' on 'Node':
The node before which the new node is to be inserted is not a child of this node.
var vnode2 = h('div', [
h('div', {}, ['One']),
h('div', {}, [{ ...sharedNode }]),
h('div', {}, ['Three']),
])
var sharedNode = () => h('div', {}, 'Selected')
var vnode1 = h('div', [
h('div', {}, ['One']),
h('div', {}, ['Two']),
h('div', {}, [sharedNode()]),
])
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!