-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
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
解析vue2.0的diff算法 #2
Comments
@ |
mark |
配图好评,看代码绝对会晕的 |
和 react 的几乎一样 |
@ascoders 是的 |
赞,mark |
详细,点赞 |
写的很好,点个赞。 |
@wangweida 是的笔误了,谢谢提醒。 |
赞,有一处需要指出的是: |
@zengwenfu 谢谢指正,本文从原始算法去解读vue的diff,没有完全契合vue,vue实际情况会更复杂,它考虑到了class的动态绑定和input的type值,对sameVnode进行了修改。 |
配图满分,很好理解 |
有个疑问,假如一个节点是根节点的好几层子节点,那么它对应的vnode的el属性应该怎么表示?是从根节点一层层套下去吗?我看了你的代码还没明白。 |
@linzb93 一层一层套的是vnode的children属性,el存的是此vnode渲染出来的Element对象,也就是真实节点。 <body>
<div></div>
<body>
//对应的vnode
{
el: document.body,
children: [
{
el: document.body.children[0],
children: [],
...
}
],
...
} |
@aooy 我也有这么想过,就是会担心性能问题(我怎么会担心这个→_→),看来真的是这样的。谢谢了。。 |
react diff 几乎一样... |
@liz282907 这篇文章学习过的,有借鉴的地方。vue和react的diff就当是黑猫白猫吧 |
react虚拟DOM算法 和vue 的这个有什么不同呢? |
学习了 |
mark |
mark |
有一点疑问搜了好多也没有特别明白的回答,就是后面说的三个优化点,具体能有个例子么,尤其是手动优化diff |
@alabihula 我觉得之前第三点的说法不妥当,所以改啦,很多时候明显自己操作dom会快很多,但是框架本身就是为了方便开发和协作的,跳出框架去做这些优化反而会增加维护的成本。 |
👍 |
@aooy 赞!感谢分享,有一处需要指正
看一下before的定义 |
对着源码看了下,豁然洞开,赞👍 |
请问,patchVnode 中的 updateEle(el, vnode, oldVnode) 的含义是啥,看了下该函数,应该是为节点设置class、ID、data等属性,谢谢 |
mark |
mark,感谢 |
看完了 一开始看得很头疼 特别是updateChildren 那里 |
题主对diff算法解析的真是到位,虽然diff不是最优解,但是综合来看,易推广 |
我也是这样理解的
|
|
你好,我发现你的第一个例子,也就是 不带key的例子有点不合理 idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
} 由于没有key ,那么肯定是走 findIdxInOld。 例如第一个点 b,通过 sameVnode 它还是可以找到 oldch 里面的 d 的啊,所以第一步不会是插入。 这是我的代码 <head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript" src="vue.js"></script>
</head>
<body>
<div id="app" @click="change">
<component :is="'h'+mm[n]" v-for="n in arr" >{{n}}</component>
</div>
</body>
<script type="text/javascript">
let s = 1;
var app = new Vue({
el: '#app',
data() {
return {
arr: ["a","b","c","d"],
tag:"span",
mm:{
"a":1,"b":2,"c":3,"d":4,"e":5
}
}
},
methods: {
change() {
this.arr = ["b","e","d","c"];
//this.tag = "h"+ s++;
}
}
})
</script> |
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
另外我结合大佬的文章和国外一个讲虚拟DOM的把两者结合实现了一个可直观感受且跑得通得DEMO。请看:https://github.com/xubaifuCode/virtual-dom-and-diff-implementation |
大佬 加上key的那张图 c是不是也不需要移动啊 |
一開始看圖真的看不懂 直接看源碼還清楚些 一直碎碎念為何C要移動 |
|
Mark |
为什么不设key,newCh和oldCh只会进行头尾两端的相互比较,这样怎么保证更新的dom正确呢? |
对于vnode.key的比较,会把oldVnode = null请问这句话怎么理解啊? |
仔细想了想 确实应该插入到newEndIdx之后,因为循环结束后 包括newStartIdx和newEndIdx都是新增的节点 |
看了评论有很多人提出来这个问题,其实是尤雨溪在解决 issues/6502 的时候添加的。文章作者写的时候这个问题还存在。 |
作者:杨敬卓
转载请注明出处
目录
前言
vue2.0加入了virtual dom,有向react靠拢的意思。vue的diff位于patch.js文件中,我的一个小框架aoy也同样使用此算法,该算法来源于snabbdom,复杂度为O(n)。
了解diff过程可以让我们更高效的使用框架。
本文力求以图文并茂的方式来讲明这个diff的过程。
virtual dom
如果不了解virtual dom,要理解diff的过程是比较困难的。虚拟dom对应的是真实dom, 使用
document.CreateElement
和document.CreateTextNode
创建的就是真实节点。我们可以做个试验。打印出一个空元素的第一层属性,可以看到标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。
virtual dom就是解决这个问题的一个思路,到底什么是virtual dom呢?通俗易懂的来说就是用一个简单的对象去代替复杂的dom对象。
举个简单的例子,我们在body里插入一个class为a的div。
对于这个div我们可以用一个简单的对象
mydivVirtual
代表它,它存储了对应dom的一些重要参数,在改变dom之前,会先比较相应虚拟dom的数据,如果需要改变,才会将改变应用到真实dom上。读到这里就会产生一个疑问,为什么不直接修改dom而需要加一层virtual dom呢?
很多时候手工优化dom确实会比virtual dom效率高,对于比较简单的dom结构用手工优化没有问题,但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此,virtual dom的解决方案应运而生,virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。
virtual dom 另一个重大意义就是提供一个中间层,js去写ui,ios安卓之类的负责渲染,就像reactNative一样。
分析diff
一篇相当经典的文章React’s diff algorithm中的图,react的diff其实和vue的diff大同小异。所以这张图能很好的解释过程。比较只会在同层级进行, 不会跨层级比较。
我们可能期望将
<span>
直接移动到<p>
的后边,这是最优的操作。但是实际的diff操作是移除<p>
里的<span>
在创建一个新的<span>
插到<p>
的后边。因为新加的
<span>
在层级2,旧的在层级3,属于不同层级的比较。源码分析
文中的代码位于aoy-diff中,已经精简了很多代码,留下最核心的部分。
diff的过程就是调用patch函数,就像打补丁一样修改真实dom。
patch
函数有两个参数,vnode
和oldVnode
,也就是新旧两个虚拟节点。在这之前,我们先了解完整的vnode都有什么属性,举个一个简单的例子:需要注意的是,el属性引用的是此 virtual dom对应的真实dom,
patch
的vnode
参数的el
最初是null,因为patch
之前它还没有对应的真实dom。来到
patch
的第一部分,sameVnode
函数就是看这两个节点是否值得比较,代码相当简单:两个vnode的key和sel相同才去比较它们,比如
p
和span
,div.classA
和div.classB
都被认为是不同结构而不去比较它们。如果值得比较会执行
patchVnode(oldVnode, vnode)
,稍后会详细讲patchVnode
函数。当节点不值得比较,进入else中
过程如下:
oldvnode.el
的父节点,parentEle
是真实domcreateEle(vnode)
会为vnode
创建它的真实dom,令vnode.el
=真实dom
parentEle
将新的dom插入,移除旧的dom当不值得比较时,新节点直接把老节点整个替换了
最后
patch最后会返回vnode,vnode和进入patch之前的不同在哪?
没错,就是vnode.el,唯一的改变就是之前vnode.el = null, 而现在它引用的是对应的真实dom。
至此完成一个patch过程。
patchVnode
两个节点值得比较时,会调用
patchVnode
函数const el = vnode.el = oldVnode.el
这是很重要的一步,让vnode.el
引用到现在的真实dom,当el
修改时,vnode.el
会同步变化。节点的比较有5种情况
if (oldVnode === vnode)
,他们的引用一致,可以认为没有变化。if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text)
,文本节点的比较,需要修改,则会调用Node.textContent = vnode.text
。if( oldCh && ch && oldCh !== ch )
, 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren
函数比较子节点,这是diff的核心,后边会讲到。else if (ch)
,只有新的节点有子节点,调用createEle(vnode)
,vnode.el
已经引用了老的dom节点,createEle
函数会在老dom节点上添加子节点。else if (oldCh)
,新节点没有子节点,老节点有子节点,直接删除老节点。updateChildren
代码很密集,为了形象的描述这个过程,可以看看这张图。
过程可以概括为:
oldCh
和newCh
各有两个头尾的变量StartIdx
和EndIdx
,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至少有一个已经遍历完了,就会结束比较。具体的diff分析
设置key和不设置key的区别:
不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象
oldKeyToIdx
中查找匹配的节点,所以为节点设置key可以更高效的利用dom。diff的遍历过程中,只要是对dom进行的操作都调用
api.insertBefore
,api.insertBefore
只是原生insertBefore
的简单封装。比较分为两种,一种是有
vnode.key
的,一种是没有的。但这两种比较对真实dom的操作是一致的。对于与
sameVnode(oldStartVnode, newStartVnode)
和sameVnode(oldEndVnode,newEndVnode)
为true的情况,不需要对dom进行移动。总结遍历过程,有3种dom操作:
oldStartVnode
,newEndVnode
值得比较,说明oldStartVnode.el
跑到oldEndVnode.el
的后边了。图中假设startIdx遍历到1。
oldEndVnode
,newStartVnode
值得比较,说明oldEndVnode.el
跑到了newStartVnode.el
的前边。(这里笔误,应该是“oldEndVnode.el跑到了oldStartVnode.el的前边”,准确的说应该是oldEndVnode.el需要移动到oldStartVnode.el的前边”)oldStartVnode.el
的前边。在结束时,分为两种情况:
oldStartIdx > oldEndIdx
,可以认为oldCh
先遍历完。当然也有可能newCh
此时也正好完成了遍历,统一都归为此类。此时newStartIdx
和newEndIdx
之间的vnode是新增的,调用addVnodes
,把他们全部插进before
的后边,before
很多时候是为null的。addVnodes
调用的是insertBefore
操作dom节点,我们看看insertBefore
的文档:parentElement.insertBefore(newElement, referenceElement)
如果referenceElement为null则newElement将被插入到子节点的末尾。如果newElement已经在DOM树中,newElement首先会从DOM树中移除。所以
before
为null,newElement将被插入到子节点的末尾。newStartIdx > newEndIdx
,可以认为newCh
先遍历完。此时oldStartIdx
和oldEndIdx
之间的vnode在新的子节点里已经不存在了,调用removeVnodes
将它们从dom里删除。下面举个例子,画出diff完整的过程,每一步dom的变化都用不同颜色的线标出。
这个例子如果我们使用手工优化,只需要3步就可以达到。
总结
The text was updated successfully, but these errors were encountered: