最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 谈谈我对Vue中diff算法的理解|七日打卡

    正文概述 掘金(早上吃包子)   2021-01-14   421

    前言

    我认为diff算法具备两个特点。
    一、高效性:有虚拟dom,必然需要diff算法。通过对比新旧虚拟dom,将有变化的地方更新在真实dom上,另外,通过diff高效的执行比对过程,从而降低时间复杂度为O(n)。
    二、必要性:vue2中为了降低watcher粒度,每个组件只有一个watcher。通过diff精确找到发生变化的节点,并复用相同的节点。
    下面我们通过手写简易diff算法,来看看具体是怎么实现的吧?

    比较新旧虚拟节点

    patch(vnode,newVnode)分为以下几种情况

    1.标签名不一样,直接换掉老节点

    可以通过vnode.el 属性,获取真实的dom元素

     if(oldVnode.tag !== vnode.tag){
        oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
     }
    

    2.都是文本节点,比较文本内容

    如果新老文本不相等,el.textContent=vnode.text

    // 如果标签一致但是不存在则是文本节点
    if(!oldVnode.tag){
        if(oldVnode.text !== vnode.text){
        	oldVnode.el.textContent = vnode.text;
        }
    }
    

    3.标签一样

    复用老真实节点 vnode.el=oldnode.el

    1.比较属性 patchProps

    let el = vnode.el = oldVnode.el;
    function patchProps(vnode, oldProps = {}) { // 初次渲染时可以调用此方法,后续更新也可以调用此方法
        let newProps = vnode.data || {};
        let el = vnode.el;
        // 如果老的属性有,新的没有直接删除
        let newStyle = newProps.style || {};
        let oldStyle = oldProps.style || {};
        for (let key in oldStyle) {
            if (!newStyle[key]) { // 新的里面不存在这个样式
                el.style[key] = '';
            }
        }
        for (let key in oldProps) {
            if (!newProps[key]) {
                el.removeAttribute(key);
            }
        }
        // 直接用新的生成到元素上
        for (let key in newProps) {
            if (key === 'style') {
                for (let styleName in newProps.style) {
                    el.style[styleName] = newProps.style[styleName];
                }
            } else {
                el.setAttribute(key, newProps[key]);
            }
        }
    }
    

    2.比较子节点

     let oldChildren = oldVnode.children || [];
            let newChildren = vnode.children || [];
    
            if (oldChildren.length > 0 && newChildren.length > 0) {
                // 双方都有子节点
    
                //  vue用了双指针的方式 来比对 
                patchChildren(el, oldChildren, newChildren);
            } else if (newChildren.length > 0) { 
            // oldVnode 无子节点,newVnode 有子节点
                for (let i = 0; i < newChildren.length; i++) {
                    let child = createElm(newChildren[i]);
                    el.appendChild(child); // 循环创建新节点
                }
    
            } else if (oldChildren.length > 0) { 
            // oldVnode 有子节点,newVnode 无子节点
                el.innerHTML = ``; // 直接删除老节点
            }
    
    都有子节点patchChildren

    谈谈我对Vue中diff算法的理解|七日打卡

    
    function patchChildren(el, oldChildren, newChildren) {
        let oldStartIndex = 0;
        let oldStartVnode = oldChildren[0];
        let oldEndIndex = oldChildren.length - 1;
        let oldEndVnode = oldChildren[oldEndIndex];
        let newStartIndex = 0;
        let newStartVnode = newChildren[0];
        let newEndIndex = newChildren.length - 1;
        let newEndVnode = newChildren[newEndIndex];
    
        const makeIndexByKey = (children)=>{
            return children.reduce((memo,current,index)=>{
                if(current.key){
                    memo[current.key] = index;
                }
                return memo;
            },{})
        }
        const keysMap = makeIndexByKey(oldChildren);
         // 同时循环新的节点和 老的节点,有一方循环完毕就结束了
         //开头指针和结尾指针重合,则比对完毕
        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];
            } 
            // 头尾比较  =》 reverse
            else if(isSameVnode(oldStartVnode,newEndVnode)){
                patch(oldStartVnode,newEndVnode);
                el.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling); // 移动老的元素,老的元素就被移动走了,不用删除
                oldStartVnode = oldChildren[++oldStartIndex];
                newEndVnode = newChildren[--newEndIndex];
            }
            else if(isSameVnode(oldEndVnode,newStartVnode)){ // 尾头比较
                patch(oldEndVnode,newStartVnode);
                el.insertBefore(oldEndVnode.el,oldStartVnode.el);
                oldEndVnode = oldChildren[--oldEndIndex];
                newStartVnode = newChildren[++newStartIndex];
            }else{
                // 乱序比对   核心diff
                // 1.需要根据key和 对应的索引将老的内容生成程映射表
                let moveIndex = keysMap[newStartVnode.key]; // 用新的去老的中查找
                if(moveIndex == undefined){ // 如果不能复用直接创建新的插入到老的节点开头处
                    el.insertBefore(createElm(newStartVnode),oldStartVnode.el);
                }else{
                    let moveNode = oldChildren[moveIndex];
                    oldChildren[moveIndex] = null; // 此节点已经被移动走了
                    el.insertBefore(moveNode.el,oldStartVnode.el);
                    patch(moveNode,newStartVnode); // 比较两个节点的属性
                }
                newStartVnode = newChildren[++newStartIndex]
            }
        }
        // 如果用户追加了一个怎么办?  
    
        // 这里是没有比对完的
        if (newStartIndex <= newEndIndex) {
            for (let i = newStartIndex; i <= newEndIndex; i++) {
                // el.appendChild(createElm(newChildren[i]))  
                // insertBefore方法 他可以appendChild功能 insertBefore(节点,null)  dom api
    
                //  看一下为指针的下一个元素是否存在
                let anchor = newChildren[newEndIndex + 1] == null? null :newChildren[newEndIndex + 1].el
                el.insertBefore(createElm(newChildren[i]),anchor);
            }
        }
        if(oldStartIndex <= oldEndIndex){
            for (let i = oldStartIndex; i <= oldEndIndex; i++) {
                //  如果老的多 将老节点删除 , 但是可能里面有null 的情况
                if(oldChildren[i] !== null) el.removeChild(oldChildren[i].el);
            }
        }
    
    }
    
    开始循环新老节点,有一方循环完毕就结束
    if 旧头==新头 isSameVnode

    patch(旧头,新头) 递归 双头指针后移一位

    else if 旧尾==新尾

    patch(旧尾,新尾) 递归 双尾指针向前移一位

    else if 旧头==新尾

    parentElm.insertBefore( oldStartVnode.elm, oldEndVnode.elm.nextSibling ); 旧头.el 移到老尾指针真实节点后面 旧头指针 后移 新尾指针 前移

    else if 旧尾==新头

    parentElm.insertBefore( oldEndVnode.elm, oldStartVnode.elm );

    旧尾指针 前移 新头指针 后移

    else if 都没匹配上 对比查找
    1. 根据 key 和对应的索引将老的内容生成映射表

    2. 用新 key 去旧 key 中查找

      • 没找到-直接创建新 dom 的插入到旧节点的开头处

      • 找到了-插入,并比较两个节点的子节点,把移动的节点插入到旧节点,移动走的节点=null,指针碰到 null,跳过,直接向后/前移一位

    循环结束

    看看是哪对指针重合没有比对完的,处理可能剩下的节点

    • 旧指针重合 新指针越界 oldVnode 还有多余没比对

    旧节点进行 for 循环批量删除 el.removeChild(oldChildren[i].el);

    • 新指针重合 旧指针越界 Vnode 还有多余没比对

    看一下新尾指针的下一个元素 newChildren[newEndIndex + 1]是否存在 不存在 直接插入,插在当前新尾真实节点的前面 全部新建,for 循环插入

    更新操作

    Vue.prototype._update = function (vnode) {
        const vm  = this;
        const prevVnode = vm._vnode; // 保留上一次的vnode
        vm._vnode = vnode;
        if(!prevVnode){
            vm.$el = patch(vm.$el,vnode); // 需要用虚拟节点创建出真实节点 替换掉 真实的$el
            // 我要通过虚拟节点 渲染出真实的dom     
        }else{
            vm.$el = patch(prevVnode,vnode); // 更新时做diff操作
        }
    }
    

    小结

    diff过程遵循深度优先、同层比较的策略。两个节点之间会根据它们是否拥有子节点或者文本节点做不同的擦欧总。比较两组子节点是算法的重点,首先假设头尾节点可能相同作四次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点。借助key通常可以非常精准找到相同节点,因此整个patch过程非常高效。 不过值得注意的一点,在对于需要增删的数据,我们不建议使用index作为key


    起源地下载网 » 谈谈我对Vue中diff算法的理解|七日打卡

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元