1987WEB视界-分享互联网热点话题和事件

您现在的位置是:首页 > WEB开发 > 正文

WEB开发

vue虚拟dom和diff算法

1987web2024-03-25WEB开发40
vue的虚拟dom和diff算法1.虚拟dom虚拟dom,我的理解就是通过js对象的方式来具体化每一个节点,把dom树上面的每个节点都变为对象里的一个元素,元素的子元素

vue的虚拟dom和diff算法 1.虚拟dom 虚拟dom,我的理解就是通过js对象的方式来具体化每一个节点,把dom树上面的每个节点都变为对象里的一个元素,元素的子元素变为子节点,节点上面的class、id、attribute等属性变为data内的值,然后通过dom上面的createElemen ...

vue的虚拟dom和diff算法

1.虚拟dom

虚拟dom,我的理解就是通过js对象的方式来具体化每一个节点,把dom树上面的每个节点都变为对象里的一个元素,元素的子元素变为子节点,节点上面的class、id、attribute等属性变为data内的值,然后通过dom上面的createElement、appendChild、insertBefore等方法进行生成dom树。

letVNode={sel:div,data:{key:0,props:{},attrs:{},class:{},style:{},fn:{}},text:虚拟dom,elm:
虚拟dom
children:[{sel:div,data:{key:0,props:{},attrs:{},class:{},style:{},fn:{}},text:虚拟dom children,elm:
虚拟dom children
children:[]}]}

2.diff算法

看了diff算法后感觉写的真是巧妙,真正做到了最小量更新 。

diff是当父节点相同时用来对子节点进行最小量更新的算法。

diff算法采用四个指针:旧节点开始指针,旧节点结束指针,新节点开始指针,新节点结束指针;

(上方虚拟节点中的key就是为了在进行diff算法时判断是否是同一个节点便于最小量更新)

while(旧节点开始指针<=旧节点结束指针&&新节点开始指针<=新节点结束指针){

分为以下五种情况:(前四种情况)

当进行下面5种判断后可能会出现新节点[新节点开始指针]旧节点[旧节点开始指针]新节点[新节点结束指针]旧节点[旧节点结束指针]为空值的情况,如果出现空值则代表当前节点已经处理过了,所以就需要将指针++或者--

if(旧节点[旧节点开始指针] ==null){

旧节点开始指针++

}else if(旧节点[旧节点结束指针]==null){

旧节点结束指针--

}else if(新节点[新节点开始指针] ==null){

新节点开始指针++

}else if(新节点[新节点结束指针] ==null){

新节点结束指针--

}

1、新节点[新节点开始指针]对比旧节点[旧节点开始指针]

如果符合此种情况,则代表新节点[新节点开始指针]旧节点[旧节点开始指针]为同一个节点,实行最小量更新,即只更新节点内的属性而不进行dom销毁创建操作,完成更新后新节点开始指针++旧节点开始指针++

2、新节点[新节点结束指针]对比旧节点[旧节点结束指针]

如果符合此种情况,则代表新节点[新节点结束指针]旧节点[旧节点结束指针]为同一个节点,实行最小量更新,即只更新节点内的属性而不进行dom销毁创建操作,完成更新后新节点结束指针--旧节点结束指针--

3、新节点[新节点结束指针]对比旧节点[旧节点开始指针]

如果符合此种情况,则代表新节点[新节点结束指针]旧节点[旧节点开始指针]为同一个节点,实行最小量更新,先更新节点内的属性,然后使用insertBefore将旧节点[旧节点开始指针]移动到旧节点[旧节点结束指针]之后,(注意:此处要移动到旧节点[旧节点结束节点]后,而不是所有旧节点后,因为这里的旧节点结束指针是会变化的),

父节点.insertBefore(旧节点[旧节点开始指针].elm,旧节点[旧节点结束指针].elm.nextSibling)

完成操作后新节点结束指针--旧节点开始指针++

4、新节点[新节点开始指针]对比旧节点[旧节点结束指针]如果符合此种情况,则代表新节点[新节点开始指针]旧节点[旧节点结束指针]为同一个节点,实行最小量更新,先更新节点内的属性,然后使用insertBefore将旧节点[旧节点结束指针]移动到旧节点[旧节点开始指针]前,(注意:此处要移动到旧节点[旧节点开始指针]前,而不是所有旧节点前,因为旧节点开始指针也是会发生变化的)

父节点.insertBefore(旧节点[旧节点结束指针].elm,旧节点[旧节点开始指针].elm)

完成操作后,旧节点结束指针--新节点开始指针++

5、遍历旧节点数组,生成一个以key为键,index为值的对象为旧节点keyIndexMap,然后查询新节点[新节点开始指针]中的key是否在旧节点keyIndexMap中存在;

如果不存在,则证明新节点[新节点开始指针]在旧节点列表中不存在,此时需要创建新节点[新节点开始指针]为真实dom,并将其插入至旧节点[旧节点开始指针]前(因为此时新节点[新节点开始指针]一定处于全部未处理的旧节点前)

父节点.insertBefore(创建dom(新节点[新节点开始指针]),旧节点[旧节点开始指针].elm)

如果存在则先需要判断旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]新节点[新节点开始指针]的sel(标签)是否相同:

如果相同则代表为同一个标签,则进行最小量更新,先更新节点内的属性,然后insertBefore将旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]移动到旧节点[旧节点开始指针]前,然后将旧节点[旧节点keyIndexMap[新节点[新节点开始指针][key]]]设置为undefined,代表当前节点处理过了;

如果不同则代表不是同一个标签,则只创建新节点[新节点开始指针]的真实dom,然后将其插入到旧节点[旧节点开始节点]

最后新节点开始指针++

}

当以上循环完成后可能还会出现没有处理到的节点,所以还需要再查找没有处理到的节点:

如果是新节点开始指针<=新节点结束指针,则代表新节点列表内还有没有处理的节点,没有处理的节点全部为新增节点,此时需要遍历新节点[新节点开始指针](包含)至新节点[新节点结束指针](包含)之间的节点,然后将其添加至新节点[新节点结束指针+1]之前(新节点[新节点结束指针+1]可能为空,新节点[新节点结束指针+1]为空时可添加到最后)

for(leti=新节点开始节点;i<=新节点结束节点;i++){//insertBefore可以自动识别空值,如果是空值,则插入到最后父节点.insertBefore(创建dom(新节点[i]),新节点[新节点结束节点-1]?.elm)}

如果是旧节点开始指针<=旧节点结束指针,则代表旧节点内还有没有处理的节点,没有处理的节点全部为需要删除节点,此时需要遍历旧节点[旧节点开始指针](包含)至旧节点[旧节点结束指针](包含) 之间的节点,然后将其全部删除。

for(leti=旧节点开始指针;i<=旧节点结束指针;i++){旧节点[i]&&(父节点.removeChild(旧节点[i].elm))}

以上就是我对diff算法的理解,下面贴上代码(阉割版,部分情况没有考虑,旨在学习diff算法,可能会有bug):

//updateChildren文件import{sameVnode}from./isimportpatchVnodefrom./patchVnodeimportcreateElementfrom./createElementexportdefaultfunctionupdateChildren(parentElm,oldCh,newCh){console.log(updateChildren)console.log(parentElm,oldCh,newCh)//旧前letoldStartIdx=0//新前letnewStartIdx=0//旧后letoldEndIdx=oldCh.length-1//新后letnewEndIdx=newCh.length-1//旧节点letoldStartVnode=oldCh[0]//旧后节点letoldEndVnode=oldCh[oldEndIdx]//新节点letnewStartVnode=newCh[0]//新后节点letnewEndVnode=newCh[newEndIdx]letkeyMap={}// 开始循环while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){// debuggerconsole.log(while)if(oldStartVnode===undefined||oldCh[oldStartIdx]===undefined){oldStartVnode=oldCh[++oldStartIdx]}elseif(oldEndVnode===undefined||oldCh[oldEndIdx]===undefined){oldEndVnode=oldCh[--oldEndIdx]}elseif(newStartVnode===undefined||newCh[newStartIdx]===undefined){newStartVnode=newCh[++newStartIdx]}elseif(newEndVnode===undefined||newCh[newEndIdx]===undefined){newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newStartVnode)){//新前和旧前是同一个节点console.log(新前和旧前是同一个节点)patchVnode(oldStartVnode,newStartVnode)oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]}elseif(sameVnode(oldEndVnode,newEndVnode)){//旧后和新后是同一个节点console.log(旧后和新后是同一个节点)patchVnode(oldEndVnode,newEndVnode)oldEndVnode=oldCh[--oldEndIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newEndVnode)){//新后和旧前是同一个节点console.log(新后和旧前是同一个节点)patchVnode(oldStartVnode,newEndVnode)//当新后节点是旧前节点时,此时需要移动节点,移动旧前节点到旧后的后面parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling)oldStartVnode=oldCh[++oldStartIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldEndVnode,newStartVnode)){//旧后和新前是同一个节点console.log(旧后和新前是同一个节点)// 当旧后和新前是同一个节点时,此时需要移动旧后节点到旧前节点的前面patchVnode(oldEndVnode,newStartVnode)parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm)oldEndVnode=oldCh[--oldEndIdx]newStartVnode=newCh[++newStartIdx]}else{//前四种都没有命中if(Object.keys(keyMap).length===0){keyMap={}for(leti=oldStartIdx;i<=oldEndIdx;i++){constkey=oldCh[i].keyif(key){keyMap[key]=i}}}console.log(keyMap)//寻找当前节点在keyMap中的位置constidxInOld=keyMap[newStartVnode.key]console.log(idxInOld)if(!idxInOld){//新节点不在旧节点中parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)}else{// 新节点在旧节点中,需要移动constelmToMove=oldCh[idxInOld]if(elmToMove.sel!==newStartVnode.sel){parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)}else{patchVnode(elmToMove,newStartVnode)// 把这项设置为undefined,表示已经移动过了oldCh[idxInOld]=undefinedparentElm.insertBefore(elmToMove.elm,oldStartVnode.elm)}}//指针向后移动newStartVnode=newCh[++newStartIdx]}}// 继续查询是否有剩余节点if(newStartIdx<=newEndIdx){console.log(新节点还有剩余节点没有处理,newStartIdx,newEndIdx)constbefore=newCh[newEndIdx+1]?.elmconsole.log(before)for(leti=newStartIdx;i<=newEndIdx;i++){//insertBefore可以自动识别undefined,如果是undefined,则插入到最后parentElm.insertBefore(createElement(newCh[i]),before)}}elseif(oldStartIdx<=oldEndIdx){console.log(旧节点还有剩余节点没有处理,oldStartIdx,oldEndIdx)for(leti=oldStartIdx;i<=oldEndIdx;i++){oldCh[i]&&(parentElm.removeChild(oldCh[i].elm))}}}letarr=[1,1,2,35,9,2,9]arr.reduce((p,n)=>{returnp^n},0)//is文件exportfunctionsameVnode(vnode1,vnode2){returnvnode1.sel===vnode2.sel&&vnode1.key===vnode2.key;}//createElement文件//真正创建domexportdefaultfunctioncreateElement(vnode){letdomNode=document.createElement(vnode.sel);if(vnode.text!==""&&(vnode.children===undefined||vnode.children.length===0)){domNode.innerText=vnode.text;// 补充elm}elseif(Array.isArray(vnode.children)&&vnode.children.length>0){for(leti=0;i<vnode.children.length;i++){domNode.appendChild(createElement(vnode.children[i]));}}vnode.elm=domNode;returnvnode.elm}//patchVnode文件importcreateElementfrom./createElementimportupdateChildrenfrom./updateChildrenexportdefaultfunctionpatchVnode(oldVnode,newVnode){// console.log(patchVnode)if(oldVnode===newVnode)returnif(newVnode.text&&(!newVnode.children||newVnode.children.length===0)){//判断newVnode的text是否为空,且不等于oldVnode的text,如果满足以上条件,则更新textoldVnode.text!==newVnode.text&&(oldVnode.elm.innerText=newVnode.text);}else{//newVnode的text为空,则判断newVnode的children是否为空,如果不为空,则更新children// 新节点没有text属性if(oldVnode.children&&oldVnode.children.length>0){// 老节点有children,新节点也有childrenupdateChildren(oldVnode.elm,oldVnode.children,newVnode.children);}else{// 老的没有children,新的有childrenoldVnode.elm.innerHTML=;for(leti=0;i<newVnode.children.length;i++){letdom=createElement(newVnode.children[i])oldVnode.elm.appendChild(dom)}}}}//patch文件importvnodefrom"./vnode";importcreateElementfrom"./createElement";importpatchVnodefrom./patchVnodeimport{sameVnode}from./isexportdefaultfunction(oldVnode,newVnode){// console.log(oldVnode, newVnode)//判断传入的第一个参数,是dom节点还是vnodeif(oldVnode.sel===""||oldVnode.sel===undefined){//传入的如果是dom节点需要包装为虚拟节点oldVnode=vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode,);}// 判断oldVnode和newVnode是否是同一个节点if(sameVnode(oldVnode,newVnode)){// console.log("是同一个节点");patchVnode(oldVnode,newVnode);}else{// console.log("不是同一个节点");letnewVnodeElm=createElement(newVnode,oldVnode.elm);if(oldVnode.elm.parentNode&&newVnodeElm){oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm);}oldVnode.elm.parentNode.removeChild(oldVnode.elm);}}//VNode文件exportdefaultfunction(sel,data,children,text,elm){constkey=data.keyreturn{sel,data,children,text,elm,key,}}//h文件importvnodefrom./vnode.js//h(div,{},文字)//h(div,{},[])//h(div,{},h())exportdefaultfunction(sel,data,c){//检查参数个数if(arguments.length!==3){thrownewError(h()参数个数不正确)}// 检查C类型if(typeofc===string||typeofc===number){returnvnode(sel,data,undefined,c,undefined)}elseif(Array.isArray(c)){letchildren=[]for(leti=0;i<c.length;i++){if(!(typeofc[i]===object&&c[i].hasOwnProperty(sel))){thrownewError(传入的数组参数中有项不是h函数)}children.push(c[i])}// 循环结束,children收集完毕returnvnode(sel,data,children,undefined,undefined)}elseif(typeofc===object&&c.hasOwnProperty(sel)){letchildren=[c]returnvnode(sel,data,children,undefined,undefined)}else{thrownewError(h()参数类型不正确)}}