You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functionupdateChildren(parentElm: Node,oldCh: Array<VNode>,newCh: Array<VNode>,insertedVnodeQueue: VNodeQueue){letoldStartIdx=0,newStartIdx=0;letoldEndIdx=oldCh.length-1;letoldStartVnode=oldCh[0];letoldEndVnode=oldCh[oldEndIdx];letnewEndIdx=newCh.length-1;letnewStartVnode=newCh[0];letnewEndVnode=newCh[newEndIdx];letoldKeyToIdx: any;letidxInOld: number;letelmToMove: VNode;letbefore: any;while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(oldStartVnode==null){oldStartVnode=oldCh[++oldStartIdx];// Vnode might have been moved left}elseif(oldEndVnode==null){oldEndVnode=oldCh[--oldEndIdx];}elseif(newStartVnode==null){newStartVnode=newCh[++newStartIdx];}elseif(newEndVnode==null){newEndVnode=newCh[--newEndIdx];}elseif(sameVnode(oldStartVnode,newStartVnode)){patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue);oldStartVnode=oldCh[++oldStartIdx];newStartVnode=newCh[++newStartIdx];}elseif(sameVnode(oldEndVnode,newEndVnode)){patchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue);oldEndVnode=oldCh[--oldEndIdx];newEndVnode=newCh[--newEndIdx];}elseif(sameVnode(oldStartVnode,newEndVnode)){// Vnode moved rightpatchVnode(oldStartVnode,newEndVnode,insertedVnodeQueue);api.insertBefore(parentElm,oldStartVnode.elmasNode,api.nextSibling(oldEndVnode.elmasNode));oldStartVnode=oldCh[++oldStartIdx];newEndVnode=newCh[--newEndIdx];}elseif(sameVnode(oldEndVnode,newStartVnode)){// Vnode moved leftpatchVnode(oldEndVnode,newStartVnode,insertedVnodeQueue);api.insertBefore(parentElm,oldEndVnode.elmasNode,oldStartVnode.elmasNode);oldEndVnode=oldCh[--oldEndIdx];newStartVnode=newCh[++newStartIdx];}else{if(oldKeyToIdx===undefined){oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx);}idxInOld=oldKeyToIdx[newStartVnode.keyasstring];if(isUndef(idxInOld)){// New elementapi.insertBefore(parentElm,createElm(newStartVnode,insertedVnodeQueue),oldStartVnode.elmasNode);newStartVnode=newCh[++newStartIdx];}else{elmToMove=oldCh[idxInOld];if(elmToMove.sel!==newStartVnode.sel){api.insertBefore(parentElm,createElm(newStartVnode,insertedVnodeQueue),oldStartVnode.elmasNode);}else{patchVnode(elmToMove,newStartVnode,insertedVnodeQueue);oldCh[idxInOld]=undefinedasany;api.insertBefore(parentElm,(elmToMove.elmasNode),oldStartVnode.elmasNode);}newStartVnode=newCh[++newStartIdx];}}}if(oldStartIdx<=oldEndIdx||newStartIdx<=newEndIdx){if(oldStartIdx>oldEndIdx){before=newCh[newEndIdx+1]==null ? null : newCh[newEndIdx+1].elm;addVnodes(parentElm,before,newCh,newStartIdx,newEndIdx,insertedVnodeQueue);}else{removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx);}}}
前言:虚拟DOM的深入学习是我在学习Vue源码过程中进行的(虽然只是学了皮毛);但是在此过程中我发现其虚拟DOM实现借鉴于
snabbdom
,所以大部分借鉴于vue,及snabbdom
,但同时存在些许异同 在说虚拟DOM之前,思考一下Jquery主要是用来干啥的,讲道理我并没有深入用过Jquery,主要得益于现代HTML,及Js的发展,使的大部分Jquery功能是完全可以用原生来实现的,但是Jquery在当时是划时代的出现,他抹平了各浏览器的差异,手动操作DOM更加快速,方便。
随着前端应用越来越复杂,手动操作DOM也越来越复杂,而且总的来说,整个过程还是从后端请求到数据后,根据数据决定如何去渲染操作DOM,那么能不能通过函数直接将数据渲染为DOM,而我们只需要关心数据呢?这就是Vue/React这类框架的思想,创建数据到UI的映射函数来实现只需要关心数据,自动生成UI,这个其实也就是早就已经有的模板引擎的功能,那么React/Vue跟早就已有的模板引擎有什么区别呢?
首先来说,我们简单的想一下,当我们要手动创建一个DOM节点时需要什么?
这样的话我们可以简单的通过一个Javascript对象来表示将要创建的DOM节点
通过上面的
createElm
函数我们可以将一个JS对象树转为真实的DOM树,那么接下来我们要变更数据,然后将变更体现到真实的UI上,最简单的方式就是清空之前的所有DOM重新调用createElm
进行渲染,如果如此做的话那么首先会有很严重的性能问题,重排重绘,JS与DOM通信都是耗时耗性能的,所以这个时候就需要diff
算法了,很多文章在说到虚拟DOM的时候都说通过JS模拟DOM结构所以性能好,我觉得其实还是有一些不严谨的,应该是diff
算法使得其避免了很多不必要的DOM操作,所以性能好,那么diff
算法到底是如何?在做对比的时候,如果我们逐一对比会花费大量的时间,传统的
diff
算法复杂度为O(n^3)而React的工程师做了大胆假设,DOM的更改是极少会出现跨层级的移动的,所以如果我们完全忽略这种情况,只去对比同层的树节点,就可以节省大量的时间,使得时间复杂度只有O(n)
看一下
snabbdom
的patch
首先
patch
会对新旧两个VNode
节点对比key
,在snabbdom
及Vue
中还会判断其他的属性,如果通过则判定当前的旧节点可以复用,进去patchVnode
函数进行具体的复用对比,如果不通过则删除旧节点,调用createElm
函数渲染新Vnode
节点,然后插入到父节点,看一下
patchVnode
的代码从代码中来看
patchVnode
核心规则主要如下children
则调用updateChildren
来对子节点进行diff
children
而新节点存在,则清空挂载DOM,然后渲染新节点children
children
而新节点不存在,则调用removeVnodes
清空老节点所有子节点children
时从上面的代码中可以看出,
updateChildren
是整个patch
最核心的函数,就是这个函数中所运用的diff
算法,使得整个节点复用效率大大提高,看一下updateChildren
在
updateChildren
中snabbdom
使用了双索引对比,新旧节点同时维护前后两个索引,然后像中间进发,逐个对比,在每次对比时会存在四个节点
newStartVnode
,newEndVnode
,oldStartVnode
,oldEndVnode
两两对比两两对比,基本会有几种方式
oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
限制条件时newStartVnode
,oldStartVnode
符合sameVnode
时,调用patchVnode
函数对比两节点,同时新旧头索引右移newEndVnode
,oldEndVnode
符合sanmeVnode
时,调用patchVnode
函数对比两节点,同事新旧尾索引左移newStartVnode
,oldEndVnode
符合sanmeVnode
时,调用patchVnode
函数对比两节点,然后将oldEndVnode
节点挂载DOM移动至oldStartVnode
节点挂载DOM前方,以对应newStartVnode
的位置,之后新节点头索引右移,旧节点尾索引左移newEndVnode
与oldStartVnode
符合sanmeVnode
是,调用patchVnode
函数对比两节点,然后将oldStartVnode
节点挂载DOM移动至oldEndVnode
节点挂载DOM之后,然后新节点尾索引左移,旧节点头索引右移vnode
的key
值创建map
按照newStartVnode.key
进行匹配,如果匹配成功则进行对比操作,否则调用createElm
渲染newStartVnode
节点while
的限制条件时oldStartIdx > oldEndIdx
则说明新节点的子节点中存在旧节点不存在的节点,调用addVnodes
将新增节点逐一插入至newCh[newEndIdx+1]
前方,因为此时newCh[newEndIdx+1]
节点已匹配完成,oldStartIdx < oldEndIdx
则说明新节点删除了部分旧节点中存在的子节点,调用removeVnodes
从oldStartIdx
到oldEndIdx
逐一删除.从上面的
diff
过程中我们可以反向理解为什么在React/Vue这一类框架中推荐为列表项绑定key
值,同时不推荐使用index
作为key
值了,如果我们使用
index
作为key
的值,不管数组内数据如何变更,其渲染的子节点key
值都是从0
开始增长的,就无法起到通过sameVnode
函数来确定是否可以复用,大大增加了diff
算法的运行时间,而不绑定key
值则有可能导致大量本可以被复用的节点被移除后重新创建,影响性能The text was updated successfully, but these errors were encountered: