Vue中diff算法的理解
Vue中diff算法的理解
diff
算法用来计算出Virtual DOM
中改变的部分,然后针对该部分进行DOM
操作,而不用重新渲染整个页面,渲染整个DOM
结构的过程中开销是很大的,需要浏览器对DOM
结构进行重绘与回流,而diff
算法能够使得操作过程中只更新修改的那部分DOM
结构而不更新整个DOM
,这样能够最小化操作DOM
结构,能够最大程度上减少浏览器重绘与回流的规模。
虚拟DOM
diff
算法的基础是Virtual DOM
,Virtual DOM
是一棵以JavaScript
对象作为基础的树,每一个节点称为VNode
,用对象属性来描述节点,实际上它是一层对真实DOM
的抽象,最终可以通过渲染操作使这棵树映射到真实环境上,简单来说Virtual DOM
就是一个Js
对象,用以描述整个文档。
在浏览器中构建页面时需要使用DOM
节点描述整个文档。
<div class="root" name="root">
<p>1</p>
<div>11</div>
</div>
如果使用Js
对象去描述上述的节点以及文档,那么便类似于下面的样子,当然这不是Vue
中用以描述节点的对象,Vue
中用以描述一个节点的对象包括大量属性,例如tag
、data
、children
、text
、elm
、ns
、context
、key
、componentOptions
、componentInstance
、parent
、raw
、isStatic
、isRootInsert
、isComment
、isCloned
等等,具体的属性可以参阅Vue
源码的/dev/src/core/vdom/vnode.js
。
{
type: "tag",
tagName: "div",
attr: {
className: "root"
name: "root"
},
parent: null,
children: [{
type: "tag",
tagName: "p",
attr: {},
parent: {} /* 父节点的引用 */,
children: [{
type: "text",
tagName: "text",
parent: {} /* 父节点的引用 */,
content: "1"
}]
},{
type: "tag",
tagName: "div",
attr: {},
parent: {} /* 父节点的引用 */,
children: [{
type: "text",
tagName: "text",
parent: {} /* 父节点的引用 */,
content: "11"
}]
}]
}
当选用diff
算法进行部分更新的时候就需要比较旧DOM
结构与新DOM
结构的不同,此时就需要VNode
来描述整个DOM
结构,首先根据真实DOM
生成Virtual DOM
,当Virtual DOM
某个节点的数据改变后会生成一个新的Vnode
,然后通过newVNode
和oldVNode
进行对比,发现有不同之处便通过在VNode
中elm
属性相对应的真实DOM
节点进行patch
修改于真实DOM
,然后使旧的Virtual DOM
赋值为新的Virtual DOM
。
diff算法
当数据发生改变时,set
方法会让调用Dep.notify
通知所有订阅者Watcher
数据发生更新,订阅者就会调用patch
进行比较,然后将相应的部分渲染到真实DOM
结构。
时间复杂度
首先进行一次完整的diff
需要O(n^3)
的时间复杂度,这是一个最小编辑距离的问题,在比较字符串的最小编辑距离时使用动态规划的方案需要的时间复杂度是O(mn)
,但是对于DOM
来说是一个树形结构,而树形结构的最小编辑距离问题的时间复杂度在30
多年的演进中从O(m^3n^3)
演进到了O(n^3)
,关于这个问题如果有兴趣的话可以研究一下论文https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
。
对于原本想要提高效率而引入的diff
算法使用O(n^3)
的时间复杂度显然是不太合适的,如果有1000
个节点元素将需要进行十亿次比较,这是一个昂贵的算法,所以必须有一些妥协来加快速度,对比较通过一些策略进行简化,将时间复杂度缩小到O(n)
,虽然并不是最小编辑距离,但是作为编辑距离与时间性能的折中是一个比较好的解决方案。
diff策略
上边提到的O(n)
时间复杂度是通过一定策略进行的,React
中提到了两个假设,在Vue
中同样适用:
- 两个不同类型的元素将产生不同的树。
- 通过渲染器附带
key
属性,开发者可以示意哪些子元素可能是稳定的。
通俗点说就是:
- 只进行统一层级的比较,如果跨层级的移动则视为创建和删除操作。
- 如果是不同类型的元素,则认为是创建了新的元素,而不会递归比较他们的孩子。
- 如果是列表元素等比较相似的内容,可以通过
key
来唯一确定是移动还是创建或删除操作。
比较后会出现几种情况,然后进行相应的操作:
- 此节点被添加或移除
->
添加或移除新的节点。 - 属性被改变
->
旧属性改为新属性。 - 文本内容被改变
->
旧内容改为新内容。 - 节点
tag
或key
是否改变->
改变则移除后创建新元素。
分析
实现diff
算法的部分在Vue
源码中的dev/src/core/vdom/patch.js
文件中,不过Vue
源码的实现比较复杂,文章分析比较核心的代码部分,精简过后的最小化版本,commit id
为43b98fe
。
在调用patch
方法时,会判断是否是VNode
,isRealElement
其实就是根据有没有nodeType
来判断是不是真实DOM
,VNode
是不存在这个字段的,如果不是真实DOM
元素,并且这两个节点是相同的,那就就会进入这个if
内部,调用patchVnode
对children
进行diff
以决定该如何更新。
// line 714
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}else{
// ...
}
接下来看一下sameVnode
方法,判断如何算是相同节点。
// line 35
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
这里的判断条件其实主要是两个:
key
必须相同,如果都是undefined
则也是相同的。DOM
元素的标签必须相同。
如果满足以上条件,那么就认为是相同的VNode
,因此就可以进行patchVnode
操作,如果不是就认为是完全新的一个VNode
,就会在上边的判断后执行下面的createElm
。
梳理一下逻辑,当进入patch
之后有两种分支可以走,如果是第一次patch
,即组件第一次挂载的时候,或者发现元素的标签不相同了,那么就认为是不同的元素,直接进行createElm
创建新的DOM
元素进行替换,否则,就是对已存在的DOM
元素进行更新,那么通过patchVnode
进行diff
,有条件的更新以提升性能,这样其实就实现了策略中原则的第一条,即两个不同类型的元素将产生不同的树,只要发现两个元素的类型不同,我们直接删除旧的并创建一个新的,而不是去递归比较。
在认为这是两个相同的VNode
之后,就需要比较并更新当前元素的差异,以及递归比较children
,在patchVnode
方法中实现了这两部分。
// line 501
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// ...
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
//...
}
cbs.update
主要是用来更新attributes
的,这里的cbs
其实是从hooks
中来的,hooks
在33
行有如下定义,const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
,其是在VNode
更新的各个阶段进行相应的操作,这里cbs.update
包含如下几个回调:updateAttributes
、updateClass
、updateDOMListeners
、updateDOMProps
、updateStyle
、update
、updateDirectives
,其主要都是更新当前结点的一些相关attributes
。
之后需要更新孩子节点,这时候分两种情况:
- 如果孩子不是
textNode
,那么需要再分三种情况处理。 - 如果当前孩子是
textNode
那么直接更新text
即可。
对孩子是VNode
的三种情况:
- 有新孩子无旧孩子,直接创建新的。
- 有旧孩子无新孩子,直接删除旧的。
- 新旧孩子都有,那么调用
updateChildren
。
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
当新旧孩子都存在,那么便调用updateChildren
方法,对于每一个孩子节点,我们依然有这么几种可能:
- 更新了节点
- 删除了节点
- 增加了节点
- 移动了节点
updateChildren
是diff
的核心算法,源代码实现如下。
// line 404
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
其对新旧两个children
数组分别在首位各用了一个指针,总共四个指针,由于指针仅仅对数组进行了一次遍历,因此时间复杂度是O(n)
,举个简单例子说明diff
过程。
old VNode: a(oldStartIdx) b c d e f(oldEndIdx)
new VNode: b(newStartIdx) f g(newEndIdx)
DOM Node: a b c d e f
首先指针相互比较,即四种对比,分别为oldStartIdx
和newStartIdx
、oldStartIdx
和newEndIdx
、oldEndIdx
和newStartIdx
、oldEndIdx
和newEndIdx
,如果没有相等的则继续。此时分为两种情况,有key
和无key
,无key
则直接创建新的DOM Node
插入到a(oldStartIdx)
之前,此处认为key
存在,有key
的话取newStartIdx
的key
值,到old VNode
去找,记录此时的oldKeyToIdx
,随即调整VNode
,将b
移动到a
之前,然后找到old VNode
中oldKeyToIdx
对应的节点值设置为undefined
,newStartIdx
指针向中间靠拢,即++newStartIdx
。
old VNode: a(oldStartIdx) undefined c d e f(oldEndIdx)
new VNode: b f(newStartIdx) g(newEndIdx)
DOM Node: b a c d e f
循环继续,此时对比oldStartIdx
和newStartIdx
、oldStartIdx
和newEndIdx
、oldEndIdx
和newStartIdx
、oldEndIdx
和newEndIdx
,发现newStartIdx
与oldEndIdx
相同,将DOM Node
中的f
进行移动调整到DOM Node
中的a(oldStartIdx)
之前,此时newStartIdx
与oldEndIdx
指针向中间靠拢,即++newStartIdx
与--oldEndIdx
。
old VNode: a(oldStartIdx) undefined c d e(oldEndIdx) f
new VNode: b f g(newStartIdx)(newEndIdx)
DOM Node: b f a c d e
循环继续,此时对比oldStartIdx
和newStartIdx
、oldStartIdx
和newEndIdx
、oldEndIdx
和newStartIdx
、oldEndIdx
和newEndIdx
,并没有相同的情况,取newStartIdx
的key
值,到old VNode
去找,没有发现相同的值,则直接创建一个节点插入到DOM Node
中的a(oldStartIdx)
之前,newStartIdx
指针向中间靠拢,即++newStartIdx
。
old VNode: a(oldStartIdx) undefined c d e(oldEndIdx) f
new VNode: b f g(newEndIdx) (newStartIdx)
DOM Node: b f g a c d e
此时循环结束,有两个选择:
- 如果
oldStartldx > oldEndldx
,说明老节点遍历完成了,新的节点比较多,所以多出 来的这些新节点,需要创建出来并添加到真实DOM Node
后面。 - 如果
newStartldx >newEndldx
,说明新节点遍历完成了,老的节点比较多,所以多 出来的这些老节点,需要从真实DOM Node
中删除这些节点。
此时我们符合场景二,所以需要从真实DOM Node
中删除[oldStartldx,oldEndldx]
区间 中的Node
节点,根据上述内容,即需要删除a c d e
四个节点,至此diff
完成。
old VNode: a(oldStartIdx) undefined c d e(oldEndIdx) f
new VNode: b f g(newEndIdx) (newStartIdx)
DOM Node: b f g
diff
完成之后便是将new VNode
作为old VNode
以便下次diff
时使用,此外关于组件的diff
,组件级别的diff
算法比较简单,节点不相同就进行创建和替换,节点相同的话就会对其子节点进行更新,最后关于调用createElm
来根据VNode
创建真实的DOM
元素,如果是一个组件,那么 createComponent
会返回true
,因此不会进行接下来的操作,如果不是组件,会进行节点创建工作,并会递归对孩子创建节点。
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://github.com/aooy/blog/issues/2
https://www.zhihu.com/question/66851503
https://juejin.im/post/6844903607913938951
https://juejin.im/post/6844903592483094535
https://reactjs.org/docs/reconciliation.html
https://www.cnblogs.com/lilicat/p/13448827.html
https://www.cnblogs.com/lilicat/p/13448915.html
https://github.com/lihongxun945/myblog/issues/33
https://www.cnblogs.com/xujiazheng/p/12101764.html
https://blog.csdn.net/dongcehao/article/details/106987886
https://blog.csdn.net/qq2276031/article/details/106407647
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/151
https://leetcode-cn.com/problems/edit-distance/solution/bian-ji-ju-chi-by-leetcode-solution/
Vue中diff算法的理解的更多相关文章
- React中diff算法的理解
React中diff算法的理解 diff算法用来计算出Virtual DOM中改变的部分,然后针对该部分进行DOM操作,而不用重新渲染整个页面,渲染整个DOM结构的过程中开销是很大的,需要浏览器对DO ...
- Vue 中 diff 算法后更新 DOM 的方法
vue 2.0加入了 virtual dom,在 node_modules\vue\src\core\vdom\patch.js 中会对虚拟 DOM 进行 diff 算法等,然后更新 DOM. 网上的 ...
- 详解vue的diff算法
前言 我的目标是写一个非常详细的关于diff的干货,所以本文有点长.也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角. 先来了解几个点... 1. 当数据发生变化时, ...
- vue的diff算法
前言 我的目标是写一个非常详细的关于diff的干货,所以本文有点长.也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角. 先来了解几个点... 1. 当数据发生变化时, ...
- 详解vue的diff算法原理
我的目标是写一个非常详细的关于diff的干货,所以本文有点长.也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角. 先来了解几个点... 1. 当数据发生变化时,vue ...
- vue中的插槽slot理解
本篇文章参考赛冷思的个人博客 1.函数默认传参 在我们写js函数我们的可能会给他们一个默认的参数,写法是 function show(age,name){ var age = age || 20; v ...
- vue中webpack的配置理解
当我们需要和后台分离部署的时候,必须配置config/index.js: 用vue-cli 自动构建的目录里面 (环境变量及其基本变量的配置) var path = require('path') ...
- Vue中keep-alive组件的理解
对keep-alive组件的理解 当在组件之间切换的时候,有时会想保持这些组件的状态,以避免反复重渲染导致的性能等问题,使用<keep-alive>包裹动态组件时,会缓存不活动的组件实例, ...
- vue中mixin的一点理解
vue中提供了一种混合机制--mixins,用来更高效的实现组件内容的复用.最开始我一度认为这个和组件好像没啥区别..后来发现错了.下面我们来看看mixins和普通情况下引入组件有什么区别? ...
随机推荐
- vue-router 报错、:Avoided redundant navigation to current location 错误、路由重复
在用vue-router 做单页应用的时候重复点击一个跳转的路由会出现报错 这个报错是重复路由引起的只需在注册路由组建后使用下方重写路由就可以 const originalReplace = VueR ...
- 为什么大家都在用Fiddler?
在我们做接口测试的时候,经常需要验证发送的消息是否正确,或者在出现问题的时候,查看手机客户端发送给server端的包内容是否正确,就需要用到抓包工具.常用的抓包工具有fiddler.wireshark ...
- Go Pentester - HTTP CLIENTS(2)
Building an HTTP Client That Interacts with Shodan Shadon(URL:https://www.shodan.io/) is the world' ...
- javascript : 递归遍历数组
我们假设有一个对象数组. 这个对象数组里的对象,有一个叫children的key,value也是一个对象数组. 这个数组里面可能还有children... 现在我们想递归遍历它. 上代码. test_ ...
- Inoreader - 在线Rss阅读器
- Python基础点记录1
1 变量:一个变量就是一个单词,只有一个单一的值 1 Python里面的数据类型 interage , floats , booleans , String等 2 Python是一个区分大小写的语言 ...
- Laragon修改配置快速创建其他框架的项目
配置方式 依葫芦画瓢,如添加thinkPHP: # Thinkphp Thinkphp 3.2=composer create-project topthink/thinkphp %s Thinkph ...
- 一切皆组件的Flutter,安能辨我是雄雌
从一开始接触Flutter,相信读者都会铭记一句话,那就是--一切皆组件.今天我们就来体会一下这句话的神奇魔力,我们先从实际的产品需求说起. 我们先来看一个简化的运行图: 我们要实现如上图所示的日期选 ...
- linux gdb 入门级教程(小白专用)
送给包含我在内的所有小白: 对于养linux真姬的本小白来说,既然你选择养它,那你就要满足他. 如果你养了它是为了码代码,那我觉得gdb应该是它的基本需求了吧?! 然而gdb哪有那些IDE来的简单啊, ...
- Day14_RabbitMQ及数据同步
学于黑马和传智播客联合做的教学项目 感谢 黑马官网 传智播客官网 微信搜索"艺术行者",关注并回复关键词"乐优商城"获取视频和教程资料! b站在线视频 0.学习 ...