vue2源码学习笔记6 diff算法的实现

目录

1。之前对于新老虚拟dom的操作

2. diff算法的比较方式:

3. 实现完整的diff算法


1。之前对于新老虚拟dom的操作

直接将老的节点替换为了新的节点,没有进行对比,会造成一些性能的浪费。

let template1 = `<li>{{age}}</li>`
let vm = new Vue({data : {age: 13}})
let render1 = compileToFunction(template1)
let preVNode = render1.call(vm)
let el = createElm(preVNode)
document.body.appendChild(el)

let template2 = `<span>{{age}}</span>`
let render2 = compileToFunction(template2)
let nextVNode = render2.call(vm)
setTimeout(() => {
    el.parentNode.replaceChild(createElm(nextVNode),el)
},1000)

这是以前的方式,生成模板根据模板抽象出ast语法树,模板引擎+call生成虚拟dom,虚拟dom生成真实dom后直接替换掉原来的老的节点

2. diff算法的比较方式:

1. 同级比较,如果父节点不同那么就不会比较子字节。
2. 两个节点不是同一个节点,key,tag不同直接替换掉没有比对
    (1)Vnode.js文件 方法isSameVnode来比较两个节点,key和tag都比
    (2)如果不一样用老节点父节点进行替换,因为之前已经把真实节点和虚拟节点对应了起来,

                所以在oldVnode的el属性上存放了真实节点
3. 两个节点是同一个节点,那么就比较属性把属性差异的进行更新
    新老节点复用,把老节点的el付给新节点的el
    (1)排除文本的情况,即tag == undefined,因为两个文本的tag都为undefined所以相同,
        如果新老文本不一样就替换,oldVNode.el.textConect = 新文本
    (2)比较标签属性,使用patchProps(el,vnode.data,oldVnode.data);里面的逻辑为
        老的样式有新的没有就清空,老的属性有新的没有就removeAttribute(key)
        新的节点新增的不用管他,因为函数原本的功能就是初始化属性会新加
4. 上面三步做完之后再比较子节点,有几种不同的情况,需要进行判断
    通过vnode的children属性拿值,有可能没有
    (1)两个都有子节点,需要对两个人的子节点进行比较
    (2)只有新节点有子节点,通过mountChildren()方法,循环子节点列表生成真实节点,然后通

        过appendChild方法挂载到el上

    (3)如果说老节点有子节点新的没有,就直接el.innerHTML = '' 清空掉即可

import {isSameVNode} from "./index";

export function patchProps(el,props,oldProps = {}) {
    // 老样式有,新样式没有就清空
    let newStyle = props.style || {}
    let oldStyle = oldProps.style || {}
    for (const key in oldStyle) {
        if(!newStyle[key]) newStyle[key] = ''
    }
    // 老节点有的属性,新节点没有
    for (const key in oldProps) {
        if(!props[key]) {
            props.removeAttribute(key)
        }
    }

    for (const key in props) {
        if(key === 'style') {
            for (const styleName in props.style) {
                el.style[styleName] = props.style[styleName]
            }
        }else {
            el.setAttribute(key,props[key])
        }
    }
}

export function createElm(VNode) {
    let {tag,data,children,text} = VNode
    if(typeof tag == 'string') {
        VNode.el = document.createElement(tag)
        // 弄一个方法去添加属性
        if(data) patchProps(VNode.el,data)
        // 循环递归添加子元素
        children.forEach(child => {
            VNode.el.appendChild(createElm(child))
        })
    }else {
        // 真实元素挂载到VNode上面与虚拟节点相对应
        VNode.el = document.createTextNode(text)
    }
    return VNode.el
}

export function patch(oldVNode,VNode) {
    let isRealDom = oldVNode.nodeType
    if(isRealDom) {
        let elm = oldVNode
        let parentElm = elm.parentNode
        let newElm = createElm(VNode)
        // 将新的元素放到老元素下面然后删掉老元素
        parentElm.insertBefore(newElm,elm.nextSibling)
        parentElm.removeChild(elm)
        return newElm
    }else {
        // 使用diff算法
        return patchVNode(oldVNode,VNode)
    }
}
function patchVNode(oldVNode,VNode) {
    // 父节点不相同的情况
    if(!isSameVNode(oldVNode,VNode)) {
        let el = createElm(VNode)
        oldVNode.el.parentNode.replaceChild(el,oldVNode.el)
        return el
    }
    // 父节点相同,子节点是文本的情况
    let el = VNode.el = oldVNode.el
    if(!VNode.tag) {
        if(VNode.text !== oldVNode.text){
            el.textContent = VNode.text
        }
    }else {
        // 父节点相同,子节点是元素的情况
        patchProps(el,VNode.data,oldVNode.data)
    }

    // 父节点都比较完了,再比较子节点
    let oldVNodeChildren = oldVNode.children || []
    let VNodeChildren = VNode.children || []
    if(oldVNodeChildren.length > 0 && VNodeChildren.length > 0) {
        // 都有子节点
    }else if(VNodeChildren.length > 0) {
        // 只有新的节点有子节点,那么就都挂载到el上
        mountChildren(el,VNodeChildren)
    }else {
        el.innerHTML = ''
    }
}

function mountChildren(el,children) {
    for (let i = 0; i < children.length; i++) {
        el.appendChild(createElm(children[i]))
    }
}

对于上述步骤的实现,都有子节点的情况如下 :

3. 实现完整的diff算法

1. 操作列表的时候经常会使用数组的方法来添加或者删除,如:pop,shift,unshift,push等 vue2就采用了一些方法去优化他。采用了双指针的方法去比较两个节点

如果是头节点并不一样,那么我们就从尾巴开始比起,添加的时候呢,不采用appendChild,因为这个方法只能尾部添加,我们可以到一个新的变量anchor来标识,他的后一个元素是否为一个节点。详情如下图:

 

第三种比较方式

第四种比较方式:

第五种比较方式:

        乱序比较,老的子节点列表和新的子节点列表,以上四个比较方式都不成功,就说明他是一个乱序,那么我们就搞一个印射表,然后再新的子节点拿到映射表比对,如果有那么就复用,同时调整老的子节点的位置,如下图所示:

        发现c在老的节点列表里面有,复用,把c放到头指针的前面,然后清空掉这个c的内容,然后新的子节点列表指针后移,继续对比,如果之前没有出现过这个那就生成后放到头指针前面 

代码实现如下:

function updateChildren(el,newVNode,oldVNode) {
    // 生成节点和双指针
    let oldStartIndex = 0
    let newStartIndex = 0
    let oldEndIndex = oldVNode.length - 1
    let newEndIndex = newVNode.length - 1

    let oldStartVNode = oldVNode[0]
    let newStartVNode = newVNode[0]
    let oldEndVNode = oldVNode[oldEndIndex]
    let newEndVNode = newVNode[newEndIndex]
    // 创建映射表
    function createMap(children) {
        let map = {}
        children.forEach((child,index) => {
            map[child.key] = index
        })
        return map;
    }
    let map = createMap(oldVNode)
    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        if(!oldStartVNode) {
            oldStartVNode = [++oldStartIndex]
            continue
        }
        if(!oldEndVNode) {
            oldEndVNode = [--oldEndIndex]
            continue
        }
        if(isSameVNode(oldStartVNode,newStartVNode)) {
            // 两个节点头头一样,递归调用patchVNode
            patchVNode(oldStartVNode,newStartVNode)
            oldStartVNode = oldVNode[++oldStartIndex]
            newStartVNode = newVNode[++newStartIndex]
            continue
        }
        if(isSameVNode(oldEndVNode,newEndVNode)) {
            // 两个节点尾尾一样
            patchVNode(oldEndVNode,newEndVNode)
            oldEndVNode = oldVNode[--oldEndIndex]
            newEndVNode = newVNode[--newEndIndex]
            continue
        }
        if(isSameVNode(oldEndVNode,newStartVNode)) {
            // 两个节点尾头一样
            patchVNode(oldEndVNode,newStartVNode)
            el.insertBefore(oldEndVNode.el,oldStartVNode.el)
            oldEndVNode = oldVNode[--oldEndIndex]
            newStartVNode = newVNode[++newStartIndex]
            continue
        }
        if(isSameVNode(oldStartVNode,newEndVNode)) {
            // 两个节点头尾一样
            patchVNode(oldStartVNode,newEndVNode)
            el.insertBefore(oldStartVNode.el,oldEndVNode.el.nextSibling)
            oldStartVNode = oldVNode[++oldStartIndex]
            newEndVNode = newVNode[--newEndIndex]
            continue
        }

        let moveIndex = map[newStartVNode.key]
        if(moveIndex !== undefined) {
            let moveVNode = oldVNode[moveIndex]
            el.insertBefore(moveVNode.el,oldVNode[oldStartIndex].el)
            oldVNode[moveIndex] = undefined
            patchVNode(moveVNode,newStartVNode)
        }else {
            el.insertBefore(createElm(newStartVNode),oldStartVNode.el)
        }
        newStartVNode = newVNode[++newStartIndex]
    }
    /**
     * 如果新的虚拟dom的头节点小于等于其尾节点,那就说明老的虚拟dom
     * 的子节点没有新的虚拟dom多(建立在都相同的基础上)那么就需要添加上,头插还是尾插
     * 取决于,anchor属性也就是头节点后有没有新的真实节点
     * 反之老的头节点小于尾节点,则需要删除
     * */
    if(newStartIndex <= newEndIndex) {
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            let childEl = createElm(newVNode[i])
            let anchor = newVNode[newEndIndex + 1] ? newVNode[newEndIndex + 1].el : null
            el.insertBefore(childEl,anchor)
        }
    }
    if(oldStartIndex <= oldEndIndex) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            if(oldVNode[i]) {
                let removeEl = oldVNode[i].el
                el.removeChild(removeEl)
            }
        }
    }
}

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值