We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
项目地址:virtual-dom-with-diff
注意:
patch
diff
patches
length
如virtual-dom源码学习一开头所说,做前端快三年了,vue也用了2年了,我一直都不懂什么是虚拟DOM!!!
挺可笑的,这样的也能叫前端工程师?确实无地自容!不是我不学,也不是我不看,相关的文章看了都多少次了,但是基本上都是大白话+图,很少有代码,再加上以前几乎996,自己也没拿出时间去真的理解它。这样真不好,干不了多久就得被淘汰。
基于以上,我先是在掘金上找到个用代码实现虚拟DOM和diff算法的文章:让虚拟DOM和DOM-diff不再成为你的绊脚石。这篇文章很不错,他不是讲大白话,而是直接上代码!先不说 diff方法,就看虚拟DOM的实现,一目了然,真的超简单!就是把DOM节点用js描述成对象而已,以前一直不懂,其实我不懂的是:我需要把DOM节点描述成什么样的js对象???
其实我以前的难点在这里!我不清楚怎么用js去描述DOM节点。准确的说,我不知道js对象中应该放些什么东西???
首先,节点标签名肯定要有的,其次是 style 以及 class 属性,然后呢?我怎么放才能保证这种js对象还能被转回同样的DOM节点呢?
style
class
这其实需要虚拟DOM库去定义属于自己的数据结构,以及根据自己的数据结构解析成DOM树的“算法”!
所以,其实虚拟DOM数据格式大家并不是完全一致的,都是属于自己定义的一套规则算法,就比如virtual-dom库和snabbdom库,他两的virtual-dom格式肯定不一样,相应的解析算法以及diff算法肯定也有区别。
我这个问题的本质是,我该怎么去设计我的虚拟DOM数据结构?
这其实并没有标准答案的。每个人都有自己的想法,关键把它实现即可。只靠想是出不来东西的,不如直接动手,虽然不知道要设计成什么样,但是大致的结构肯定有。例如以下几点:
type: div
<!-- dom结构 --> <div id="root" class="flex" style="color: #fff;"> <span class="center" style="font-size: 18px;"></span> </div>
// 对应的js对象结构 { type: 'div', children: [ { type: 'span' } ] }
{ type: 'div', class: 'flex', style: 'color: #fff;', children: [ { type: 'span', class: 'center', style: 'font-size: 18px;', } ] }
{ type: 'div', props: { class: 'flex', style: 'color: #fff;', }, children: [ { type: 'span', props: { class: 'center', style: 'font-size: 18px;', } } ] }
先弄这种最简单的数据结构,我怎么把DOM节点转换成这种结构呢???
当然是从目标节点开始不断往其子孙后代遍历了!
既然如此,我必须拿到该节点,这时节点上的 id 属性立功了!
id
let node = document.querySelector('#id')
然后就可以递归遍历node及其子孙后代,生成我的数据结构了。我的数据结构可能会用在多个地方,那我能不能把它写成构造器方便实例化调用呢?
当然可以!配合 class 更香:
class Vdom { constructor (node) { this.type = node.nodeName this.props = {} this.children = [] if (node.style.cssText) { this.props.style = node.style.cssText } if (node.classList.length) { this.props.class = Array.from(node.classList).join(',') } Array.from(node.childNodes).forEach(child => { this.children.push(new Vdom(child)) }) } }
注意,以上代码需要了解DOM API,不然就尴尬了。尤其是 classList 和 childNodes 它们都是类数组对象!!!还有,我直接拿的是 node.nodeName ,这个其实是大写的标签名,直接拿来也OK的,不过最终数据结构中所有的type 字段对应的都是大写了:
classList
childNodes
node.nodeName
type
{ type: 'DIV', props: { class: 'flex', style: 'color: #fff;', }, children: [ { type: 'SPAN', props: { class: 'center', style: 'font-size: 18px;', } } ] }
是不是OK了?
想多了!我还有好多情况没考虑呢!例如节点是input,value放哪?文字节点怎么办?
value
这就要考验我们DOM API熟不熟悉了,我不熟悉。文字节点的 nodeType === 3,这要切记,至于value,依然放到props中不就行了,把DOM结构改一下:
nodeType === 3
props
<div id="root" class="flex" style="color: #fff;"> 姓名 <input type="text" value="无始大帝"> <span class="center" style="font-size: 18px;">仙路尽头谁为峰,一见无始道成空</span> </div>
现在我的 Vdom 也必须做出相应的变化了:
Vdom
class Vdom { constructor (node) { this.type = node.nodeName this.props = {} this.children = [] if (node.style.cssText) { this.props.style = node.style.cssText } if (node.classList.length) { this.props.class = Array.from(node.classList).join(',') } if (this.type === 'INPUT' || this.type === 'TEXTAREA') { this.props.value = node.value } Array.from(node.childNodes).forEach(child => { if (child.nodeType === 1) { this.children.push(new Vdom(child)) } if (child.nodeType === 3) { this.children.push(child) } }) } }
好了,Vdom 大概就这样吧,其实还有很多情况呢,但是我的目的不是要写个通用的虚拟DOM库,我只是为了理解它的原理才写的,不需要面面俱到的。
有了 Vdom 数据结构,如何生成dom节点呢?我得保证他们能来回切换,互通有无啊!
其实跟dom节点生成vdom类似,还是递归遍历。只不过这次对象换成了vdom结构。当然,需要用到 appendChild 方法来增加子节点。
appendChild
不过我并没有去写,毕竟原来的dom节点存在啊,只需要当有改变时去更新原来的节点即可。那么如果vdom数据改变了,如何去更新node节点呢?
一个思路是直接拿新的vdom数据去递归遍历,应用更新。好像没啥问题,只要保证新旧vdom结构一致,只改变属性确实可以,但是万一他们结构不同了,节点变了怎么办?
这里就是 diff 和 patch 大展身手的地方了。
我的第一版其实是错误的,第一版建立在新旧vdom结构一致的基础上,这根本不现实。
第二版的关键点是,我在每个patch对象中加了个 vdom 属性,缓存当前节点对应的旧vdom数据,这样才能保证节点一一对应不混乱!但是代码并没有完善。
vdom
这里关键点在于 diff 方法怎么写?
要想知道怎么写,先要明白我要什么?这个 diff 调用后应该有什么效果?我需要它返回什么给我?
很简单,我需要它把所有的节点差异给我,而且还要让我明白是哪个节点,不然我没法去更新!
例如这样的结构:
{ vdom: 旧节点, patch: { props: { class: class差异 } } }
如果是文字节点变化了,没有 props 咋办?那么结构可能是这样的:
{ vdom: 旧节点, patch: 文字 }
以上结构只针对单独一个节点的差异,但节点是嵌套的啊,父子节点都有差异怎么办呢?这很关键,这决定了我如何去设计 diff 方法!!!
这个问题在于,我如何把所有节点的差异对象平铺到一个大对象中?即它们的属性键名怎么设计才能不冲突???
这里不得不佩服 virtual-dom 库了,它vnode结构有个属性叫 count,这个 count 跟其子节点数量以及子节点的 count 有关,说白了,就是跟子节点的数量以及子孙节点中的元素节点数量有关,而且还有个 index 变量在自增。
count
index
我怎么去设计呢?
我的比较low,就维护一个 index 变量,让它不断自增,不管你是文字节点还是元素节点,只要有差异,就用当前 index 做键名,差异做键值放进大的 patches 对象中,同时让 index 自增以避免冲突。最后,我还要加个length 属性把它做成类数组对象,方便 pacth 中转为数组调用。
pacth
所以,我最终的 patches 对象应该是这种格式(这个不是本篇开头的例子,而是我代码里的例子):
{ 0: { vdom: { // 旧的根节点,省略了... }, patch: { props: {class: 'flex'} } }, 1: { vdom: { // 旧input节点,省略了... }, patch: { props: { value: '无始天帝' } } }, 2: { vdom: { // 旧span节点,省略 }, patch: { props: { class: 'flex justify-center aligin-center' } } }, 3: { vdom: '仙路尽头谁为峰,一见无始道成空' patch: '吾为天帝,当镇世间一切敌' } }
为了方便,我会把它转为数组,得到了 patchArr 对象,如何更新呢?
patchArr
还是拿旧的 vdom 对象递归遍历,去跟 patchArr 数组对比。
由于我 diff 内部采用的是 先序优先深度遍历 方法,简而言之,我是一个节点递归遍历完其所有子孙节点才会去递归遍历第二个节点,所以顺序是和递归遍历vdom一致的,这样只要比对 vdom === patch.vdom 即可确定需要更新的节点。
vdom === patch.vdom
因为顺序一致,patchArr 第一个元素肯定先被应用,接下来就是第二个,第三个...按顺序来应用,那我只要应用一个就删除一个,改变 patchArr 数组,始终保持跟新数组的第一个元素对象做比对即可。
本篇主要是一个记录,具体代码还是要看virtual-dom-with-diff。其实虚拟DOM不难,只要明白需要什么数据格式然后转换成我需要的就好了,难得是diff方法,需要动点脑子。
接下来,我想把我之前写的mvvm和虚拟DOM以及diff结合起来,那样不就是个vue吗?哈哈哈哈哈,加油!
The text was updated successfully, but these errors were encountered:
No branches or pull requests
模拟实现virtual-dom以及使用diff方法更新
项目地址:virtual-dom-with-diff
注意:
patch
对象依然是嵌套结构,并且属性键名是 层级-子节点位于父节点下标索引,这种结构在更新时,迫使我不得不去递归遍历patch
对象,这点其实很不好。diff
方法的修改,这里的diff
会将所有的差异对象平铺放到一个大的patches
对象中,这样在更新时就不需要递归了;而且,我的属性键名其实是从0开始递增的,加了length
属性,把patches
对象构造成类数组对象,更新时直接转成数组,比较方便!为什么要写这个?
如virtual-dom源码学习一开头所说,做前端快三年了,vue也用了2年了,我一直都不懂什么是虚拟DOM!!!
挺可笑的,这样的也能叫前端工程师?确实无地自容!不是我不学,也不是我不看,相关的文章看了都多少次了,但是基本上都是大白话+图,很少有代码,再加上以前几乎996,自己也没拿出时间去真的理解它。这样真不好,干不了多久就得被淘汰。
基于以上,我先是在掘金上找到个用代码实现虚拟DOM和diff算法的文章:让虚拟DOM和DOM-diff不再成为你的绊脚石。这篇文章很不错,他不是讲大白话,而是直接上代码!先不说 diff方法,就看虚拟DOM的实现,一目了然,真的超简单!就是把DOM节点用js描述成对象而已,以前一直不懂,其实我不懂的是:我需要把DOM节点描述成什么样的js对象???
我需要把DOM节点描述成什么样的js对象???
其实我以前的难点在这里!我不清楚怎么用js去描述DOM节点。准确的说,我不知道js对象中应该放些什么东西???
首先,节点标签名肯定要有的,其次是
style
以及class
属性,然后呢?我怎么放才能保证这种js对象还能被转回同样的DOM节点呢?这其实需要虚拟DOM库去定义属于自己的数据结构,以及根据自己的数据结构解析成DOM树的“算法”!
所以,其实虚拟DOM数据格式大家并不是完全一致的,都是属于自己定义的一套规则算法,就比如virtual-dom库和snabbdom库,他两的virtual-dom格式肯定不一样,相应的解析算法以及diff算法肯定也有区别。
我这个问题的本质是,我该怎么去设计我的虚拟DOM数据结构?
怎么去设计我的虚拟DOM数据结构?
这其实并没有标准答案的。每个人都有自己的想法,关键把它实现即可。只靠想是出不来东西的,不如直接动手,虽然不知道要设计成什么样,但是大致的结构肯定有。例如以下几点:
type: div
来表示名词不就行了?先弄这种最简单的数据结构,我怎么把DOM节点转换成这种结构呢???
怎么把DOM节点转换成我需要的数据结构呢???
当然是从目标节点开始不断往其子孙后代遍历了!
既然如此,我必须拿到该节点,这时节点上的
id
属性立功了!然后就可以递归遍历node及其子孙后代,生成我的数据结构了。我的数据结构可能会用在多个地方,那我能不能把它写成构造器方便实例化调用呢?
当然可以!配合
class
更香:注意,以上代码需要了解DOM API,不然就尴尬了。尤其是
classList
和childNodes
它们都是类数组对象!!!还有,我直接拿的是node.nodeName
,这个其实是大写的标签名,直接拿来也OK的,不过最终数据结构中所有的type
字段对应的都是大写了:是不是OK了?
想多了!我还有好多情况没考虑呢!例如节点是input,
value
放哪?文字节点怎么办?这就要考验我们DOM API熟不熟悉了,我不熟悉。文字节点的
nodeType === 3
,这要切记,至于value
,依然放到props
中不就行了,把DOM结构改一下:现在我的
Vdom
也必须做出相应的变化了:好了,
Vdom
大概就这样吧,其实还有很多情况呢,但是我的目的不是要写个通用的虚拟DOM库,我只是为了理解它的原理才写的,不需要面面俱到的。有了
Vdom
数据结构,如何生成dom节点呢?我得保证他们能来回切换,互通有无啊!vdom如何生存dom节点呢?
其实跟dom节点生成vdom类似,还是递归遍历。只不过这次对象换成了vdom结构。当然,需要用到
appendChild
方法来增加子节点。不过我并没有去写,毕竟原来的dom节点存在啊,只需要当有改变时去更新原来的节点即可。那么如果vdom数据改变了,如何去更新node节点呢?
vdom数据改变,如何去更新node节点呢?
一个思路是直接拿新的vdom数据去递归遍历,应用更新。好像没啥问题,只要保证新旧vdom结构一致,只改变属性确实可以,但是万一他们结构不同了,节点变了怎么办?
这里就是
diff
和patch
大展身手的地方了。我的第一版其实是错误的,第一版建立在新旧vdom结构一致的基础上,这根本不现实。
第二版的关键点是,我在每个patch对象中加了个
vdom
属性,缓存当前节点对应的旧vdom数据,这样才能保证节点一一对应不混乱!但是代码并没有完善。这里关键点在于
diff
方法怎么写?diff怎么写?
要想知道怎么写,先要明白我要什么?这个
diff
调用后应该有什么效果?我需要它返回什么给我?很简单,我需要它把所有的节点差异给我,而且还要让我明白是哪个节点,不然我没法去更新!
例如这样的结构:
如果是文字节点变化了,没有
props
咋办?那么结构可能是这样的:以上结构只针对单独一个节点的差异,但节点是嵌套的啊,父子节点都有差异怎么办呢?这很关键,这决定了我如何去设计
diff
方法!!!这个问题在于,我如何把所有节点的差异对象平铺到一个大对象中?即它们的属性键名怎么设计才能不冲突???
这里不得不佩服 virtual-dom 库了,它vnode结构有个属性叫
count
,这个count
跟其子节点数量以及子节点的count
有关,说白了,就是跟子节点的数量以及子孙节点中的元素节点数量有关,而且还有个index
变量在自增。我怎么去设计呢?
我的比较low,就维护一个
index
变量,让它不断自增,不管你是文字节点还是元素节点,只要有差异,就用当前index
做键名,差异做键值放进大的patches
对象中,同时让index
自增以避免冲突。最后,我还要加个length
属性把它做成类数组对象,方便pacth
中转为数组调用。所以,我最终的
patches
对象应该是这种格式(这个不是本篇开头的例子,而是我代码里的例子):为了方便,我会把它转为数组,得到了
patchArr
对象,如何更新呢?如何更新呢?如何写patch方法呢?
还是拿旧的 vdom 对象递归遍历,去跟
patchArr
数组对比。由于我
diff
内部采用的是 先序优先深度遍历 方法,简而言之,我是一个节点递归遍历完其所有子孙节点才会去递归遍历第二个节点,所以顺序是和递归遍历vdom一致的,这样只要比对vdom === patch.vdom
即可确定需要更新的节点。因为顺序一致,
patchArr
第一个元素肯定先被应用,接下来就是第二个,第三个...按顺序来应用,那我只要应用一个就删除一个,改变patchArr
数组,始终保持跟新数组的第一个元素对象做比对即可。小结
本篇主要是一个记录,具体代码还是要看virtual-dom-with-diff。其实虚拟DOM不难,只要明白需要什么数据格式然后转换成我需要的就好了,难得是diff方法,需要动点脑子。
接下来,我想把我之前写的mvvm和虚拟DOM以及diff结合起来,那样不就是个vue吗?哈哈哈哈哈,加油!
The text was updated successfully, but these errors were encountered: