vue dom diff,virtual dom diff算法
本文主要介绍了vue中虚拟DOM和Diff算法的知识,有需要的朋友可以参考一下。希望能有所帮助。祝大家进步很大,早日升职加薪。
目录
前言虚拟dom:什么是虚拟DOM为什么要用虚拟DOM:虚拟DOM库diff算法snabbdom的核心init函数H函数patch函数(核心)diff算法介绍传统diff算法snabbdom的diff算法优化updateChildren(内核:判断子节点的差异)新开始节点和旧开始节点(情况1)新结束节点和旧结束节点(情况2)旧开始节点/新结束节点(情况3)旧结束节点/新开始节点(情况4) 新开始节点/旧节点数组(情况5)5)键Diff操作的作用(避免渲染错误):Diff操作可以更准确; (避免呈现错误)不建议使用索引作为键:
前言
面试官:
你知道虚拟DOM和Diff算法吗?请描述一下。
我:
呃,鹅,那个’,完了,突然智商不在线,语言组织不好,回答不好或者根本回答不出来;
所以这次我就把相关知识点总结一下,让你有一个清晰的认知。除此之外,还会让你以后在这种情况下感到舒服,舒服,舒服。
虚拟DOM(Virtual DOM):
什么是虚拟DOM
总之,虚拟DOM是用来描述真实DOM的javaScript对象,可能不够生动。让我们举个例子:
用代码分别描述真实DOM和虚拟DOM。
真正的DOM
ul class=list
李/李
李/李
李/李
/ul
对应的虚拟DOM:
设vnode=h(ul.list ,[
h(李,甲),
h(李,乙),
h(李,丙),
])
console.log(虚拟节点)
对应的虚拟DOM:
H函数生成的虚拟DOM就是这个JS对象(Vnode)的源代码:
导出接口VNodeData {
道具?道具
attrs?属性
班级?类别
风格?VNodeStyle
数据集?数据集
开?开
英雄?英雄
attachData?AttachData
钩子?挂钩
钥匙?键
ns?字符串//用于SVG
fn?()=VNode //for thunks
args?any[] //for thunks
[key: string]: any //用于任何其他第三方模块
}
导出类型键=字符串数字
常量接口VNode={
Sel:字符串未定义,//选择器
Data: VNodeData undefined,//vnodedata上面定义的VNodeData
children:array vnode string undefined,//子节点,与文本互斥。
Text: string undefined,//标签中间的文本内容
Elm: Node undefined,//转换的真实DOM
Key: Key undefined //字符串或数字
}
补充:上面的H函数你可能有点熟悉,但我已经有一段时间没想起来了。没关系。我会帮你记起来的。
开发中常见的真实场景,渲染函数渲染:
//案例1在vue项目中创建main.js的Vue实例
新Vue({
路由器,
店,
render: h=h(App)
}).$ mount( # app );
//在情况2列表中使用render。
列:[
{
标题:“操作”,
按键:“动作”,
宽度:150,
render: (h,params)={
返回h(div ,[
h(按钮,{
道具:{
尺寸:“小”
},
风格:{
margin right:“5px”,
边缘底部:“5px”,
},
开:{
点击:()={
this . toedit(params . row . uuid);
}
}
},编辑)
]);
}
}
]
为什么要使用虚拟DOM:
MVVM框架解决了视图和状态同步的问题。
模板引擎可以简化视图操作,没有办法跟踪状态。
虚拟DOM跟踪状态变化。
参考github上virtual-dom的动机描述。
虚拟DOM可以维护程序的状态,跟踪最后的状态,通过比较前后两种状态的差异来更新真实DOM的跨平台使用。
浏览器渲染DOM,服务器渲染SSR(Nuxt.js/Next.js)。前端是vue,后者是React Native应用(Weex/React Native)小程序(mpvue/uni-app)。真正的DOM属性有很多,创建DOM节点的代价也很大。
虚拟DOM只是一个普通的JavaScript对象,不需要很多属性来描述,创建成本很小。
提高复杂视图中的渲染性能(操作dom会消耗大量性能,缩小操作dom的范围可以提高性能)
灵魂问:用虚拟DOM一定比直接渲染真实DOM快吗?
答案当然是否定的,听我说:
示例:节点发生变化时的DOMB多莫比
上述情况:
示例1:创建一个DOMB,然后替换DOMB例2:创建一个虚拟的DOM DIFF算法,发现DOMB和DOMA不是同一个节点,最后,创建一个DOMB,替换DOMA;可以清楚的看到1更快,同样的结果,2要创建一个虚拟的DOM DIFF来计算比较。
所以说使用虚拟DOM比直接操作真实DOM更快是错误的,也是不严谨的。
例如,当DOM树中子节点的内容发生变化时:
当一些复杂的节点,比如一个父节点,有多个子节点,只有一个子节点的内容发生了变化,那么我们就不需要像例1那样重新渲染DOM树了。这时候虚拟DOM DIFF算法就能很好的体现出来。我们可以使用示例2中的虚拟DOM Diff算法找出发生变化的子节点并更新其内容。
总结:在复杂视图的情况下渲染性能得到提升,是因为虚拟DOM Diff算法可以准确找到DOM树的变化位置,减少DOM操作(重排和重绘)。
虚拟dom库
小吃店
Vue.js2.x使用的虚拟dom是修改后的Snabbdom的200SLOC(单行代码)左右,是TypeScript通过模块可扩展源代码开发的最快的虚拟DOM之一。
diff算法
看了上面的文章,相信大家对Diff算法有了初步的概念。是的,Diff算法其实就是找出它们之间的区别。
首先要明确diff算法的概念,即Diff的对象是虚拟DOM,更新真实DOM是Diff算法的结果。
下面,我将撕掉snabbdom源代码的核心部分,为大家打开Diff的心扉。
snabbdom的核心
Init()设置模块。创建patch()函数。使用h()函数创建JavaScript对象(Vnode)来描述真实的DOMpatch()比较新旧Vnode,将变化的内容更新到真实的DOM树中。
init函数
当初始化函数时,设置模块,然后创建patch()函数。先通过一个场景案例有个直观的体现:
从“snabbdom/build/package/init.js”导入{init}
从“snabbdom/build/package/h.js”导入{h}
//1.导入模块
从“snabbdom/build/package/modules/style”导入{ style module };
从“snabbdom/build/package/modules/event listeners”导入{ event listeners module };
//2.注册模块
常量补丁=init([
样式模块,
eventListenersModule模块
])
//3.使用h()函数的第二个参数传入模块中使用的数据(对象)。
设vnode=h(div ,[
h(h1 ,{ style:{ background color: red } }, Hello world ),
h(p ,{on: {click: eventHandler}}, Hello P )
])
函数eventHandler() {
警报(“好痛,别碰我”)
}
const app=document . query selector(# app)
补丁(应用程序、虚拟节点)
init使用导入的模块时,可以用这些模块提供的api在H函数中创建虚拟DOM(Vnode)对象;在上面,样式模块和事件模块用于使创建的虚拟dom具有样式属性和事件属性。最后通过patch函数比较两个虚拟DOM(app会先转换成虚拟DOM)来更新视图;
让我们简单看一下init的源代码部分:
//src/package/init.ts
/*第一个参数是每个模块。
第二个参数是DOMAPI,可以把DOM转换成其他平台的API。
也就是说支持跨平台使用。不传输时,默认为htmlDOMApi。见下文。
Init是高阶函数,一个函数返回另一个函数,这个函数可以缓存两个参数,modules和domApi。
然后,将来直接传递oldValue和newValue(vnode)就可以了*/
导出函数init(模块:ArrayPartialModule,domApi?DOMAPI) {
.
返回函数patch (oldVnode: VNode Element,vnode: VNode): VNode {}
}
h函数
有些地方还会以createElement命名。它们是同一个东西,它们都创建虚拟DOM。在上面的文章中,相信大家对H函数已经有了初步的了解,也已经关联了使用场景,就不介绍场景案例了,直接上源代码部分:
//h函数
导出函数h (sel: string): VNode
导出函数h (sel: string,data: VNodeData null): VNode
导出函数h (sel: string,children: VNodeChildren): VNode
导出函数h (sel: string,data: VNodeData null,children: VNodeChildren): VNode
导出函数h (sel: any,b?任何,c?any): VNode {
var数据:VNodeData={}
var儿童:任何
var文本:任何
var i:数量
.
Return vnode (sel,data,children,text,undefined)//最后返回一个vnode函数
};
//vnode函数
导出函数vnode (sel: string undefined,
数据:任何未定义,
children:array vnode string undefined,
text: string 未定义,
elm:Element Text undefined):VNode {
const key=data===未定义?未定义:data.key
Return {sel,data,children,text,elm,key }//最终生成Vnode对象
}
总结:H先生函数变成了vnode函数,然后vnode函数生成了一个Vnode对象(虚拟DOM对象)
补充:
在H函数源代码部分,涉及到一个函数重载的概念,简单解释一下:
参数个数或类型不同的函数()JavaScript中没有重载。TypeScript中存在重载,但重载的实现与通过代码调整实现参数重载的概念有关,与返回值无关。
示例1(函数重载-参数数量)
函数add(a:数字,b:数字){
console.log(a b)
}
函数add(a:数字,b:数字,c:数字){
console.log(a b c)
}
加法(1,2)
加法(1,2,3)
示例2(函数重载-参数类型)
函数add(a:数字,b:数字){
console.log(a b)
}
函数add(a:数字,b:字符串){
console.log(a b)
}
加法(1,2)
添加(1, 2 )
patch函数(核心)
如果看了前面的伏笔,看到这里可能会走神。醒醒吧,这里是核心,你在高地上,兄弟。
pa tch(old vnode,newVnode)将新节点中发生变化的内容渲染到真实的DOM中,最后将新节点作为旧节点(core)返回,进行下一步处理。比较新旧VNodes是否是相同的节点(节点的key和sel相同)。如果不是同一个节点,删除之前的内容,重新渲染,然后判断新的VNode是否有文本。如果有,并且与oldVnode的文本不同,则直接更新文本内容(patchVnode)。如果新的VNode有子节点,判断子节点是否有变化(updateChildren,最麻烦最难实现)。源代码:
返回函数patch(oldVnode: VNode Element,vnode: VNode): VNode {
假设I:数字,elm:节点,父节点
const insertedVnodeQueue:VNodeQueue=[]
//cbs.pre是所有模块的pre钩子函数的集合。
for(I=0;I CBS . pre . length;cbs.pre[i]()
//isVnode函数确定oldVnode是否为虚拟DOM对象。
如果(!isVnode(oldVnode)) {
//如果不是,则将元素转换为虚拟DOM对象
oldVnode=emptyNodeAt(oldVnode)
}
//sameVnode函数用于判断两个虚拟DOM是否相同。源代码见补充1;
if (sameVnode(oldVnode,vnode)) {
//如果相同,运行patchVnode比较两个节点,后面会突出显示(核心)。
patchVnode(oldVnode,Vnode,insertedVnodeQueue)
}否则{
elm=oldVnode.elm!//!是ts的一种写法。代码oldVnode.elm必须有一个值
//parentNode是获取父元素。
parent=api.parentNode(elm)作为节点
//createElm用于创建要插入vnode的dom元素(新的虚拟DOM)
createElm(vnode,insertedVnodeQueue)
如果(家长!==null) {
//将dom元素插入父元素,删除旧的dom。
api.insertBefore(parent,vnode.elm!api.nextSibling(elm))//将新创建的元素放在旧的dom后面
removeVnodes(parent,[oldVnode],0,0)
}
}
for(I=0;我插入了insertedVnodeQueue.lengthi) {
insertedVnodeQueue[i]。数据!钩子!插入!(insertedVnodeQueue[i])
}
for(I=0;I CBS . post . length;哥伦比亚广播公司邮报
返回vnode
}
patchVnode
第一阶段触发prepatch函数和update函数(prepatch函数都会被触发,update函数只有在不相同的情况下才会被触发)。第二阶段真正比较新旧VNODes之间的差异。第三阶段触发postpatch函数来更新节点源代码:
函数patchVnode(oldVnode: VNode,Vnode: VNode,insertedVnodeQueue:VNode queue){
const hook=vnode.data?钩
钩子?预修补?(oldVnode,Vnode)
const elm=vnode . elm=old vnode . elm!
const old ch=old VNode . children as VNode[]
const ch=VNode . children as VNode[]
if (oldVnode===vnode)返回
if (vnode.data!==未定义){
for(设I=0;I CBS . update . length;cbs.update[i](oldVnode,Vnode)
vnode.data.hook?更新?(oldVnode,Vnode)
}
If (isUndef(vnode.text)) {//新节点的text属性未定义
If (isDef(oldCh) isDef(ch)) {//当新旧节点都有子节点时
如果(oldCh!==ch) updateChildren (elm,oldch,ch,insertdvnodequeue)//及其子节点不同。将执行updatechildren函数,这将在后面突出显示(核心)
} else if (isDef(ch)) {//只有新节点有子节点。
//旧节点有了text属性,就会把给真正dom的text属性。
if(isDef(old vnode . text))API . settext content(elm,)
//并将新节点的所有子节点插入到真正的dom中
addVnodes(elm,null,ch,0,ch.length - 1,insertedVnodeQueue)
} else if (isDef(oldCh)) {//清除真实dom的所有子节点。
remove nodes(elm,oldCh,0,oldCh.length - 1)
} else if(isdef(old vnode . text)){//将“”赋予真实dom的text属性
api.setTextContent(elm,)
}
} else if (oldVnode.text!==vnode.text) {//如果旧节点的文本与新节点的文本不同
If (isDef(oldCh)) {//如果旧节点有子节点,则删除所有子节点。
remove nodes(elm,oldCh,0,oldCh.length - 1)
}
api.setTextContent(elm,vnode.text!)//把新节点的文本给真正的dom
}
钩子?postpatch?(oldVnode,vnode) //更新视图
}
可能有点猝不及防。这是另一个思维导图:
diff算法简介
传统diff算法
虚拟DOM中的Diff算法传统算法求两棵树每个节点的差值,会运行N1(DOM 1中的节点数)* N2(节点数*n2(dom2)进行比较,然后更新差值。
snabbdom的diff算法优化
Snbbdom根据dom的特点对传统的diff算法进行了优化。DOM操作时,很少跨级别操作节点,只比较同一级别的节点。
接下来我们将介绍updateChildren函数如何比较子节点的异同,这也是diff算法中的一个核心和难点;
updateChildren(核中核:判断子节点的差异)
这个函数分为三个部分,第一部分:声明变量,第二部分:比较同级节点,第三部分:完成循环(见下图);
与同级节点相比的五种情况:
OldStartVnode/newStartVnode(旧开始节点/新开始节点)相同的oldEndVnode/新结束节点(旧开始节点/新结束节点)相同的旧结束Vnode/相同的特殊情况newStartVnode(旧结束节点/新开始节点)当情况1、2、3、4都不满足要求时执行。在oldVnodes中找到与newStartVnode相同的节点,并将其移动到oldStartVnode中。如果没有找到,那么在oldStartVnode上创建一个执行进程就是一个循环。在每个循环中,一旦执行了上述五种情况中的一种,循环就结束了。
循环结束工作:直到oldstartidoldendedx newstartidxnewendidx(表示旧节点或新节点已被遍历)
为了更直观的理解,我们来看一下五种情况与同级节点相比的实现细节:
新开始节点和旧开始节点(情况1)
如果情况1满足要求:(从起始节点开始比较新旧节点,oldCh[oldStartIdx]和newCh[newStartIdx]判断是否是相同的节点(key和sel相同),然后执行patchVnode找出两者的区别,更新图;如果没有区别,什么都不做,结束oldStartIdx /newStartIdx的一个循环。
新结束节点和旧结束节点(情况2)
如果案例1不符合,则判断案例2。如果是:(比较新旧节点的旧端节点,比较oldCh[oldEndIdx]和newCh[newEndIdx],执行sameVnode(key和sel相同)判断是否是相同的节点。)执行patchVnode,找出两者的区别,更新视图。如果没有差异,则什么也不做,并结束一个循环
旧开始节点/新结束节点(情况3)
如果情况1和情况2都不匹配,尝试情况3:(旧节点的起始节点开始与新节点的结束节点进行比较,比较oldch [oldstuddx]和newendidx],执行sameVnode(key和sel相同)判断是否是相同的节点。)执行patchVnode以找出它们之间的差异并更新视图。如果没有区别,什么都不会做。一个周期结束后,OLDCH [OLDSTIDX]对应的实dom移位到oldCh[oldEndIdx],OLDSTIDX/news iddx-;
旧结束节点/新开始节点(情况4)
如果情况1、2、3不匹配,尝试情况4:(旧节点的结束节点开始与新节点的开始节点oldCh[oldEndIdx]和newCh[newStartIdx]进行比较,执行sameVnode(key和sel相同)判断是否是相同的节点),然后执行patchVnode找出两者的区别,更新视图。如果没有区别,什么都不做,一旦循环结束,oldCh[oldEndIdx]对应的实dom就移位到OLDCH [old stand idx]对应的实dom之前的旧end idx—/new start idx;
新开始节点/旧节点数组中寻找节点(情况5)
从旧节点开始搜索。如果找到与newCh[newStartIdx]相同的节点(并称之为对应节点[1]),执行patchVnode找出两者的区别,更新视图,如果没有区别,什么都不做,结束一个循环。
对应于相应节点[1]的真实dom被移动到对应于oldch [oldstatidx]的真实dom的前面。
如果没有找到相同的节点,则创建一个对应于newCh[newStartIdx]节点的真实dom,并将其插入到对应于oldch [oldstitdx]的真实dom的前面。
newStartIdx
下面介绍一下结束循环的收尾工作(oldstartidoldendedx newstartidxnewendidx):
首先遍历新节点的所有子节点(newStartIdxnewEndIdx),然后循环结束。
当遍历新节点的所有子节点时,删除不对应于同一节点的子节点。
首先遍历旧节点的所有子节点(oldStartIdxoldEndIdx),循环结束。
旧节点的所有子节点的遍历结束在额外的子节点被插入到旧节点的结束节点之前;(源代码:newch [newendix1]。elm),这是对应的旧端节点的真实DOM。Newendix1在匹配同一个节点时需要-1,所以需要加回作为结束节点。
最后,附上源代码:
函数updateChildren(parentElm,oldCh,newCh,insertedVnodeQueue) {
设old startidx=0;//旧节点开始节点索引
设new start idx=0;//新节点的开始节点索引
设olden didx=old ch . length-1;//旧节点的结束节点索引
设oldStartVnode=old ch[0];//旧节点启动节点
设oldEndVnode=oldCh[olden didx];//旧节点的结束节点
设newEndIdx=newch . length-1;//新节点的结束节点索引
设newStartVnode=newCh[0];//新节点启动该节点
设newEndVnode=newCh[newEndIdx];//新节点结束节点
让oldKeyToIdx//节点移动相关性
让idxInOld//节点移动相关性
让elmToMove//节点移动相关性
让在前面;
//与同级节点进行比较
while(old startidx=olden didx newStartIdx=newEndIdx){
if (oldStartVnode==null) {
oldStartVnode=old ch[old startidx];//Vnode可能已经向左移动
}
else if (oldEndVnode==null) {
oldEndVnode=old ch[-olden didx];
}
else if (newStartVnode==null) {
newStartVnode=newCh[newStartIdx];
}
else if (newEndVnode==null) {
newEndVnode=newCh[-newEndIdx];
}
Else if (same vnode (oldstartvnode,new start vnode)){//判断情况1
patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue);
oldStartVnode=old ch[old startidx];
newStartVnode=newCh[newStartIdx];
}
Else if(相同vnode(旧结束vnode,新结束vnode)) {//Case 2
补丁程序节点(oldEndVnode、newEndVnode、insertedvnodequeue);
oldendvnode=old ch[-oldenddx]:
newendvnode=newch[-newenddx]:
}
else if(samvnode(old start vnode,newendvnode)){//vnode右移情况3
补丁程序节点(oldStartVnode、newEndVnode、insertedvnodequeue);
api.insertBefore(parentElm、oldStartVnode.elm、api。nextsibling(oldendvnode。elm));
旧开始vnode=旧ch[旧开始idx];
newendvnode=newch[-newenddx]:
}
else if(samvnode(oldendvnode,newstartvnode)){//vnode左移情况四
补丁程序节点(oldEndVnode、newStartVnode、insertedvnodequeue);
api.insertBefore(parentElm,oldEndVnode.elm,old startvnode。榆树);
oldendvnode=old ch[-oldenddx]:
news tartvnode=newch[news tartidx];
}
else { //情况5
if(old keytodx===undefined)>
old keytodx=createkeytool dix(old ch、oldStartIdx、oldenddx);
}
idxinold=old keytodx[news tartvnode。键];
if(idxinold)){//新元素//创建新的节点在旧节点的新节点前
api.insertBefore(parentElm,createElm(newStartVnode,insertedVnodeQueue),old startvnode。榆树);
}
其他
埃尔莫托沃=奥尔德什[idxinold];
如果(elmtomove。sel!==newStartVnode.sel) { //创建新的节点在旧节点的新节点前
api.insertBefore(parentElm,createElm(newStartVnode,insertedVnodeQueue),old startvnode。榆树);
}
其他
//在旧节点数组中找到相同的节点就对比差异更新视图,然后移动位置
补丁程序节点(elmtomove、newStartVnode、insertedvnodequeue);
奥德奇[idxinold]=未定义;
api.insertBefore(parentElm,elmtomove。榆树,老startvnode。榆树);
}
}
news tartvnode=newch[news tartidx];
}
}
//循环结束的收尾工作
if(old start idx=oldenddx news startidx=newenddx)>
if(old startidx oldenddx)>
//newch[newenddx 1].榆树!榆树就是旧节点数组中的结束节点对应的多姆元素
//newenddx 1是因为在之前成功匹配了纽恩迪高级的(deluxe的简写)需要-1号
//newch[newenddx 1].榆树!榆树!因为已经匹配过有相同的节点了,它就是等于旧节点数组中的结束节点对应的多姆元素(oldch[oldenddx 1]。(榆树)
before=newch[newenddx 1]==null?空值:newendidx 1 .榆树!榆树!
//把新节点数组中多出来的节点插入到在此之前前
addvnnodes(父elm,before,newCh,newStartIdx,newendidx,insertedvnodequeue);
}
其他
//这里就是把没有匹配到相同节点的节点删除掉
removevnnodes(父elm、oldCh、oldStartIdx、oldenddx);
}
}
}
key的作用
差异(差异)操作可以更加快速;差异(差异)操作可以更加准确;(避免渲染错误)不推荐使用索引作为关键点以下我们看看这些作用的实例:
Diff操作可以更加准确;(避免渲染错误):
实例:a、b、c三个多姆元素中的乙、丙间插入一个z轴元素
没有设置关键点
当设置了关键点:
Diff操作可以更加准确;(避免渲染错误)
实例:a、b、c三个多姆元素,修改了一个a元素的某个属性再去在一个a元素前新增一个z轴元素
没有设置关键点:
因为没有设置关键点,默认都是未定义(未定义),所以节点都是相同的,更新了文字(吨)的内容但还是沿用了之前的多姆,所以实际上a-z(a原本打勾的状态保留了,只改变了文本),b-a,c-b,d-c,遍历完毕发现还要增加一个多姆,在最后新增一个文字(吨)为d。非政府组织的多姆元素
设置了关键点:
当设置了关键点,甲,乙,丙,丁都有对应的钥匙a-a,b-b,c-c,d-d,内容相同无需更新,遍历结束,新增一个文字(吨)为z轴的多姆元素
不推荐使用索引作为key:
设置索引为关键点:
这显然是低效的。我们只想找出不同的节点更新,使用索引作为关键字会增加操作时间。我们可以通过将键设置为与节点文本一致来解决这个问题:
以上是vue中虚拟DOM和Diff算法知识的详细内容。更多关于vue虚拟DOM和Diff算法的信息请关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。