Vue底层原理之虚拟DOM(2)节点更新流程

更新节点的流程:
在这里插入图片描述先完成不是同一个节点的情况:

patch.js:

import vnode from './vnode'
import createElement from './createElement'
export default function(oldVnode,newVnode){
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if(oldVnode.sel==''||oldVnode.sel==undefined){//没有sel属性就是DOM节点
        oldVnode=vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);//DOM节点包装成虚拟节点
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if(oldVnode.sel==newVnode.sel&&oldVnode.key==newVnode.key){
        // 同一个节点
    }else{
        // 不是同一个节点,暴力删除旧的,添加新的
        createElement(newVnode,oldVnode.elm)
    }
}

createElement.js:

// 真正创建节点,将vnode创建为DOM,插入到pivot这个元素之前
export default function(vnode,pivot){
    let domNode=document.createElement(vnode.sel);
    // 有子节点还是有文本
    if(vnode.text!=''&&(vnode.children==undefined||vnode.children.length==0)){
        // 内部是文本
        domNode.innerText=vnode.text;
        // 节点上树  标杆节点的父元素调用insertBefore方法
        pivot.parentNode.insertBefore(domNode,pivot);
    }else if(Array.isArray(vnode.children)&&vnode.children.length>0){

    }
}

在创建DOM元素的时候,需要判断该节点有没有子节点,没有就直接设置好文本,让节点上树。该节点需要有一个标杆节点,这个标杆就是原来旧节点对应的DOM,我们要在标杆之前插入新节点。
而如果新的节点有子节点,就需要递归调用createElement函数,当子节点再没有子节点,只有文本时,递归结束。但是,这里的子节点是没有标杆节点(pivot)的,所以我们需要修改一下函数。

// 真正创建节点,将vnode创建为DOM
export default function createElement(vnode){
    let domNode=document.createElement(vnode.sel);
    // 有子节点还是有文本
    if(vnode.text!=''&&(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);// 一旦调用createElement就意味着创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树
            domNode.appendChild(chDOM);//真正的DOM
        }
    }
    // 补充elm属性
    vnode.elm=domNode
    return vnode.elm;
}
export default function(oldVnode,newVnode){
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if(oldVnode.sel==''||oldVnode.sel==undefined){//没有sel属性就是DOM节点
        oldVnode=vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);//DOM节点包装成虚拟节点
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if(oldVnode.sel==newVnode.sel&&oldVnode.key==newVnode.key){
        // 同一个节点
    }else{
        // 不是同一个节点,暴力删除旧的,添加新的
        let newVnodeElm=createElement(newVnode);
        // 插入到旧节点之前
        if(oldVnode.elm.parentNode!=undefined&&newVnodeElm){
            oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm)
        }
        // 删除旧节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

此时,如果:

const vnode1=h('h1',{},'你好');
const vnode2=h('ul',{},[
    h('li',{},'111'),
    h('li',{},'111'),
    h('li',{},'111')
])
patch(vnode1,vnode2);

页面就只会显示vnode2。到这里,我们实现了两个虚拟节点不是同一个的情形。此时还没有使用到diff算法。
接下来,需要实现两个虚拟节点是同一个节点的情况。续上上边的流程图:
在这里插入图片描述同一个对象指的是const vnode2=vnode1,显然,不需要操作什么。流程图中,五角星位置的情况最为复杂,就是新旧虚拟节点都有children,是需要递归的。我们先来实现一下五角星之外的情形。

export default function (oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {//没有sel属性就是DOM节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);//DOM节点包装成虚拟节点
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
        // 同一个节点
        // 判断是否是同一个对象 
        if (oldVnode === newVnode) return;// 同一个对象直接返回
        // 判断新vnode有没有text属性
        if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {// 有text  
            if (newVnode.text === oldVnode.text) {//新旧text是否相同            
                return;
            } else {
                // 旧节点的文本替换为新节点的文本,oldVnode是虚拟节点,它不能点出innerText,只能oldVnode.elm.innerText
                // newVnode也是虚拟属性,并且这里它还没有elm,所以要用newVnode.text
                // 即便旧节点中有children,改了innerText,children也就没了
                oldVnode.elm.innerText = newVnode.text;
            }
        } else {// 新的没有text属性  意味着新的有children
            // 判断旧的有没有children
            if (oldVnode.children == undefined || oldVnode.children.length === 0) {// 旧的没有children,意味着旧的有text属性
                // 清空text
                oldVnode.elm.innerHTML = ''
                // 把新的children添加进来
                for (let i = 0; i < newVnode.children.length; i++) {
                    let ch = newVnode.children[i];
                    let chDOM = createElement(ch);
                    oldVnode.elm.appendChild(chDOM)
                }
            }
        }
    } else {
        // 不是同一个节点,暴力删除旧的,添加新的
        let newVnodeElm = createElement(newVnode);
        // 插入到旧节点之前
        if (oldVnode.elm.parentNode != undefined && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除旧节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

当两个虚拟节点都有children时,由于需要递归调用函数,我们需要把部分代码抽离出来,建立patchVnode.js文件:

import createElement from './createElement'
export default function patchVnode(oldVnode,newVnode){
    // 判断是否是同一个对象 
    if (oldVnode === newVnode) return;// 同一个对象直接返回
    // 判断新vnode有没有text属性
    if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {// 有text  
        if (newVnode.text === oldVnode.text) {//新旧text是否相同            
            return;
        } else {
            // 旧节点的文本替换为新节点的文本,oldVnode是虚拟节点,它不能点出innerText,只能oldVnode.elm.innerText
            // newVnode也是虚拟属性,并且这里它还没有elm,所以要用newVnode.text
            // 即便旧节点中有children,改了innerText,children也就没了
            oldVnode.elm.innerText = newVnode.text;
        }
    } else {// 新的没有text属性  意味着新的有children
        // 判断旧的有没有children
        if (oldVnode.children == undefined || oldVnode.children.length === 0) {// 旧的没有children,意味着旧的有text属性
            // 清空text
            oldVnode.elm.innerHTML = ''
            // 把新的children添加进来
            for (let i = 0; i < newVnode.children.length; i++) {
                let ch = newVnode.children[i];
                let chDOM = createElement(ch);
                oldVnode.elm.appendChild(chDOM)
            }
        }
    }
}

这是从patch.js中抽离出来的。原来的patch中这部分代码替换为patchVnode(oldVnode,newVnode);即可。
先在vnode.js里,把key属性加上:

export default function(sel,data,children,text,elm){
    const key=data.key;
    return{
        sel,data,children,text,elm,key
    }
}

两个都有children又可分为几种情况:新增,删除,修改。。
如果是新增的话,遍历新节点的子节点,如果发现某个子节点在原来的节点中不存在,则为新增,它要插入的位置为所有未处理的节点之前,这不等同于所有已处理节点之后。

 else{//新旧都有children
    // 所有未处理节点的开头
    let un=0;
    for(let i=0;i<newVnode.children.length;i++){
        let newCh=newVnode.children[i];
        // 看oldVnode中有没有节点和它相同
        let isExist=false;
        for(let j=0;j<oldVnode.children.length;j++){
            let oldCh=oldVnode.children[j];
            if(oldCh.sel==newCh.sel&&oldCh.key==newCh.key){// 相同节点
                isExist=true;
                // 找到相同的,
                un++;
                break;
            }
        }
        if(!isExist){//新节点的某个子节点旧的没有
            console.log(i)
            let dom=createElement(newCh);
            newCh.elm=dom;
            if(un<oldVnode.children.length){
                oldVnode.elm.insertBefore(dom,oldVnode.children[un].elm);
            }else{
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}

un指向所有未处理节点的开头,如果新节点的子节点在旧节点中能够找到,则un下移,一直到某个子节点为新增节点时,将该子节点插入到un指向节点之前即可。
但是,这种做法很繁琐,我们只是处理了新增节点的情况,假设C变成了M,或者B删掉,诸如此类情况都要再加判断,一一处理。

到这里,就要引出diff算法了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值