Vue虚拟DOM和diff算法核心原理详解

虚拟DOM

虚拟DOM是用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。

真实dom:

虚拟dom:

为什么要使用虚拟dom:

我们知道,Vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM,而操作真实DOM又是非常耗费性能的。所以我们可以用JS模拟出一个DOM节点,称之为虚拟DOM节点。当数据发生变化时,我们对比变化前后的虚拟DOM节点,通过DOM-Diff算法计算出需要更新的地方,然后去更新需要更新的视图。

h函数

虚拟DOM由h函数渲染产生。

h函数用来产生虚拟节点(vnode)

//比如这样调用h函数
h('a', {props: {href: 'http://www.test.com'}}, '举个例子');
//将得到这样的虚拟节点
{"sel": "a", "data": {props: {href: 'http://www.test.com'}}, "text": "举个例子")
//他表示的真正的dom节点
<a href = "http://www.test.com">举个例子</a>

h函数可以嵌套使用,从而得到虚拟dom树。

嵌套后得到虚拟dom树:

手写h函数

源码是typeScript实现的,以下用js实现一个简易版的h函数,简单易懂。

//编写一个低配版本的h函数,这个函数必须接收3个参数,缺一不可
//也就是调用的形态必须是下面三种之一
//1:h('div', {}, '文字')
//2:h('div', {}, [])
//3:h('div', {}, h())
function h(sel, data, c){
    if(arguments.length !== 3)
        throw new Error('对不起,h函数必须传三个参数,我们是低配版')
    //检查参数c的类型
    if(typeof c == 'string' || typeof c == 'number'){
        //说明现在调用h函数是形态1
        return vnode(sel, data, undefined, c, undefined)
    }else if(Array.isArray(c)){
        //说明现在调用h函数是形态2
        let children = []
        //遍历c,收集children
        for(let i = 0; i < c.length; i++){
            //检查c[i]必须是一个对象
            if(!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))){
                throw new Error('传入的数组参数中有项不是h函数')
            }
            children.push(c[i])
        }
        //循环结束说明children收集完了
        return vnode(sel, data, children, undefined, undefined)
    }else if(typeof c == 'object' && c.hasOwnProperty('sel')){
        //说明现在调用h函数是形态3
        //传入的c是唯一的chilren
        let children = [c]
        return vnode(sel, data, c,undefined, undefined)
    }else{
        throw new Error('第三个参数类型不对')
    }
}
//实现vnode函数,用来处理返回虚拟DOM树对象
function vnode(sel, data, children, text, elm){
    return {
        //es6语法,相当于sel:sel,data:data
        sel, data, children, text, elm
    }
}

接下来测试h函数的功能:

let vnode1 = h('div',{}, [
    h('p', {}, '1'),
    h('p', {}, '2')
])
console.log(vnode1)

成功得到虚拟dom,一个简易版h函数就实现了。

diff算法

key:

key属性是diff算法中重要的属性。key是节点的唯一辨识,告诉diff算法,更改前后他们是同一个DOM节点。

只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的,插入新的。而如何判断是否为同一个虚拟节点,通过判断选择器相同且key相同。

只进行同层比较,不进行跨层比较。

patch函数

diff算法核心patch函数用来将节点上树和更新

虚拟dom如何变成真正的dom,是涵盖在diff算法里的。

接下来是精细化比较的详细步骤:

接下来是五角星中具体undateChildren方法:

四种命中查找按顺序:

1.新前与旧前 2.新后与旧后 3.新后与旧前 4.新前与旧后

如果四种都没有命中,就需要用循环来寻找了。

按照上面的流程用js实现patch函数:

export default function patch(oldVnode, newVnode){
    //判断传入的第一个参数是dom节点还是虚拟节点
    if(oldVnode.sel == '' || oldVnode.sel == undefined){
        //传入的第一个参数是DOM节点,此时要包装成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }

    //判断oldVnode和newVnode是不是同一个节点
    if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel){
        console.log('是同一个节点')
        patchVnode(oldVnode,newVnode)
    }else {
        console.log('不是同一个节点,暴力删除新的,插入旧的')
        let newVnodeElm = createElement(newVnode)
        //插入到老节点之前
        if(oldVnode.elm.parentNode && newVnodeElm){
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        //删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}
//真正创建节点,将vnode创建为DOM
export default function createElement(vnode){
    //创建一个DOM节点
    let domNode = document.createElement(vnode.sel)
    //判断有子节点还是有文本
    if(vnode.text != undefined || 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]
            //创建出它的DOM,一旦调用createElement意味着创建出Dom了,并且它的elm属性指向了创建出的dom,但是还没有上树
            let chDom = createElement(ch)
            //上树
            domNode.appendChild(chDom)
        }
    }
    //补充elm属性
    vnode.elm = domNode

    return vnode.elm
}
export default function patchVnode(oldVnode, newVnode) {
    //判断新旧vnode是不是同一个对象
    if(oldVnode === newVnode) return
    //判断新的vnode有没有text属性
    if(newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)){
        if(newVnode.text != oldVnode.text){
            //如果新的text和老的text不同,直接让新的text写入老的elm中,如果老的elm中有children也会消失掉
            oldVnode.elm.innerText = newVnode.text
        }
    }else {
        //新vnode没有text属性,意味着有children
        //判断老的有没有children
        if(oldVnode.children != undefined && oldVnode.children.length > 0){
            //老节点有children,新节点也有children,此时是最复杂的情况
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
        }else{
            //老的没有children,新的有children
            //清空老节点的内容
            oldVnode.elm.innerHTML = ''
            //遍历新vnode的子节点,创建DOM,上树
            for(let i = 0; i < newVnode.children.length;i++){
                let dom = createElement(newVnode.children[i])
                oldVnode.elm.appendChild(dom)
            }
        }
    }
}
//判断是否为同一个虚拟节点
function checkSameVnode(a,b){
    return a.sel == b.sel && a.key == b.key
}
export default function updateChildren(parentElm, oldCh, newCh){
    //旧前
    let oldStartIdx = 0
    //新前
    let newStartIdx = 0
    //旧后
    let oldEndIdx = oldCh.length - 1
    //新后
    let newEndIdx = newCh.length - 1
    //旧前节点
    let oldStartVnode = oldCh[0]
    //旧后节点
    let oldEndVnode = oldCh[oldEndIdx]
    //新前节点
    let newStartVnode = newCh[0]
    //新后节点
    let newEndVnode = newCh[newEndIdx]

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
        if(checkSameVnode(oldStartVnode, newStartVnode)){
            //新前与旧前
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if(checkSameVnode(oldEndIdx, newEndIdx)){
            //新后与旧后
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if(checkSameVnode(oldStartVnode, newEndVnode)){
            //新后与旧前
            patchVnode(oldStartVnode, newEndVnode)
            //当新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if(checkSameVnode(oldEndVnode, newStartVnode)) {
            //新前与旧后
            patchVnode(oldEndVnode, newStartVnode)
            //当新前与旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm.nextSibling)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else{
            //四种都没有命中
        }
    }
    //继续看看有没有剩余的
    if(newStartIdx <= newEndIdx){
        //插入的标杆
        const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm

        for (let i = newStartIdx; i <= newEndIdx; i++){
            //newCh[i]现在还没有真正的DOM,所以要调用createElement函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]), before)
        }
    }else if(oldStartIdx <= oldEndIdx){
        //批量删除oldStart和oldEnd指针之间的项
        for(let i = oldStartIdx; i <= oldEndIdx; i++){
            parentElm.removeChild(oldCh[i].elm)
        }
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值