/**
-
diff全称difference,译为中文就是差异、不同的意思,而diff算法顾名思义就是用来得到差一点的,同时我们需要一个本体,一个参照体,通过对本体和参照体的比较得出他们有哪些差异点;
-
这个过程我们简称找不同,而这也是diff算法的本质
-
在前端领域,diff算法多用在框架中,如vue、react中;他的本体和参照物都是虚拟dom
-
什么是虚拟dom:虚拟dom本质上就是一个对象,该对象描述了一个UI节点所对应的一些必要信息
-
真实dom的crud损耗是固定的,使用虚拟dom的意义在于在diff比对的时候,真实dom上的属性很多,开销很大;所有需要使用虚拟dom
-
为什么需要虚拟dom?
-
因为真实dom在创建的时候会携带非常多的属性,我们知道最终在进行diff的时候是需要本体和参照体的,在目前不考虑前端diff的具体实现细节,我们仅从宏观逻辑角度去考虑,至少可以
-
明确一点:本体和参照物的细节越多,对比所消耗的精力和时间也就越多;而整个真实dom的属性这么多,如果我们都一个一个拿出来比较,无疑是一笔巨大的损耗
-
前端的diff其实就是毕竟虚拟dom的差异,或者说虚拟dom树的差异
-
虚拟dom树
-
vue
-
diff算法书写在一个名为patch的函数中
-
1、如果两个都是文本节点,就看文本值相不相同;如果不同则直接应用型的文本值(这是最不消耗性能的)
-
2、如果oldNode存在,newNode没有了;那就代表卸载,直接移除对应的dom(这也是没有办法的)
-
3、如果oldNode不存在,newNode存在;那就代表新增,需要直接新增dom(这也是没有办法的)
-
4、如果两个节点都不为空,而且还是可以复用的同一个节点;那就直接去比较子元素,在这种情况下绝对不可能是文本节点;新旧两个节点的tag、key都一直的话,节点就可以直接复用;但是需要注意还是要比较其他属性,如id、class
-
5、如果两个节点都完全不同,那只能以newNode为准了,旧的直接删除 */
// 构建虚拟dom class Vnode { key; //唯一的key值 isStatic; // vue3中标记的时候是静态节点 componentInstance; // 如果是组件的话,该属性代表组件实例 constructor( tag, // 虚拟dom类型 children, //该虚拟dom的子节点,每一个子节点又是一个新的虚拟dom text, // 该虚拟dom的文本,只有文本节点才有值 elm,// 该虚拟dom对应的真实dom parent, // 该虚拟dom的父节点 data, // 该虚拟节点的属性信息,比如class,props,styles等等
){ // 初始化 this.tag = tag; this.children = children; this.text = text; this.elm = elm; this.parent = parent; this.data = data; }}
const div = new Vnode( "div", ["helloWorld"], null, "div", null, { attrs:{ class:"wrapper" } }
) console.log(div,"dvvv") // 判断是否是undefined或者null function isUndef(v){ return v === undefined || v === null; }
function isDef(v){ return v !==undefined && v!==null; }
// 判断两个节点是否相同 function isSameNode(a,b){ return ( a.key === b.key && a.tag === b.tag && isDef(a.data) === isDef(b.data) && isSameInputType(a,b)
)} // 判断两个输入框的类型是否相同 function isSameInputType(a,b){ if(a.tag!=='input')return true; const typeA = a.data.attrs.type; const typeB = b.data.attrs.type; return typeA === typeB; } // vue diff 算法 function patch(oldNode,newNode){ // 1. 首先,如果oldNode有值,新的没有了,那就直接移除 if(isDef(oldNode) && isUndef(newNode)){ oldNode.elm && oldNode.remove(); } // 2. 如果旧节点没有定义,新节点有值,那就是直接创建 if(isDef(newNode) && isUndef(oldNode)){ newNode.parent.elm.appendChild(newNode.elm) } // 3. 如果两个是一样的节点,就去进行子节点比对 // 判断是不是真实dom const isRealElement = isDef(oldNode.nodeType); if(!isRealElement && isSameNode(oldNode,newNode)){ // isRealElement用户判断整个oldNode是不是虚拟dom // 如果两个是一样的节点,去对比子节点 patchNode(oldNode,newNode) } }
function patchNode(oldNode,newNode){ // 能够走进这个方法,至少证明了 // 1 oldNode的tag和newNode的tag是一样的 // 2 同时两个节点的key也是一样的,elm也是一样的;所以我们不需要创建新的真实dom // 还需要注意的地方就是,vue3中推出了静态节点,如果该节点被标记为静态节点,就无条件复用 //基于上面的分析,我们在这个方法里首先要看的就是两个节点是不是静态节点 // 静态节点会在初始化的时候给每个节点打上isStatic标记
if(oldNode === newNode)return;// 如果两个引用完全一样,直接返回 const elm = newNode.elm = oldNode.elm; // elm直接复用 if(oldNode.isStatic && newNode.isStatic){ newNode.componentInstance = oldNode.componentInstance;//如果都是静态节点,直接复用旧节点的组件实例 return; } // 判断新旧节点是否是文本节点,如果是文本节点,比较文本是否相同。如果不同就应用最新的文本 if(isDef(newNode.text)){ if(newNode.text!==oldNode.text){ // 直接修改真实文本节点的值,页面也会直接变化; vue是一边diff,一边更新的 newNode.elm.textContent = newNode.text; } return } // 如果不是text节点,对比两个节点data属性 const {attrs:oldAttrs} = oldNode.data const {attrs:newAttrs} = newNode.data; const attrKeys = Object.values(oldNode); attrKeys.forEach(attr=>{ if(newNode.data[attr]!==oldNode.data[attr]){ elm.setAttribute(attr,newNode.data[attr]) } }) // 比较子节点 const oldChild = oldNode.children; const newChild = newNode.children; updateChildren(elm,oldChild,newChild);} // 承载了diff算法的核心 /** *
-
@param {*} parentElm 两个子节点的父节点
-
@param {*} oldCh 旧节点
-
@param {*} newCh 新节点
-
@param {*} insertedVnodeQueue
-
@param {*} removeOnly
-
比较新子树和旧子树的差异,并且更新真实dom */ function updateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){ let oldStartIndex = 0; // 旧树首节点索引 let newStartIndex = 0; // 新树首节点索引 let oldEndIndex = oldCh.length -1; // 旧树尾节点索引 let newEndIndex = newCh.length -1; // 新树尾节点索引 let oldStartNode = oldCh[oldStartIndex]; // 旧树首节点 let newStartNode = newCh[newStartIndex] // 新树首节点 let oldEndNode = oldCh[oldEndIndex]; // 旧树尾节点 let newEndNode = newCh[newEndIndex]; // 新树尾节点 let oldKeyToIdx,idxInOld,vnodeToMove; // 如果oldStartIndex大于了oldEndIndex,说明循环已经走完了 while(oldStartIndex<=oldEndIndex && newStartIndex<=newEndIndex){ // 判断旧树的首节点是不是空(undefined或者null) if(isUndef(oldStartNode)){ // 如果旧树的首节点没有,那就往下找,找到有为止 oldStartNode = oldCh[++oldStartIndex] }else if(isUndef(oldEndNode)){ // 能够走到这个判断,说明旧树的首节点已经找到,尾节点还没找到 oldEndNode = oldCh[--oldEndIndex] }else if(isSameNode(oldStartNode,newStartNode)){ // 旧树的首节点和新树的尾节点可以复用 // 能走到这个判断,说明 // 1.旧树的首尾节点全部找到了 // 2.旧树的首节点和新树的尾节点可以复用 // updateChild只能比较子元素,虽然dom可以重用,但是不能保证class没有变化 // insertedVnodeQueue 表示已经处理过的虚拟dom,一旦有新增dom的操作的时候就会起作用 patchNode(oldStartNode,newStartNode,insertedVnodeQueue); oldStartNode = oldCh[++oldStartIndex]; newStartNode = newCh[++newStartIndex]; }else if(isSameNode(oldEndNode,newEndNode)){
patchNode(oldEndNode,newEndNode,insertedVnodeQueue); oldEndNode = oldCh[--oldEndIndex] newEndNode = newCh[--newEndNode] }else if(isSameNode(oldStartNode,newEndNode)){ // 能够进入到这判断,说明 // 1. 旧树的首尾节点都有 // 2. 旧树的首节点和新书的首节点不同 // 3. 旧树的尾节点和新树的尾节点也不同 // 看旧树的首节点和新树的尾节点是否相同 patchNode(oldStartNode,newEndNode,insertedVnodeQueue); insesrtBefore(parentElm,oldEndNode.elm,nodeOps.nextSibling(oldEndNode.elm)); oldStartNode = oldCh[++oldStartIndex] newEndNode = newCh[--newEndIndex] }else if(isSameNode(oldEndNode,newStartNode)){ patchNode(oldEndNode,newStartNode,insertedVnodeQueue); insesrtBefore(parentElm,oldEndNode.elm,oldStartNode.elm) oldEndNode = oldCh[--oldEndIndex] newStartNode = newCh[++newStartIndex] }else { // 能够进入这个判断,说明 // 1. 旧树的首尾节点都有值 // 2. 旧树的首节点和新树的首节点无法重用 // 3. 旧树的首节点和新树的尾节点无法重用 // 4. 旧树的尾节点和新树的首节点无法重用 // 5. 旧树的尾节点和新树的尾节点无法重用 // 把目前没有用到的旧树里的所有节点的key都收集起来了 // 把新节点的key和存起来的key逐一比较 if(isUndef(oldKeyToIdx)) oldKeyToIdx= createKeyToOldIdx(oldh,oldStartIndex,oldEndIndex) // idxInOld: 代表的是新树的首节点在旧树中能否找到对应的key值,如果能够找到,那么这个idxInOld就是该key值对应的虚拟dom所在的索引位 idxInOld = isDef(newStartNode.key)?oldKeyToIdx[newStartNode.key]:findIdxInOld(newStartNode,oldCh,oldStartIndex,oldEndIndex) if(isUndef(idxInOld)){ // 进入这里,表示把旧树都循环找完了,还是没找到;表示没有任何可以重用的元素,只有直接创建 createElm(newStartNode, insertedVnodeQueue, parentElm, oldStartNode.elm, false, newCh, newStartIndex) }else { // 找到了可以复用的节点 vnodeToMove = oldCh[idxInOld] // 拿到了可以和当前新树的首节点复用的旧树中的节点 if(isSameNode(vnodeToMove,newStartNode)){ patchNode(vnodeToMove,newStartNode,insertedVnodeQueue); oldch[idxInOld] = undefined; insesrtBefore(parentElm,vnodeToMove.elm,newStartNode.elm) }else { // 还是不能复用,创建新节点 createElm(newStartNode, insertedVnodeQueue, parentElm, oldStartNode.elm, false, newCh, newStartIndex) } newStartNode = newCh[++newStartIndex] } }}
// 如果旧树的首节点index大于了旧树的尾节点index,说明新节点有新增的节点 if(oldStartIndex>oldEndIndex){ refElm = isUndef(newCh[newEndIndex +1])?null:newCh[newEndIndex+1].elm addVnodes(parentElm,refElm,newCh,newStartIndex,newEndIndex,insertedVnodeQueue) }else if(newStartIndex>newEndIndex){ // 新树的newStartIndex大于了newEndIndex,表示新树少了节点 removeVnodes(oldCh,oldStartIndex,oldEndIndex) } }
function createKeyToOldIdx(children,beginIdx,endIdx){ let key,map = []; for(let i=beginIdx;i<endIdx;i++){ key = children[i].key; if(key){ map[key] = i } } return map; }
function findIdxInOld(node,oldCh,start,end){ for(let i = start;i<end;i++){ const c = oldCh[i]; if(isDef(c) && isSameNode(node,c)) return i; } }
-