vue diff算法与虚拟dom知识整理(13) 手写patch子节点更新换位策略

上一文中我们编写了 patch中新旧节点都有子节点的 插入节点的逻辑
但旧节点的子节点发生顺序 或数量变化 我们还没有处理 那我们现在继续

我们先来看看 原本是怎么写的
我们打开我们的案例 找到 node_modules 下面的snabbdom/src下面的 init.ts文件
在这里插入图片描述
我们在里面找到一个 updateChildren 函数
在这里插入图片描述
这里 我们看到 这里定义了各种 新前 旧前之前变量的定义
在这里插入图片描述
然后下面还是开启循环 疯狂判断 新前旧前一不一样 然后 新前旧后是否相同等等
真的不同情况做不同处理
在这里插入图片描述
好啦 我们自己也来写一个吧

我们在案例的src跟目录下的snabbdom下创建一个patchVNode.js
然后我们打开 patch.js 找到

if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
    
    

将里面的代码拿到patchVNode.js中
在这里插入图片描述

patchVNode.js参考代码如下

import createElement from "./createElement";
export default function(newVnode, oldVnode) {
    
    
    //判断  如果 新旧虚拟节点完全相同 则不做操作  直接返回
    if(oldVnode === newVnode) return;
    /*
        判断新节点的text是不是空的  包括就算text是空子符串  也算有 只要不是undefined未定义
        同时  也要判断  要newVnode没有children  或 children 是个空数组
    */
    if(newVnode.text != undefined&&(!newVnode.children||!newVnode.children.length)) {
    
    
        //确定一下新旧节点的text文本是不是不同  如果相同就什么都不用做了
        if(newVnode.text !== oldVnode.text) {
    
    
            //利用innerText将新节点的text写入到老节点的elm中 因为  elm  存的是真实的dom
            oldVnode.elm.innerText = newVnode.text;
        }
    }else{
    
    
    //否则就算新节点没有text
        //判断老节点有没有children
        if(oldVnode.children&&oldVnode.children.length) {
    
    
            //当新旧节点都有子节点
            //定义un用于记录当前更新的元素下标
            let un = 0;
            //遍历写节点的子元素
            newVnode.children.map((news,index) => {
    
    
                //定义 isExist  判断新节点中有没有和旧节点一样的元素
                let isExist = false;
                //遍历旧节点子集
                oldVnode.children.map(old => {
    
    
                    //判断  如果有一模一样的元素  什么都不用做
                    if(old.sel == news.sel &&old.key == news.key) {
    
    
                        isExist = true;
                    }
                })
                //判断  如果在旧节点中没有找到相同的节点 就输出出来
                if(!isExist){
    
    
                    //通过createElement将虚拟节点变成真正的孤儿节点
                    let dom = createElement(news);
                    //当当前下标的子节点的elm属性记录上这个新创建的孤儿节点
                    news.elm = dom;
                    //判断un是不是已经大于了子节点创的   如果是 表示这个下标在老节点找不到
                    if(un < oldVnode.children.length) {
    
    
                        //在老节点对应un下标的前面插入这个节点
                        oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm);
                    }else{
    
    
                        //直接在老节点的最后面插入
                        oldVnode.elm.appendChild(dom);
                    }
                }else{
    
    
                    //否则就表示 本次循环中 子元素找到了相同的元素 将un+1
                    un++;
                }
            })
        } else {
    
    
            //新的有children  老的没有
            //通过innerHTML 清空老节点中的内容
            oldVnode.elm.innerHTML = "";
            //二者都有children  就算最复杂的情况
            newVnode.children.map(item => {
    
    
                let dom = createElement(item);
                oldVnode.elm.appendChild(dom);
            })
        }
    }
}

简单说 就是 将 判断为同一节点之后的 精细化处理的逻辑 全部搬到了patchVNode中

然后 我们patch.js 引入一下patchVNode

import patchVNode from "./patchVNode";

然后 刚才去掉的逻辑还是有用的
我们在去掉代码的位置调用patchVNode

patchVNode(newVnode, oldVnode);

在这里插入图片描述
当然 我们还是要验证的 看看一切是不是都还正常
我们改src下的index.js入口文件代码如下

import h from "./snabbdom/h";
import patch from "./snabbdom/patch";

const container = document.getElementById("container");

const vnode = h("section", {
    
    }, [
  h("p", {
    
    key:"a"}, "a"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"c"}, "c")
]);

patch( container, vnode)

const btn = document.getElementById("btn");
const vnode1 = h("section", {
    
    },[
  h("p", {
    
    key:"a"}, "a"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"c"}, "c"),
  h("p", {
    
    key:"d"}, "d")
]);

btn.onclick = function(){
    
    
  patch( vnode, vnode1)
}

运行项目
在这里插入图片描述
点击更改dom
在这里插入图片描述
可以看到功能还是一切正常的

然后 在src跟目录下的snabbdom下创建一个updateChildren.js

updateChildren中那些逻辑 我们就往这里写

我们现在 updateChildren 中写上这些代码

import patchVNode from "./patchVNode";

//判断是否为同一个虚拟节点
function checkSameVnode(a,b) {
    
    
    return a.sel == b.sel && a.key == b.key;
}

export default function(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循环语句 来处理
        只要  旧前小于等于旧前 而且 新前小于等于新后 他就会一直执行
        也可以理解为  只要 新前大于了新后   或者  旧前大于了旧后  这个循环就停了
    */
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
        //判断 新节点和旧节点的第一个子节点是否相同  或者是 判断  旧前节点和新前节点是不是同一个虚拟节点
        if(checkSameVnode(oldStartVnode,newStartVnode)) {
    
    
            //直接用patchVNode 对他进行精细化比较
            patchVNode(newStartVnode,oldStartVnode);
            //将新前和旧前都  +1  新前和旧前节点都下一一个节点指向
            oldStartVnode = oldch[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
    }
}

这里 我们大概了逻辑 就是 在最开始定义好需要用来判断的 新前旧前 指向节点这些
然后 我们开启循环 处理所有的子节点
这里只写了第一判断 我们判断 新节点的第一和旧节点的第一个子节点 是不是同一个虚拟节点 如果是 就调用patchVNode继续去做精细化比较
这样 他们之前就递归起来了

然后 我们来到
patchVNode.js
先引入updateChildren

import updateChildren from "./updateChildren";

然后 我们找到

if(oldVnode.children&&oldVnode.children.length) {
    
    

将里面的代码去掉
然后调用updateChildren

updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);

在这里插入图片描述
简单说 后面子节点之前的精细化比较我们自己不做了 我们交给updateChildren来做

然后 我们将 src下的index代码更改如下

import h from "./snabbdom/h";
import patch from "./snabbdom/patch";

const container = document.getElementById("container");

const vnode = h("section", {
    
    }, [
  h("p", {
    
    key:"a"}, "a"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"c"}, "c")
]);

patch( container, vnode)

const btn = document.getElementById("btn");
const vnode1 = h("section", {
    
    },[
  h("p", {
    
    key:"a"}, "AAA"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"c"}, "c")
]);

btn.onclick = function(){
    
    
  patch( vnode, vnode1)
}

可以看到 这些我们更改了 第一个a节点的文本
然后运行项目
在这里插入图片描述
点击更改dom

在这里插入图片描述
这样 第一个节点的精细化比较 我们就写好了

然后可以将index.js代码更改如下

import h from "./snabbdom/h";
import patch from "./snabbdom/patch";

const container = document.getElementById("container");

const vnode = h("section", {
    
    }, [
  h("p", {
    
    key:"a"}, "a"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"c"}, "c")
]);

patch( container, vnode)

const btn = document.getElementById("btn");
const vnode1 = h("section", {
    
    },[
  h("p", {
    
    key:"a"}, [
    h("p", {
    
    }, "123"),
    h("p", {
    
    }, "3123"),
    h("p", {
    
    }, "121231233")
  ]),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"c"}, "c")
]);

btn.onclick = function(){
    
    
  patch( vnode, vnode1)
}

运行项目
在这里插入图片描述
点击更改dom

在这里插入图片描述
简单说 逻辑就是 两个节点进入patch
因为 他们都没有定义key 都是 section 标签
所以 达到了相同节点判断 走进了patchVNode
在这里插入图片描述
然后 因为 新旧节点都是没有文本 过掉第一个条件 然后 判断他们的子节这个条件
走进了 updateChildren
在这里插入图片描述
因为 新旧节点的子节点 第一个都是 p标签 key都是a 所以 走进 了这个条件 又一次用 两个第一个子节点作为参数 进行精细化比较
在这里插入图片描述
然后这里判断 我们新节点的a里面是三个子节点 所以这里判断 text的达不到
然后第二个条件 但我们旧节点的a里面是文本 没有子节点
就走到了最后的这个else
然后我们清空旧节点内容
将新节点的子节点插进去
在这里插入图片描述
然后 updateChildren.js 循环中if多加一个条件

else if(checkSameVnode(oldEndVnode, newEndVnode)) {
    
    
    //将两个节点的最后一个节点做精细化比较
    patchVnode(newEndVnode,oldEndVnode);
    //新后旧后节点与表示向前推一个
    oldEndVnode = oldch[--oldEndIdx];
    newEndVnode = newCh[--newEndIdx];
}

在这里插入图片描述
我们对 新后和旧后节点的一个处理

然后我们运行项目
在这里插入图片描述
然后点击 更改dom
在这里插入图片描述
这样 最后一个节点的精细化比较也出来了

然后 就是一个比较神奇的操作 判断旧前 和 新后
就是判断 旧节点的第一个子元素 在你新节点上 是不是跑最后面去了

我们在 updateChildren.js 循环中if多加一个条件

//然后判断  旧前与新后是不是同一个节点
}else if(checkSameVnode(oldStartVnode, newEndVnode)) {
    
    
    //拿旧后和新前做精细化比较
    patchVNode(oldStartVnode, newEndVnode);
    //用parentElm父级调用insertBefore将旧前节点  插入到旧后节点的下一个兄弟节点的前面
    parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
    //后移旧前节点
    oldStartVnode = oldch[++oldStartIdx];
    //前移新后节点
    newEndVnode = newCh[--newEndIdx];
}

在这里插入图片描述

然后 我们将 src下的index.js代码修改如下

import h from "./snabbdom/h";
import patch from "./snabbdom/patch";

const container = document.getElementById("container");

const vnode = h("section", {
    
    }, [
  h("p", {
    
    key:"a"}, "a"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"c"}, "c")
]);

patch( container, vnode)

const btn = document.getElementById("btn");
const vnode1 = h("section", {
    
    },[
  h("p", {
    
    key:"c"}, "c"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"a"}, "a")
]);

btn.onclick = function(){
    
    
  patch( vnode, vnode1)
}

我们将两个节点的子节点顺序 完全颠倒过来
在这里插入图片描述
我们点击更改dom
在这里插入图片描述
这样 我们的顺序颠倒就可以了

简单讲一下逻辑 第一次进来 判断新后 旧前 他们都是 p标签 key为a
所以 拿旧前和新后做精细化比较的做小化更新 保证旧前的内容变的和新后一样
然后 这里涉及到一个 insertBefore 的知识点 如果你放入的新节点 是没有上dom树的 他会帮你插入 如果已经上树了 那么 就会变成移动
因为旧节点肯定是上过树的 所以 elm 代表这个节点在dom树上的节点
所以 这里的逻辑是 将 旧前的elm移动到旧后节点的elm的后一个兄弟节点的前面 你也可以简单理解为插入到旧后节点的后面 不需要担心这个兄弟节点拿不到 拿不到一样可以插进去
那么 此时旧后还是c节点 他后面没有节点了 所以 a就被移动到了最后面

然后第二次
新后因为被 – 指向了a的上一个节点 旧前也被下移 指向了旧节点的下一个子节点
因此 又是条件3达到了 旧前 p标签 key为b 新后 p标签 b
然后精细化处理两个节点
然后 将b插入到旧后节点的前面 因为我们并没有更改旧后节点 因为我们条件三 只是将旧前下移 新后上移
所以 尽管你把a已经插入在了c后面 c还是这个旧后节点 这样 你将b插入到 c的后一个兄弟节点 的前面 这次去那c的下一个兄弟节点有了 就是a节点 所以 b会插入在a的前面
这样 就节点的顺序就变成了 看到的 c b a
但还没完 还有一次循环呢 c进来
此时 第一个条件达到了
旧前和新前是一样的 都是这个c节点
对他们做一下精细化比较 更新c节点内容

然后就达到了我们现在的效果

然后 就还有最后一种情况
旧后与新前的匹配

//判断当旧后和新前为同一个节点时
}else if (checkSameVnode(oldEndVnode, newStartVnode)) {
    
    
    //对旧后与新前最精细化比较
    patchVNode(oldEndVnode, newStartVnode);
    //还是那个insertBefore特性 dom节点存在就是移动 不是插入  将 旧后移动到旧前的前面
    parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
    //将旧后节点前移
    oldEndVnode = oldch[--oldEndIdx];
    //将新前节点后移
    newStartVnode = newCh[++newStartIdx];
}

在这里插入图片描述
然后 我们将 src下的index改成这样

import h from "./snabbdom/h";
import patch from "./snabbdom/patch";

const container = document.getElementById("container");

const vnode = h("section", {
    
    }, [
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"a"}, "a"),
  h("p", {
    
    key:"c"}, "c")
]);

patch( container, vnode)

const btn = document.getElementById("btn");
const vnode1 = h("section", {
    
    },[
  h("p", {
    
    key:"c"}, "c"),
  h("p", {
    
    key:"b"}, "b"),
  h("p", {
    
    key:"a"}, "a")
]);

btn.onclick = function(){
    
    
  patch( vnode, vnode1)
}

在这里插入图片描述
我们点击更改dom
在这里插入图片描述
也是达到了效果 捋一下逻辑
第一次 达到了条件四 旧后和新前都是 p key为c
先对 旧后和新前做一下精细化比较
然后执行insertBefore 将旧后插入到旧前前面去
然后 将旧后向前 新前向后 加一个

然后 第二次进来 旧前没有改变 而新前在前面的逻辑被加一 指向了b 此时 新前和旧前都是b 就触发了第一个
然后后面的一次 两个a也是相同的都是做第一个 双前相同逻辑处理了

但目前还是存在一些逻辑漏洞 有时会死循环 这个 我们下次继续

猜你喜欢

转载自blog.csdn.net/weixin_45966674/article/details/130916337