虚拟dom和diff算法

文章介绍了虚拟DOM的概念,它是一个节点描述对象,通过`h`函数创建。文章详细阐述了从虚拟节点到真实DOM的渲染过程,包括创建元素、添加属性、更新属性以及对比和更新子节点的策略。还提到了关键的`patch`函数用于比较旧的和新的虚拟节点,以实现高效DOM更新。

虚拟dom和diff算法

前言

代码实现

虚拟节点

虚拟dom是什么?虚拟dom就是一个节点描述对象,即一个对象。

h:渲染一个组件,渲染一个节点

渲染:把vnode渲染成domElement

把虚拟节点渲染成真实dom

第1步:创建元素/文本节点

这里只是第一层,而且div上的属性还没加


export function render(vnode,container){
    let ele=createDomElementFromVnode(vnode);
    container.appendChild(ele)
}
function createDomElementFromVnode(vnode) {
    let {type,key,props,children,text}=vnode;
    if(type){
        vnode.domElement=document.createElement(type)
    }else{
        vnode.domElement=document.createTextNode(text)
    }
    return vnode.domElement;
}

第2步:给domElement加属性


export function render(vnode,container){
    let ele=createDomElementFromVnode(vnode);
    // let {children=[]}=vnode;
    // children.forEach(child=>{
    //     render(child,ele);
    // })
    container.appendChild(ele)
}
function createDomElementFromVnode(vnode) {
    let {type,key,props,children,text}=vnode;
    if(type){
        vnode.domElement=document.createElement(type)
        updateProperties(vnode)
        // 视频里是放在了这里
        children.forEach(child=>render(child,vnode.domElement))
    }else{
        vnode.domElement=document.createTextNode(text)
    }
    return vnode.domElement;
}
function updateProperties(newVnode,oldProps={}){
    let domElement=newVnode.domElement;
    let newProps=newVnode.props;
    for(let prop in oldProps){
        if(!newProps[prop]){
            delete domElement[prop]
        }
    }
    // style需要特殊处理
    for(let prop in oldProps.style){
        if(!newProps.style[prop]){
            domElement.style[prop]=''
        }
    }
    /**
     * @todo className dom事件等
     */
    for(let prop in newProps){
        if(prop==='style'){
            // style需要特殊处理
            for(let styleProp in newProps.style){
                domElement.style[styleProp]=newProps.style[styleProp]
            }
        }else{
            domElement[prop]=newProps[prop]
        }

    }
}

总结:

1.主要接收type props children来创建虚拟节点。虚拟dom就是一个对象,把type和key单独提取出来;

若是文本包成一个只有text属性的vnode

  1. 通过render渲染成真实节点,把真实节点插到容器中

先根据type区分是元素还是文本节点。

元素节点:把属性赋到元素节点上。比较老属性和新属性的差异,并且算出最新的赋到最新的元素上。递归渲染

文本节点:

domdiff

涉及到相关dom操作

获取父节点 elem.parentNode

替换孩子 replaceChild(newChild, oldChild)

文本内容 textContent='xxx'

export function patch(oldVnode,newVnode) {
    /**
     * 类型不同 直接替换
     */
    if(oldVnode.type !== newVnode.type){
        // 底层肯定是dom操作
        return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode),oldVnode.domElement)
    }
    if(oldVnode.text){
        if(oldVnode.text !== newVnode.text){
            return oldVnode.domElement.textContent=newVnode.text;
        }
    }
    newVnode.domElement=oldVnode.domElement;
    updateProperties(newVnode,oldVnode.props)


}

updateChildren

列表diff比对*

对常见的dom操作优化

比较头指针

比较尾指针

分了2种情况

vue的话数据变化的时候才会走这个patch

mvvm => 调用patch

暴力比对

没有key,则重新创建插入


export function render(vnode,container){
    let ele=createDomElementFromVnode(vnode);
    // let {children=[]}=vnode;
    // children.forEach(child=>{
    //     render(child,ele);
    // })
    container.appendChild(ele)
}
function createDomElementFromVnode(vnode) {
    let {type,key,props,children,text}=vnode;
    if(type){
        vnode.domElement=document.createElement(type)
        updateProperties(vnode)
        // 视频里是放在了这里
        // 递归渲染子虚拟节点
        children.forEach(child=>render(child,vnode.domElement))
    }else{
        vnode.domElement=document.createTextNode(text)
    }
    return vnode.domElement;
}
function updateProperties(newVnode,oldProps={}){
    let domElement=newVnode.domElement;
    let newProps=newVnode.props;
    for(let prop in oldProps){
        if(!newProps[prop]){
            delete domElement[prop]
        }
    }
    // style需要特殊处理
    for(let prop in oldProps.style){
        if(!newProps.style[prop]){
            domElement.style[prop]=''
        }
    }
    /**
     * @todo className dom事件等
     */
    for(let prop in newProps){
        if(prop==='style'){
            // style需要特殊处理
            for(let styleProp in newProps.style){
                domElement.style[styleProp]=newProps.style[styleProp]
            }
        }else{
            domElement[prop]=newProps[prop]
        }

    }
}

export function patch(oldVnode,newVnode) {
    /**
     * 类型不同 直接替换
     */
    if(oldVnode.type !== newVnode.type){
        // 底层肯定是dom操作
        return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode),oldVnode.domElement)
    }

    if(oldVnode.text){
        if(oldVnode.text !== newVnode.text){
            return oldVnode.domElement.textContent=newVnode.text;
        }
    }
    // 复用oldNode dom
    let domElement=newVnode.domElement=oldVnode.domElement;
    updateProperties(newVnode,oldVnode.props) // 更新自身属性

    let oldChildren=oldVnode.children;
    let newChildren=newVnode.children;
    if(oldChildren.length>0 && newChildren.length>0){
        // 列表比对 最复杂的
        updateChildren(domElement,oldChildren,newChildren)
    }else if(oldChildren.length>0){
        domElement.innerHTML=''
    }else if(newChildren.length>0){
        for(let i=0;i<newChildren.length;i++){
            domElement.appendChild(createDomElementFromVnode(newChildren[i]));
        }
    }


}
function updateChildren(parent,oldChildren,newChildren) {
    // 设置了头&尾2个指针
    let oldStartIndex=0;
    let oldStartVnode=oldChildren[oldStartIndex];
    let oldEndIndex=oldChildren.length-1;
    let oldEndVnode=oldChildren[oldEndIndex];

    let newStartIndex=0;
    let newStartVnode=newChildren[newStartIndex];
    let newEndIndex=newChildren.length-1;
    let newEndVnode=newChildren[newEndIndex];

    // 2排指针往前走
    while (oldStartIndex<=oldEndIndex && newStartIndex<=newEndIndex){

        // 判空处理
        if(!oldStartVnode){
            oldStartVnode=oldChildren[++oldStartIndex]
        }else if(!oldEndVnode){
            oldEndVnode=oldChildren[--oldEndIndex]
        }
        if(isSameVnode(oldStartVnode,newStartVnode)){
            patch(oldStartVnode,newStartVnode)
            // 往后走
            oldStartVnode=oldChildren[++oldStartIndex]
            newStartVnode=newChildren[++newStartIndex]
        }else if(isSameVnode(oldEndVnode,newEndVnode)){
            patch(oldEndVnode,newEndVnode)
            oldEndVnode=oldChildren[--oldEndIndex];
            newEndVnode=newChildren[--newEndIndex]
        }else if(isSameVnode(oldStartVnode,newEndVnode)){
            patch(oldStartVnode,newEndVnode)
            parent.insertBefore(oldStartVnode.domElement,oldEndVnode.domElement.nextElementSibling)
            oldStartVnode=oldChildren[++oldStartIndex];
            newEndVnode=newChildren[--newEndIndex]
        }else if(isSameVnode(oldEndVnode,newStartVnode)){
            patch(oldEndVnode,newStartVnode)
            parent.insertBefore(oldEndVnode.domElement,oldStartVnode.domElement)
            oldEndVnode=oldChildren[--oldEndIndex];
            newStartVnode=newChildren[++newStartIndex]
        }else{
            const map=createMapByKeyToIndex(oldChildren);
            let index=map[newStartVnode.key];
            if(index==null){
                parent.insertBefore(createDomElementFromVnode(newStartVnode),oldStartVnode.domElement)
            }else {
                patch(oldChildren[i],newStartVnode)
                parent.insertBefore(oldChildren[i].domElement,oldStartVnode.domElement)
                oldChildren[i]=undefined;
            }
            newStartVnode=newChildren[++newStartIndex]
        }
    }
    if(newStartIndex<=newEndIndex){
        for(let i=newStartIndex;i<=newEndIndex;i++){
            let beforeElement=newChildren[newEndIndex+1]==null?null:newChildren[newEndIndex+1].domElement;
            parent.insertBefore(createDomElementFromVnode(newChildren[i]),beforeElement)
            // parent.appendChild(createDomElementFromVnode(newChildren[newStartIndex]))
        }
    }
    // 移除老队列中多余的
    if(oldStartIndex<=oldEndIndex){
        for(let i=oldStartIndex;i<=oldEndIndex;i++){
            oldChildren[i] && parent.removeChild(oldChildren[i].domElement)
        }
    }

}
function createMapByKeyToIndex(oldChildren){
    let map={}
    oldChildren.forEach((child,i)=>{
        if(child.key){
            map[child.key]=i
        }
    })
    return map;
}
function isSameVnode(oldVnode,newVnode) {
    return oldVnode.type===newVnode.type && oldVnode.key===newVnode.key;
}

https://gitee.com/betterangela/vue-cli-demo3.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值