-
Notifications
You must be signed in to change notification settings - Fork 473
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
Vue 2.0 的 virtual-dom 实现简析 #18
Comments
vue 的 vnode 能转换为 react 的 vnode 么? |
太牛,不敢直视。 |
zuo zhe gao neng |
Even it's difficulty for me to read the analysis of source code. |
It's difficult to read these |
666,感谢分享干货,已推荐到 SegmentFault 头条 (๑•̀ㅂ•́)و✧ |
-,-居然看到了北林的校友 |
@xtx1130 老铁,握爪 |
厉害! |
反正我是看懵逼了 |
厉害啊。。。 |
哪怕现在再来看,都觉得非常牛逼 |
为啥我觉得有一步骤分析错了呢 |
BJFU+1 |
赞👍 |
文章写的很棒~不过diff算法的栗子中好像有点点问题,说下我的理解: 子节点第一次比较时,newStartIndex位置的 B节点 在oldCh节点的start和end位置都不存在,这个时候应该是执行while中的最后一个else(而不是直接将B添加到oldStartVnode最后再删除old中的B),即开始比较key值,由于oldCh中存在newStartVnode.key,即idxInOld存在,则将找到的key一致的oldVnode再和newStartVnode进行diff,如果sameVnode(elmToMove, newStartVnode)成立,则patchVnode,(省略中间步骤)同时newStartIdx加一。 所以比较到最后,oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,需要将多出来的(F)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。 |
感谢分享 |
很棒!~~ |
“同时节点属性中是不带key标记的”,说例子有问题的那位兄弟没看到这句话吗。。 |
Vue版本: 2.3.2
virtual-dom
(后文简称vdom
)的概念大规模的推广还是得益于react
出现,virtual-dom
也是react
这个框架的非常重要的特性之一。相比于频繁的手动去操作dom
而带来性能问题,vdom
很好的将dom
做了一层映射关系,进而将在我们本需要直接进行dom
的一系列操作,映射到了操作vdom
,而vdom
上定义了关于真实dom
的一些关键的信息,vdom
完全是用js
去实现,和宿主浏览器没有任何联系,此外得益于js
的执行速度,将原本需要在真实dom
进行的创建节点
,删除节点
,添加节点
等一系列复杂的dom
操作全部放到vdom
中进行,这样就通过操作vdom
来提高直接操作的dom
的效率和性能。Vue
在2.0
版本也引入了vdom
。其vdom
算法是基于snabbdom算法所做的修改。在
Vue
的整个应用生命周期当中,每次需要更新视图的时候便会使用vdom
。那么在Vue
当中,vdom
是如何和Vue
这个框架融合在一起工作的呢?以及大家常常提到的vdom
的diff
算法又是怎样的呢?接下来就通过这篇文章简单的向大家介绍下Vue
当中的vdom
是如何去工作的。首先,我们还是来看下
Vue
生命周期当中初始化的最后阶段:将vm
实例挂载到dom
上,源码在src/core/instance/init.js实际上是调用了src/core/instance/lifecycle.js中的
mountComponent
方法,mountComponent
函数的定义是:注意上面的代码中定义了一个
updateComponent
函数,这个函数执行的时候内部会调用vm._update(vm._render(), hyddrating)
方法,其中vm._render
方法会返回一个新的vnode
,(关于vm_render
是如何生成vnode
的建议大家看看vue
的关于compile
阶段的代码),然后传入vm._update
方法后,就用这个新的vnode
和老的vnode
进行diff
,最后完成dom
的更新工作。那么updateComponent
都是在什么时候去进行调用呢?实例化一个
watcher
,在求值的过程中this.value = this.lazy ? undefined : this.get()
,会调用this.get()
方法,因此在实例化的过程当中Dep.target
会被设为这个watcher
,通过调用vm._render()
方法生成新的Vnode
并进行diff
的过程中完成了模板当中变量依赖收集工作。即这个watcher
被添加到了在模板当中所绑定变量的依赖当中。一旦model
中的响应式的数据发生了变化,这些响应式的数据所维护的dep
数组便会调用dep.notify()
方法完成所有依赖遍历执行的工作,这里面就包括了视图的更新即updateComponent
方法,它是在mountComponent
中的定义的。updateComponent
方法的定义是:完成视图的更新工作事实上就是调用了
vm._update
方法,这个方法接收的第一个参数是刚生成的Vnode
,调用的vm._update
方法(src/core/instance/lifecycle.js)的定义是在这个方法当中最为关键的就是
vm.__patch__
方法,这也是整个virtaul-dom
当中最为核心的方法,主要完成了prevVnode
和vnode
的diff
过程并根据需要操作的vdom
节点打patch
,最后生成新的真实dom
节点并完成视图的更新工作。接下来就让我们看下
vm.__patch__
里面到底发生了什么:在对
oldVnode
和vnode
类型判断中有个sameVnode
方法,这个方法决定了是否需要对oldVnode
和vnode
进行diff
及patch
的过程。sameVnode
会对传入的2个vnode
进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode
只是局部发生了更新,然后才会对这2个vnode
进行diff
,如果2个vnode
的基本属性存在不一致的情况,那么就会直接跳过diff
的过程,进而依据vnode
新建一个真实的dom,同时删除老的dom
节点。vnode
基本属性的定义可以参见源码:src/vdom/vnode.js里面对于vnode
的定义。每一个
vnode
都映射到一个真实的dom
节点上。其中几个比较重要的属性:tag
属性即这个vnode
的标签属性data
属性包含了最后渲染成真实dom
节点后,节点上的class
,attribute
,style
以及绑定的事件children
属性是vnode
的子节点text
属性是文本属性elm
属性为这个vnode
对应的真实dom
节点key
属性是vnode
的标记,在diff
过程中可以提高diff
的效率,后文有讲解比如,我定义了一个
vnode
,它的数据结构是:最后渲染出的实际的
dom
结构就是:让我们再回到
patch
函数当中,在当oldVnode
不存在的时候,这个时候是root节点
初始化的过程,因此调用了createElm(vnode, insertedVnodeQueue, parentElm, refElm)
方法去创建一个新的节点。而当oldVnode
是vnode
且sameVnode(oldVnode, vnode)
2个节点的基本属性相同,那么就进入了2个节点的diff
过程。diff
的过程主要是通过调用patchVnode
(src/core/vdom/patch.js)方法进行的:更新真实
dom
节点的data
属性,相当于对dom
节点进行了预处理的操作接下来:
这其中的
diff
过程中又分了好几种情况,oldCh
为oldVnode
的子节点,ch
为Vnode
的子节点:oldVnode.text !== vnode.text
,那么就会直接进行文本节点的替换;vnode
没有文本节点的情况下,进入子节点的diff
;oldCh
和ch
都存在且不相同的情况下,调用updateChildren
对子节点进行diff
;oldCh
不存在,ch
存在,首先清空oldVnode
的文本节点,同时调用addVnodes
方法将ch
添加到elm
真实dom
节点当中;oldCh
存在,ch
不存在,则删除elm
真实节点下的oldCh
子节点;oldVnode
有文本节点,而vnode
没有,那么就清空这个文本节点。这里着重分析下
updateChildren
(src/core/vdom/patch.js)方法,它也是整个diff
过程中最重要的环节:在开始遍历
diff
前,首先给oldCh
和newCh
分别分配一个startIndex
和endIndex
来作为遍历的索引,当oldCh
或者newCh
遍历完后(遍历完的条件就是oldCh
或者newCh
的startIndex >= endIndex
),就停止oldCh
和newCh
的diff
过程。接下来通过实例来看下整个diff
的过程(节点属性中不带key
的情况):首先从第一个节点开始比较,不管是
oldCh
还是newCh
的起始或者终止节点都不存在sameVnode
,同时节点属性中是不带key
标记的,因此第一轮的diff
完后,newCh
的startVnode
被添加到oldStartVnode
的前面,同时newStartIndex
前移一位;第二轮的
diff
中,满足sameVnode(oldStartVnode, newStartVnode)
,因此对这2个vnode
进行diff
,最后将patch
打到oldStartVnode
上,同时oldStartVnode
和newStartIndex
都向前移动一位第三轮的
diff
中,满足sameVnode(oldEndVnode, newStartVnode)
,那么首先对oldEndVnode
和newStartVnode
进行diff
,并对oldEndVnode
进行patch
,并完成oldEndVnode
移位的操作,最后newStartIndex
前移一位,oldStartVnode
后移一位;第四轮的
diff
中,过程同步骤3;第五轮的
diff
中,同过程1;遍历的过程结束后,
newStartIdx > newEndIdx
,说明此时oldCh
存在多余的节点,那么最后就需要将这些多余的节点删除。在
vnode
不带key
的情况下,每一轮的diff
过程当中都是起始
和结束
节点进行比较,直到oldCh
或者newCh
被遍历完。而当为vnode
引入key
属性后,在每一轮的diff
过程中,当起始
和结束
节点都没有找到sameVnode
时,首先对oldCh
中进行key
值与索引的映射:createKeyToOldIdx
(src/core/vdom/patch.js)方法,用以将oldCh
中的key
属性作为键
,而对应的节点的索引作为值
。然后再判断在newStartVnode
的属性中是否有key
,且是否在oldKeyToIndx
中找到对应的节点。key
,那么就将这个newStartVnode
作为新的节点创建且插入到原有的root
的子节点中:key
,那么就取出oldCh
中的存在这个key
的vnode
,然后再进行diff
的过程:通过以上分析,给
vdom
上添加key
属性后,遍历diff
的过程中,当起始点
,结束点
的搜寻
及diff
出现还是无法匹配的情况下时,就会用key
来作为唯一标识,来进行diff
,这样就可以提高diff
效率。带有
Key
属性的vnode
的diff
过程可见下图:注意在第一轮的
diff
过后oldCh
上的B节点
被删除了,但是newCh
上的B节点
上elm
属性保持对oldCh
上B节点
的elm
引用。The text was updated successfully, but these errors were encountered: