patch函数实现虚拟DOM上树(diff算法进行精细化对比)简易patch实现

patch函数

拿到新旧节点,先进行判断,如果不是虚拟节点则进行虚拟节点的创建,在判断是否是同一个节点。如果是同一个节点则需要进行精细化对比。如果不是同一个节点,则需要暴力删除,此时我们需要先将新节点转为真实的DOM,在将dom依据老节点为标杆进行插入,插入完成之后进行老节点的删除。其中vnode函数在上一章节讲h函数中有使用用来创建虚拟节点。createElement用来依据虚拟节点创建真实的DOM并挂载在虚拟节点的elm上。patchVNode函数对新老节点进行进一步的比较。

import vnode from './vnode'
import createElement from './createElement'
import patchVNode from './patchVNode'
export default function (oldVnode, newVnode) {
    if(oldVnode.sel == '' || oldVnode.sel == undefined ){
        // 为DOM元素时创建一个对应的oldVnode
        oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode)
    }

    // 判断是不是同一个节点
    if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key ){
        patchVNode(oldVnode , newVnode)
    }else{
        // 不是同一个节点,进行删除
        let newVnodeElm =  createElement(newVnode)
        if(newVnodeElm && oldVnode.elm.parentNode){
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }

}

patchVNode函数

此时我们已经判断了是同一个节点,此时我们需要进行精细化比较。首先判断如果新旧节点完全相同,我们则不需要进行任何操作,不需要再重新上树。如果新节点有文本属性并且新节点不存在子节点,则我们进行比较新老节点的文本节点,如果不一样(包括老节点有子节点)我们则可以直接讲新节点的文本节点替换老节点里面的内容。使用oldVnode.elm.innerText = newVnode.text。如果新节点没有text属性有子节点,则我们需要考虑老节点是否有子节点,如果老节点没有子节点,我们可以直接清空老节点的内容,并讲新节点的子节点创建出来添加到老节点里面。如果新老节点都有子节点,我们则需要进行更近一步对比。updateChildren用来精细化比较新老节点都有子节点的情况。

 import createElement from "./createElement";
import updateChildren from "./updateChildren";
/**
 * @param {*} oldVnode 
 * @param {*} newVnode 
 * @returns 
 * 是同一个节点的时候需要进行精细化比较,此时我们使用patchVNode进行比较并上树
 */
export default function patchVNode( oldVnode ,newVnode ){
    if(oldVnode === newVnode) return ;
    if(newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)){
        // 新节点有text属性
        if(newVnode.text != oldVnode.text){
            // 如果新的text和老的不同,则需要替换。如果老的是DOM也会被覆盖
            oldVnode.elm.innerText = newVnode.text
        }
    }else{
        // 新节点没有text属性有children
        // 老节点有children
        if(oldVnode.children != undefined && oldVnode.children.length > 0){
            updateChildren(oldVnode.elm ,oldVnode.children , newVnode.children )
        }else{
            // 清空老的text
            oldVnode.elm.innerHTML = '';
            // 给新的子节点进行children(子元素的添加)
            for(let i = 0 ; i < newVnode.children.length ; i++){
                let dom = createElement(newVnode.children[i])
                oldVnode.elm.appendChild(dom)
            }
        }
    }
}

updateChildren函数(diff算法的体现)

我们给传进来的子节点进行添加指针。
新节点的前指针:newStartIndex 指向新节点还没有处理完的第一个子节点
新节点的后指针:newEndIndex 指向新节点还没有处理完的最后一个子节点
旧节点的前指针:oldStartIndex 指向老节点还没有处理完的第一个子节点
旧节点的后指针:oldEndIndex 指向老节点还没有处理完的最后一个子节点
拿到前节点和尾部节点
新节点前子节点:newStartVnode 新节点还没有处理完的第一个子节点
新节点尾子节点:newEndVnode 新节点还没有处理完的最后一个子节点
老节点前子节点:oldStartVnode 老节点还没有处理完的第一个子节点
老节点前子节点:oldStartVnode 老节点还没有处理完的最后一个子节点

diff算法的四个命中
1.新前与旧前
2.新后与旧后
3.新后与旧前
4.新前与旧后
规则:
1.命中新前与旧前,则将新老指针的前指针往后移一位。并使用patchVNode函数进行子节点内部比较,之后进行上树。

2.命中新后与旧后,则将新老指针的后指针往前移一位。并使用patchVNode函数进行子节点内部比较,之后进行上树。

3.命中新后与旧前,我们需要使用patchVNode函数进行子节点内部比较,此时由于新老节点命中位置不一样,需要改变位置。因为老节点是已经在树上的节点,此时我们的新节点和老节点是一个(命中)我们则只需要将老节点命中的那个节点位置放到后面来,也就是最后一个节点。然后再将旧节点的前指针往后移动一位,新节点的后指针往前移动一位。

提示:新节点最后一个节点命中,所以新的DOM树肯定是命中最后的节点在最后的位置,所以我们插入是插入在未处理完老节点的后面。

4.命中新前旧后,(和三相反),只不过此时我们需要插入到老节点的未处理完的最前面。

5.命中一种就不在进行命中判断了。

6.如果四个都没有命中则通过循环进行查找。因为四个都没有命中,所以可能是对上了中间位置,此时我们通过循环遍历进行查找。为了提高效率我们通过将老节点的key和对应的位置映射在map中。我们通过key直接在map中找位置,如果没有找到位置则说明是以前不存在这个节点,此时我们就需要将这个子节点创建出来,并且添加到还没有处理完老节点开始的前面。如果找到了,则说明是节点的位置发生了移动,此时我们需要根据位置拿到对应的老节点,并将其保存起来,因为我们需要在老节点中将其变为undefined,我们将拿到的老节点和新节点也需要进行深度对比,调用patchVNode,并且将位置移动到老节点还没有处理完的节点前面。此时一套流程下来,当前新节点的第一个需要处理完的节点也就被处理好了,我们需要将新节点的前指针往后移动一位。

7.当新老节点的子节点位数不一样的时候,则会出现要么存在没有处理完的节点,要么存在多余的节点。所以我们循环处理节点的循环条件是新老节点的前指针分别小于新老节点后指针的位置。

8.当循环结束新节点前指针还小于等于新节点的后指针则说明还存在新的节点没有添加进去,此时我们将前后指针中间的元素创建出来并进行添加上树。此时我们只要将创建好的元素添加到老节点最后的位置就好。

9.当循环结束老节点前指针还小于等于老节点的后指针则说明还存在老的节点需要删除,此时我们遍历老节点前后指针的未删除的元素,进行删除。

10.注意点,我们在没有命中的时候处理好的元素置为了undefined,所以我们在进行循环的时候,对当前所有的指针和元素需要进行判断,如果是undefined或者null,我们需要对指针进行移动及对节点进行拿到最新的。

import patchVNode from "./patchVNode";
import createElement from "./createElement";

/**
 * @param {*} a 
 * @param {*} b 
 * @returns 
 * 判断是否是同一个节点
 */
 function checkSameVnode(a,b){
    return a.sel == b.sel && a.key == b.key
}

/**
 * @param {*} parentElm 父节点
 * @param {*} oldCh  旧节点的子元素
 * @param {*} newCh  新节点子元素
 */
export default function updateChuldren(parentElm , oldCh , newCh){
    // 新前
    let newStartIndex = 0;
    // 旧前
    let oldStartIndex = 0;
    // 新后
    let newEndIndex = newCh.length - 1;
    // 旧后
    let oldEndIndex = oldCh.length - 1;
    // 新前节点
    let newStartVnode = newCh[0]
    // 旧前节点
    let oldStartVnode = oldCh[0]
    // 新后节点
    let newEndVnode = newCh[newEndIndex]
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIndex]
    // 缓存
    let keyMap = null

    while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
        // 首先不是判断命中,而是路过依据加undefuned标记的东西
        if(oldStartVnode == null || oldCh[oldStartIndex] == undefined){
            oldStartVnode = oldCh[++oldStartIndex]
        }else  if(newStartVnode == null ||newCh[newStartIndex] == undefined){
            newStartVnode =newCh[++newStartIndex]
        }else  if(newEndVnode == null || newCh[newEndIndex] == undefined){
            newEndVnode = newCh[--newEndIndex]
        }else  if(oldEndVnode == null || oldCh[oldEndIndex] == undefined){
            oldEndVnode = oldCh[--oldEndIndex]
        }else if(checkSameVnode(oldStartVnode,newStartVnode)){
            // 命中新前和旧前
            patchVNode(oldStartVnode,newStartVnode);
            oldStartVnode = oldCh[++oldStartIndex]
            newStartVnode = newCh[++newStartIndex]
        }else if(checkSameVnode(oldEndVnode,newEndVnode)){
            // 命中新后和旧后
            patchVNode(oldEndVnode,newEndVnode);
            oldEndVnode = oldCh[--oldEndIndex]
            newEndVnode = newCh[--newEndIndex]
        }else if(checkSameVnode(oldStartVnode,newEndVnode)){
            // 命中新后旧前
            patchVNode(oldStartVnode,newEndVnode);
            // 插入
            parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling)
            oldStartVnode = oldCh[++oldStartIndex]
            newEndVnode = newCh[--newEndIndex]
        }else if(checkSameVnode(oldEndVnode,newStartVnode)){
            // 命中新前旧后
            patchVNode(oldEndVnode,newStartVnode)
            // 插入
            parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIndex]
            newStartVnode = newCh[++newStartIndex]
        }else{
            // 都没有命中
            if(!keyMap){
                keyMap = {}
                for(let i = oldStartIndex; i<=oldEndIndex ; i++){
                    const key = oldCh[i].key
                    if(key != undefined){
                        keyMap[key] = i
                    }
                }
            }
            // 寻找当前这项(newStartIndx) 这项在keyMap中的映射位置
            const idxInOld = keyMap[newStartVnode.key]
            if(idxInOld == undefined){
                // 要加项
                parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)
            }else{
                // 不是全新的想,需要移动
                const elmToMove = oldCh[idxInOld]
                patchVNode(elmToMove,newStartVnode)
                // 把这项设置为undefined
                oldCh[idxInOld] = undefined
                // 移动,调用insertBefore可以实现移动
                parentElm.insertBefore(elmToMove.elm,oldStartVnode.elm)
            }
            newStartVnode = newCh[++newStartIndex]
        }
    }

    // 如果while结束代表要进行插入或者删除
    // 看看有没有剩余的节点没有处理
    if(newStartIndex <= newEndIndex){
        for(let i = newStartIndex ; i <= newEndIndex ; i++){
            // insertBefore可以自动识别null,如果是null则会自动拍到队尾,newch里面的元素还不是DOM,需要自己去创建
            parentElm.insertBefore(createElement(newCh[i]),old[oldStartIndex].elm )
        }
    }else if(oldStartIndex <= oldEndIndex){
        // 需要进行多余老节点的删除
        for(let i = oldStartIndex ; i <= oldEndIndex ; i++){
            if(oldCh[i]){
                parentElm.removeChild(oldCh[i].elm)
            }
        }
    }

}


createElement函数

依据sel进行节点的创建,注意,此处没有添加标签属性,包括类。
首先我们判断是否有子节点,如果只有文本节点则直接使用innerText。若存在子节点,我们则需要同理创建子节点,并进行appendChild。此处进行了递归。这样子节点之后的还是可以通过createElement函数进行创建。创建完之后我们需要把真实的DOM挂载在虚拟节点的elm属性上。

/**
 * @param {*} vnode 虚拟节点
 * 把虚拟节点
 */
export default function createElement(vnode){
    let  domNode = document.createElement(vnode.sel)
    // 判断是否有子节点
    if(vnode.txet !== '' && vnode.children == undefined || vnode.children.length == 0){
        domNode.innerText = vnode.text
    }else if(Array.isArray(vnode.children) && vnode.children.length > 0){
        // 内部是子节点,就要递归创建节点
        for(let i =0 ; i < vnode.children.length ; i++ ){
            let ch = vnode.children[i]
            let chDOM = createElement(ch)
            domNode.appendChild(chDOM)
        }
    }
    vnode.elm = domNode
    return vnode.elm
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值