手写vue-diff算法(二)

diff 算法简介

1.diff算法-同层比较

  • 在日常开发中,很少发生将父亲节点和儿子节点进行交换的场景
  • 性能瓶颈
    综合这两点,diff算法仅对同层节点进行对比

2.比较方式

dom是一个树型结构,diff算法是将新、老两个虚拟节点树进行比对。
从根节点开,同层对比,diff比对是“深度优先遍历”的递归比对

  • 两个节点不是同一个节点,直接删除老的,换上新的
  • 两个节点是同一个节点,复用老节点,将差异属性更新
  • 节点比较完成后,比较两个节点的儿子节点
Vue2与Vue3在节点更新时的性能对比:
1.递归比对是vue2的性能瓶颈,当组件树庞大时会产生性能问题;
2.在vue3中,会收集动态节点,并对他们的变化进行标记,根据标记进行更新,而无需使用diff递归比对一遍
3.Vue3是线性比对,而Vue2是两棵树的比对,效率上会比vue2高出很多;

3.如何确定两个节点是同一个节点

标签相同并且key值相同

一般节点没有key值,标签相同既可以复用,当标签相同不希望复用时可以使用key属性对节点进行标记,key值不相同,即使标签名相同的两个元素,也不会进行复用。

// src/vdom/index.js

/**
 * 判断两个虚拟节点是否是同一个虚拟节点
 * @param {*} newVnode 新虚拟节点
 * @param {*} oldVnode 老虚拟节点
 * @returns 
 */
export function isSameVnode(newVnode, oldVnode){
  // 判断逻辑:tag 标签名 和 key 完全相同
  return (newVnode.tag === oldVnode.tag)&&(newVnode.key === oldVnode.key); 
}

实现diff算法

整体流程:

  • 不是同一节点直接替换
  • 是同一节点
    • 特殊情况:文本,文本的tagundefined,判断如果节点的tagundefined,则直接用新的文本替换老的文本。
    • 正常节点
      • 属性替换
        调用patchProps方法,需要对style进行单独处理
      • 对比儿子节点,判断五种情况

1.不是同一节点直接替换

根据isSameVnode方法判断是否为同一节点

function patchVNode(oldVNode, vnode) {
    if (!isSameVnode(oldVNode, vnode)) {
        let el = createElm(vnode)
        oldVNode.el.parentNode.replaceChild(createElm(vnode), oldVNode.el)
        return el
    } else {
        return 
    }
}

2.是同一节点

2.1文本情况

由于vNodeoldVNode的tag相同,只需要判断其中一个的tagundefined即可

        // 将老节点赋值给新节点
        let el = vnode.el = oldVNode.el;
        // 相同可能是文本的情况 文本的tag是undefined
        if (!oldVNode.tag) {
            if (oldVNode.text !== vnode.text) {
                el.textContent = vnode.text;//用新的文本覆盖旧的文本
            }
        }

2.2属性替换

重构patchProps方法

对于style需要进行特殊处理,如果新旧节点都有stylestyle的属性值为字符串类型,不能直接进行替换,需要对样式属性进行收集,再进行比较和更新;

export function patchProps(el, oldProps = {}, props = {}) {
    let oldStyles = oldProps.style || {};
    let newStyles = props.style || {};
    
    for (let key in oldStyles) { // 老的样式中有 新的没有则删除
        if (!newStyles[key]) {
            el.style[key] = ''
        }
    }

    for (let key in oldProps) { // 老的属性中有
        if (!props[key]) { // 新的没有删除属性
            el.removeAttribute(key);
        }
    }
    for (let key in props) { // 用新的覆盖老的
        if (key === 'style') { // style{color:'red'}
            for (let styleName in props.style) {
                el.style[styleName] = props.style[styleName];
            }
        } else {
            el.setAttribute(key, props[key]);
        }
    }
}

2.3对比儿子节点

  • 新的有儿子,老的没有,直接赋值
  • 新的没有儿子,老的有,直接删除
  • 新的老的都有,进行处理
        let oldChildren = oldVNode.children || [];
        let newChildren = vnode.children || [];


        if (oldChildren.length > 0 && newChildren.length > 0) {
            // 完整的diff算法 需要比较两个人的儿子
            updateChildren(el, oldChildren, newChildren);

        } else if (newChildren.length > 0) { // 没有老的,有新的
            mountChildren(el, newChildren);
        } else if (oldChildren.length > 0) { // 新的没有  老的有 要删除
            el.innerHTML = ''; // 可以循环删除
        }
        return el

添加节点

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

在真正执行diff比对前,针对于【情况 1】“老的有儿子,新的没有儿子”和【情况 2】“老的没有儿子,新的有儿子”这两种特殊情况,优先进行了特殊处理;

当以上两种情况均不满足,即【情况 3】新老节点都有儿子时,就必须进行diff比对了;

所以,updateChildren方法才是diff算法的核心;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值