diff算法
vdom
因为是纯粹的JS
对象,所以操作它会很高效,但是vdom
的变更最终会转换成DOM
操作,为了实现高效的DOM
操作,一套高效的虚拟DOM diff
算法显得很有必要。
Vue
的diff
算法仅在同级的vnode
间做diff
,递归地进行同级vnode
的diff
,最终实现整个DOM
树的更新。
oldStart
+oldEnd
,newStart
+newEnd
这样2对指针,分别对应oldVdom
和newVdom
的起点和终点。起止点之前的节点是待处理的节点,Vue
不断对vnode
进行处理同时移动指针直到其中任意一对起点和终点相遇。处理过的节点Vue
会在oldVdom
和newVdom
中同时将它标记为已处理。
Vue
通过diff
算法来比较新老vdom
的差异,计算出需要操作dom
的最少次数,实际中并以下措施来提升diff
的性能。
- 1、优先处理特殊场景:处理头尾同类型节点。
- 2、原地复用:尽可能复用
DOM
,尽可能不发生DOM
的移动。Vue
在判断更新前后指针是否指向同一个节点,其实不要求它们真实引用同一个DOM
节点,实际上它仅判断指向的是否是同类节点(比如2个不同的div
,在DOM
上它们是不一样的,但是它们属于同类节点),如果是同类节点,那么Vue
会直接复用DOM
,这样的好处是不需要移动DOM
。
整体视图
处理头尾同类型节点,即
oldStart
和newStart
指向同类节点的情况 /oldEnd
和newEnd
指向同类节点的情况,直接移动指针。处理头尾、尾头同类型节点,即
oldStart
和newEnd
,以及oldEnd
和newStart
指向同类节点的情况,互换位置。处理新增的节点:在
oldVdom
中找不到节点,说明它是新增的,那么就创建一个新的节点,插入DOM
树,插到什么位置?插到oldStart
指向的节点前面,然后将newStart
后移1位标记为已处理。oldStart
不需要移动,因为oldVdom
中没有这个节点。处理更新的节点:
newStart
来到的位置,在oldVdom
中能找到它而且不在指针位置(查找oldVdom
中oldStart
到oldEnd
区间内的节点),说明它的位置移动了,那么需要在DOM
树中移动它,移到哪里?移到oldStart
指向的节点前面,与此同时将节点标记为已处理(因为最后oldVdom
中此时还是存在这个节点的,之后指针会游到该节点的位置,如果被标记过已经处理了,则是需要出现在新DOM
中的节点,不要删除它,而是之前就以前移动过它了),跟前面几种情况有点不同,newVdom
中该节点在指针下,可以移动newStart
进行标记,而在oldVdom
中该节点不在指针处,所以采用设置为undefined
的方式来标记处理删除的节点:如果
newStart
跨过了newEnd
,它们相遇啦!而这个时候,oldStart
和oldEnd
还没有相遇,说明这2个指针之间的节点是此次更新中被删掉的节点。而如果有之前被标记过的节点,则不做处理,只删除此时没有标记过的节点。
在应用中也可能会遇到oldVdom
的起止点相遇了,但是newVdom
的起止点没有相遇的情况,这个时候需要对newVdom
中的未处理节点进行处理,这类节点属于更新中被加入的节点,需要将他们插入到DOM
树中。
第一部分是一个循环,循环内部是一个分支逻辑,每次循环只会进入其中的一个分支,每次循环会处理一个节点,处理之后将节点标记为已处理(oldVdom
和newVdom
都要进行标记,如果节点只出现在其中某一个vdom
中,则另一个vdom
中不需要进行标记),标记的方法有2种,当节点正好在vdom
的指针处,移动指针将它排除到未处理列表之外即可,否则就要采用其他方法,Vue
的做法是将节点设置为undefined
。
循环结束之后,可能newVdom
或者oldVdom
中还有未处理的节点,如果是newVdom
中有未处理节点,则这些节点是新增节点,做新增处理。如果是oldVdom
中有这类节点,则这些是需要删除的节点,相应在DOM
树中删除之
整个过程是逐步找到更新前后vdom
的差异,然后将差异反应到DOM
树上(也就是patch
),特别要提一下Vue
的patch
是即时的,并不是打包所有修改最后一起操作DOM
(React
则是将更新放入队列后集中处理)
虚拟dom
1、dom
很慢
当创建一个元素比如div
,有以下几项内容需要实现: HTML element
、Element
、GlobalEventHandler
。简单的说,就是插入一个dom
元素的时候,这个元素上本身或者继承很多属性如 width
、height
、offsetHeight
、style
、title
,另外还需要注册这个元素的诸多方法,比如onfucos
、onclick
等等。 这还只是一个元素,如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率很低。
尤其是在js
操作DOM
的过程中,不仅有dom
本身的繁重,js
的操作也需要浪费时间,我们认为js
和DOM
之间有一座桥,如果你频繁的在桥两边走动,显然效率是很低的。
虚拟dom
就是解决这个问题的! 虽然它解决不了dom
自身的繁重(虚拟dom
只实现了真实dom
的重要的属性和事件,但是最终渲染在页面的真实dom
依然是繁重的),但是虚拟dom
可以对js
操作dom
这一部分内容进行优化。
2、设计
隔离dom
并不仅仅是因为dom
慢,而也是为了把界面和业务完全隔离,操作数据的只关心数据,操作界面的只关心界面。
即我提供一个Component
,然后你只管给我数据,界面的事情完全不用你操心,我保证会把界面变成你想要的样子。所以说着力点就在于View
层。只要你给的数据是[1, 2, 3]
,我保证显示的是[1, 2, 3]
。没有什么删除一个Element
,添加一个Element
这样的事情。NO。你要我显示什么就给我一个完整的列表。
3、实现
首先,vdom
并没有完全实现dom
,即vdom
和真正地dom
是不一样的,vdom
最主要的还是保留了Element
之间的层次关系和一些基本属性。因为真实dom
实在是太复杂,一个空的Element
都复杂得能让你崩溃,并且几乎所有内容我根本不关心好吗。所以vdom
里每一个Element
实际上只有几个属性,即最重要的,最为有用的,并且没有那么多乱七八糟的引用,比如一些注册的属性和函数啊,这些都是默认的,创建vdom
进行diff
的过程中大家都一致,是不需要进行比对的。所以哪怕是直接把vdom
删了,根据新传进来的数据重新创建一个新的vdom
出来都非常非常非常快。。
所以,引入了vdom
之后,你给我一个数据,我根据这个数据生成一个全新的vdom
,然后跟我上一次生成的vdom
去 diff,得到一个Patch
,然后把这个Patch
打到浏览器的dom
上去。完事。并且这里的patch
显然不是完整的vdom
,而是新的vdom
和上一次的vdom
经过diff
后的差异化的部分。
假设在任意时候有,vdom1
== dom1
(组织结构相同, 显然vdom
和真实dom
是不可能完全相等的)。当有新数据来的时候,我生成vdom2
,然后去和vdom1
做diff
,得到一个Patch
(差异化的结果)。然后将这个Patch
去应用到dom1
上,得到dom2
。如果一切正常,那么有vdom2
== dom2
(同样是结构上的相等)。此时vdom2
就会与下一次vdom3
进行比较。
1.用JavaScript
对象来表示DOM
树的结构; 然后用这个树构建一个真正的DOM
树,插入到文档中。
2.当状态变更的时候,重新构造一个新的对象树,然后用这个新的树和旧的树作对比,记录两个树的差异。
3.把2所记录的差异应用在步骤一所构建的真正的DOM
树上,视图就更新了。
差异化实现
差异类型
1.替换原来的节点,如把上面的div换成了section。
2.移动、删除、新增子节点, 例如上面div的子节点,把p和ul顺序互换。
3.修改了节点的属性。
4.对于文本节点,文本内容可能会改变。
所以,我们可以定义下面的几种类型:
var REPLACE = 0;
var REORDER = 1;
var PROPS = 2;
var TEXT = 3;
对于节点替换,很简单,判断新旧节点的tagName是不是一样的,如果不一样的说明需要替换掉。 如div换成了section,就记录下:
patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}]
除此之外,还新增了属性id为container,就记录下:
pathches[0] = [
{
type: REPLACE,
node: newNode
},
{
type: PROPS,
props: {
id: 'container'
}
}
]
如果是文本节点发生了变化,那么就记录下:
pathches[2] = [
{
type: TEXT,
content: 'virtual DOM2'
}
]
列表对比算法
a b c d e f g h i => a b c h d f g i j
现在对节点进行了删除、插入、移动的操作。新增j节点,删除e节点,移动h节点,现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合),优化操作次数,我们能够获取到某个父节点的子节点的操作,就可以记录下来:
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]
把差异引用到真正的DOM树上
因为步骤一所构建的 JavaScript 对象树(Vdom)和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}
function dfsWalk (node, walker, patches) {
var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异
var len = node.childNodes ? node.childNodes.length: 0
for (var i = 0; i < len; i++) { // 深度遍历子节点
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}
if (currentPatches) {
applyPatches(node, currentPatches) // 对当前节点进行DOM操作
}
}
function applyPatches (node, currentPatches) {
currentPatches.forEach(function (currentPatch) {
switch (currentPatch.type) {
case REPLACE:
node.parentNode.replaceChild(currentPatch.node.render(), node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}
参考1 参考2