第九节:vue3 Diff 算法 元素比较

152 阅读4分钟

更新的逻辑

  • 如果前后元素不一致, 删除老的 添加新的
  • 老的和新的一样,复用。属性可能不一样,比对属性更新属性
  • 比对子集

初始化和diff算法都在 上一章:第八节 实现的path方法

前后元素不一致

示例

render(h('h1',{style: {'color': 'red'},onClick: ()=>{alert(1)}},h('span','lyp'),'hello'),app)
setTimeout(()=>{
    render(h(Text, '111'),app)
},1000)

实现

// 是否是相同虚拟节点
export const isSameVNodeType = (n1, n2) => { 
    // 判断两个虚拟节点是否是同一节点:1、标签名一致 2、key是一样的
    return n1.type === n2.type && n1.key === n2.key;
}

const patch = (n1,n2,container) => {
    // 初始化和diff算法都在path方法

    // 没有更新
    if(n1 === n2) return

    // 1、有n1, n1和n2不是同一个节点, 卸载老的再添加新的
    if(n1 && !isSameVNodeType(n1,n2)){ 
        unmount(n1) // 删除老的
        n1 = null // 下边processText 会走创建
    }
    // 上一节实现过的
    const {type, shapeFlag} = n2
        switch(type){ // 如果是文本直接插入
            case Text:
                processText(n1,n2,container);
                break;
            default:
                // 是元素的话 渲染元素 
                if(shapeFlag & ShapeFlags.ELEMENT){
                    processElement(n1, n2, container)
                }
        }
}

前后元素一致

文本更新

示例

render(h(Text, '111'),app)
setTimeout(()=>{
    render(h(Text, '222'),app)
},1000)

实现

// 处理文本
const processText= (n1,n2,container) => {
    if(n1 === null){
        // 初始化流程
        n2.el = hostCreateText(n2.children)
        hostInsert(n2.el, container)
    }else {
        // 文本更新, 可以直接复用老的元素
        const el = n2.el = n1.el //n1.el 赋值给 n2.el
        if(n1.children !== n2.children){
            hostSetText(el, n2.children)  // 文本的更新
        }

    }
}

比较两个元素的属性和孩子节点

示例

render(h('h1',{style: {'color': 'red'}}, 'lyp'),app)
setTimeout(()=>{
    render(h('h1',{style: {'color': 'blue',background: 'red'}}, 'lyp'),app)
},1000)

入口

const patchElement = (n1, n2) => {
    let el = (n2.el = n1.el);
    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    patchProps(oldProps, newProps, el); // 比对新老属性
    patchChildren(n1, n2, el); // 比较元素的孩子节点
}
const processElement = (n1, n2, container) => {
    if (n1 == null) {
        mountElement(n2, container)
    } else {
        patchElement(n1, n2); // 比较两个元素
    }
}

属性比对实现

// 3、比对属性
const  patchProps = (oldProps, newProps, el) => {
    for(let key in newProps){  // 新的里边有直接用新的盖掉就好
        hostPatchProp(el,key,oldProps[key], newProps[key])
    }
    for(let key in oldProps){ // 如果老的里边有新的里边没有 把老的删掉
        if(newProps[key] === undefined){
            hostPatchProp(el,key,oldProps[key], undefined)  // null没有被认为是空
        }
    }
}

子元素比对实现

子元素 可能是 文本、null、数组

子元素比较情况
新儿子旧儿子操作方式
文本数组(删除老儿子,设置文本内容)
文本文本(更新文本即可)
文本(更新文本即可) 与上面的类似
数组数组(diff算法)
数组文本(清空文本,进行挂载)
数组(进行挂载) 与上面的类似
数组(删除所有儿子)
文本(清空文本)
(无需处理)
使用
// 文本 数组
render(h('h1',{style: {'color': 'red'}}, '333'),app)
setTimeout(()=>{
    render(h('h1',{style: {'color': 'blue',background: 'red'}}, ['111', '222']),app)
},1000)
// 文本 文本
render(h('h1',{style: {'color': 'red'}}, 'lyp'),app)
setTimeout(()=>{
    render(h('h1',{style: {'color': 'blue',background: 'red'}}, 'jdlyp'),app)
},1000)
// 文本 空
render(h('h1',{style: {'color': 'red'}}, 'lyp'),app)
setTimeout(()=>{
    render(h('h1',{style: {'color': 'blue',background: 'red'}}),app)
},1000)
// 数组 文本
render(h('h1',{style: {'color': 'red'}}, ['111', '222']),app)
setTimeout(()=>{
    render(h('h1',{style: {'color': 'blue',background: 'red'}}, '333'),app)
},1000)

render(h('h1',{style: {'color': 'red'}}),app)
setTimeout(()=>{
    render(h('h1',{style: {'color': 'blue',background: 'red'}}, h('span', 'jdlyp')),app)
},1000)
实现
  • 1、新节点是 文本
  • 2、新的 是数组
  • 3、新的既不是文本 也不是 数组,就剩下一种情况为空
// 删除所有子节点
const unmountChildren = (children) =>{
    for(let i = 0 ; i < children.length; i++){
        unmount(children[i]);
    }
}
// 比较元素的子节点
const patchChildren = (n1, n2, el) => {
    const c1 = n1 && n1.children
    const c2 = n2 && n2.children
    // 可能是 文本、null、数组
    // 比较两个 儿子节点的 差异

    // 文本	数组	(删除老儿子,设置文本内容)
    // 文本	文本	(更新文本即可)
    // 数组	数组	(diff算法)
    // 数组	文本	(清空文本,进行挂载)
    // 空	数组	(删除所有儿子)
    // 空	文本	(清空文本)

    const prevShapeFlag = n1.shapeFlag;
    const shapeFlag = n2.shapeFlag;
    if(shapeFlag & ShapeFlags.TEXT_CHILDREN){ // 1、新节点是 文本
        if(prevShapeFlag & ShapeFlags.ARRAY_CHILDREN){ //文本 数组
            unmountChildren(c1);  // 删除老的所有子节点
        }
        if(c1 !== c2){  // 文本 文本 设置文本(包括 文本 空)
            hostSetElementText(el,c2);
        }
    }else {
        if(prevShapeFlag & ShapeFlags.ARRAY_CHILDREN){  // 2、新的 是数组
            if(shapeFlag & ShapeFlags.ARRAY_CHILDREN){  // 数组 数组
                // diff算法
            }else{
                unmountChildren(c1);  //  数组 文本和null 、老的不是数组 删除以前的所有子节点
            }
        }else{ // 3、新的既不是文本 也不是 数组,就剩下一种情况为空
            if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN){ // 3.1、 老的是 文本
                hostSetElementText(el,'');  // 清空 文本
            }
            if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 3.2 老的是数组
                mountChildren(c2, el);   // 清空子节点
            }
        }
    }
}

下一章我们实现 新旧子集都是数组的 diff算法