Vue的虚拟DOM与diff算法

真实DOM与虚拟DOM:

虚拟DOM:用JS对象描述DOM的层次结构。真实DOM中的一切属性都在虚拟DOM中有对应的属性 

diff核心:精细化对比、最小量更新

新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出如何最小量更新,然后反映在真实DOM上

 真实DOM => 模版编译 => 虚拟DOM

 

 

1、h函数(产生虚拟节点VNode)调用h得到虚拟节点

复制代码
//调用:
h('a', { props: { href: 'http://www.baidu.com' }}, '百度');

//得到虚拟DOM:
{ 
  "sel": "a", 
  "data": { props: { href: 'http://www.baidu.com' } },
  "text": "百度" 
}

//表示的真实DOM:
<a href="http://www.baidu.com">百度</a>
复制代码

 

 

 

 

2、虚拟节点所包含的属性

复制代码
{
    children: undefined
    data: {}
  //elm为虚拟节点所对应的真实节点,若为undefined则是没有上DOM树 elm: undefined
  //key唯一标识 key: undefined sel:
"div" text: "我是一个盒子" }
复制代码

 

 

 

 

3、简单的虚拟节点上树流程

复制代码
//创建patch函数,模块采用snabbdom
const patch = init([
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
]);

//创建虚拟节点
var firstVNode = h("a", { props: { href: "http://www.baidu.com" } }, "百度");
console.log(firstVNode);

//虚拟节点上树
const container = document.getElementById("container");
patch(container, firstVNode);
复制代码

 

4、h函数的嵌套使用

复制代码
//嵌套h函数
const thirdVNode = h("ul", {}, [
  h("li", {}, "牛奶"),
  h("li", {}, [
    h("p", {}, "雪碧"), 
    h("p", {}, "芬达")
  ]),
  h("li", {}, "可乐"),
]);

//得到虚拟DOM
{
"sel": "ul",
"data": {},
"children":
 [ 
  { "sel": "li", "text": "牛奶" },
  { "sel": "li", 
    {
        "children":
         [ 
          { "sel": "p", "text": "雪碧" },
          { "sel": "p", "text": "芬达" },
 ] 
} },
  { "sel": "li", "text": "可乐" } 
 ] 
}            
复制代码

 

5、h函数实现

  h函数的一些重要点(snabbdom为例)

  • h函数返回一个vnode

    

  • vnode函数将接收的参数返回成对象

    

 

 

  •  同理,内部children也不断创建vnode节点

    

  •  h函数,如果是js类型下则需要模拟重载(函数的方法名相同,但参数不同,返回的类型不同),判断参数的含义;ts版直接重载

    

  •  手写简单的h函数
复制代码
//以下vnode函数只返回对象,不做diff处理
//这里只考虑接受三个参数,弱重载
// 形态 h('div', {}, '文字')
// 形态 h('div', {}, [])
// 形态 h('div', {}, h())
export default function (sel, data, c) {
  if (arguments.length != 3) {
    throw new Error("请输入三个参数");
  }
  //检查c类型
  //如果是文字
  if (typeof c == "string" || typeof c == "number") {
    //按序返回
    return vnode(sel, data, undefined, c, undefined);
  }
  //如果是数组
  else if (Array.isArray(c)) {
    let children = [];
    //先遍历c
    for (let i = 0; i < c.length; i++) {
      //首先c[i]必须是对象,其实在这就是考虑是h函数
      if (!(typeof c[i] == "object" && c[i].hasOwnProperty("sel"))) {
        throw new Error("传入的数组参数中,需要为h函数");
      }
      //收集好数组的h函数,整理children
      children.push(c[i]);
    }
    return vnode(sel, data, children, undefined, undefined);
  }
  //如果是h函数,即对象,且有sel属性
  else if (typeof c == "object" && c.hasOwnProperty("sel")) {
    //虽然他只有一个children,但是为了统一,还是要把他放到数组里
    let children =[c]
    return vnode(sel, data, children, undefined, undefined);
  } else {
    throw new Error("请传入正确类型的参数");
  }
}
复制代码

 

6、diff算法

  1. key是唯一标识,diff算法对比更新DOM的时候看的就是key,而不是内容。如果没有key,当在前排插入节点的时候,全部节点将会重排重绘(涉及元素的几何更新时,叫重排。而只涉及样式更新而不涉及几何更新时,叫重绘。对于两者来说,重排必定引起重绘,但是重绘并不一定引起重排)

  2、只有是同一个虚拟节点下(父级标签相同且key相同),才进行精细化比较 ,否则就是暴力删除旧的、插入新的。比如原来是ul,下面是li,然后改成了ol下面还是li,且ol和ul的key相同,其也会全部更新

  3、只进行同层比较,不会跨层比较,否则就是暴力删除旧的、插入新的。。即本来children只有一层,新节点有两层children,diff直接全部更新children

  4、diff遇到不一样的也不是直接拆了重建,比如他会判断是不是只是移动位置,从而交换位置,而不是删除再新增

 

7、diff算法的实现:

  1、patch函数

  

  • patch首先初始化节点,当老节点不是虚拟节点的时候(第一次使用的时候),先让他变成虚拟节点(和老师的比,新版多了文档片段的转换

    

     

  •  再判断新老节点是否为同一节点,如果是则精细化diff,如果不是则直接暴力拆除,递归创建子节点

    

  •  判断方式:sel、is和key相同(和老师的比,新版多了data内的is)

    

  •  手写patch函数(包含createElm)

  

 

复制代码
//patch函数
import createElm from "./createElm";
import vnode from "./vnode";

export default function (oldVNode, newVNode) {
  //判断老节点是否为vnode
  if (oldVNode.sel == "" || oldVNode.sel == undefined) {
    //DOM节点要包装成虚拟节点
    oldVNode = vnode(
      oldVNode.tagName.toLowerCase(),
      {},
      [],
      undefined,
      oldVNode
    );
  }
  //判断old和new是不是同一个节点
  if (oldVNode.key == newVNode.key && oldVNode.sel == newVNode.sel) {
    //同一节点精细比较
  } else {
    //不是则删除旧的,上新的
    let newVNodeElm = createElm(newVNode);
    //当返回的值存在,且oldVNode存在parentNode,毕竟手写的,防报错
    if (oldVNode.elm.parentNode && newVNodeElm) {
      oldVNode.elm.parentNode.insertBefore(newVNodeElm, oldVNode.elm);
      //删除老节点
      oldVNode.elm.parentNode.removeChild(oldVNode.elm);
    }
  }
}

//createElm函数
export default function createElement(vnode) {
  let domNode = document.createElement(vnode.sel);
  //由于是简单的diff,所以这里就不考虑又传文字又传children的情况
  if (
    vnode.text != "" &&
    (vnode.children == undefined || vnode.children.length == 0)
  ) {
    //文本情况,不要做太多的事情,让patch做
    domNode.innerText = vnode.text;
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    //递归创建子节点
    //在这步之前是把标杆拿出去了,不然不好递归
    for (let i = 0; i < vnode.children.length; i++) {
      let ch = vnode.children[i];
      //开启递归,直到没有children
      let chDOM = createElement(ch);
      //给父亲添加孩子生成dom树
      //又因为子元素没有标杆,不能用insertBefore,所以直接append
      domNode.appendChild(chDOM);
    }
  }
  //帮其上树,补充elm
  vnode.elm = domNode;
  //返回这个真实的dom节点
  return vnode.elm;
}
复制代码

 

 

  2、精细化diff(这里都只考虑:没有text就有children)

    

 

 

  •    手写精细化第一部分patch
复制代码
  if (oldVNode.key == newVNode.key && oldVNode.sel == newVNode.sel) {
    //同一节点精细比较
    //先判断两点在内存中是否为同一对象
    if (oldVNode == newVNode) {
      return;
    }
    if (
      newVNode.text != "" &&
      (newVNode.children == undefined || newVNode.children.length == 0)
    ) {
      //命中,newvnode有text无children
      //如果text不同就覆盖,相同则不用操作
      if (newVNode.text != oldVNode.text) {
        oldVNode.elm.innerText = newVNode.text;
      }
    } else {
      //newVNode没有text,则有children
      //判断oldVNode有没有children
      if (oldVNode.children != undefined && oldVNode.children.length > 0) {
        //老的也有children,此时情况最复杂,要递归
        //这里为第二部分,放在下面
        // patchVNode();
      } else {
        //老的没有children,则有text
        //清空text并追加children
        oldVNode.elm.innerText = "";
        for (let i = 0; i < newVNode.children.length; i++) {
          let dom = createElm(newVNode.children[i]);
          oldVNode.elm.appendChild(dom);
        }
      }
    }
  }
复制代码
  •   精细化比较第二部分(五角星部分)

    首先分析,新、老节点都有children,新节点相比于老节点,有增、删、移、更新内容四种情况。

    如果单纯的手写,需要考虑的内容太多,且四种情况容易杂糅。因此引出diff算法

 

diff的子节点更新策略:

 

四种命中查找(指针依次往下走,且规则按顺序判断执行,命中其中一种,就不会往下判断)

① 新前与旧前相同

② 新后与旧后相同

③ 新后与旧前相同 (此种发生了,涉及移动节点,那么新后指向的节点(其实移旧前也可以),移动到旧后之后

④ 新前与旧后相同 (此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前

若都没有命中,需要通过循环寻找老节点内是否有相同,有则移动新前,且将老节点中相同的节点undefined(Vue)

 

 

 

先定下循环的规则:while(新前<=新后&&旧前<=旧后)

新增的逻辑:当旧节点先完成循环,则新前和新后的中间节点为需要新增的节点

删除的逻辑:当新节点先完成循环,则旧前和旧后的中间节点为需要删除的节点

 

 

多删(位置多变)的逻辑:当四个都不命中,则循环旧节点,找到与新前相同的,Vue中将其命为undefined,然后新前指针往前,继续循环至循环结束

  •  命中3时:新前指向的节点,移动到旧后之后,新后指针上移,旧前指针节点undefined后下移。

        这里要注意,是没处理的旧后的后面,按顺序插入(如图B、C),若已经有添加过的(如图老节点的A),那要在添加过的之前添加(也就是旧后的后面)

 

  •  命中4时:新前指向的节点,移动到旧前之前,新前指针下移,旧后指针节点undefined后上移,同理,注意插入顺序

 

复制代码
import createElm from "./createElm";
import patchVNode from "./patchVNode";

//判断是否同节点
function sameVNode(a, b) {
  return a.sel == b.sel && a.key == b.key;
}

export default function updateChildren(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];
  //创建keyMap帮助快速查找key
  let keyMap = null;
  //diff的循环规则
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    //这里得先判断oldStart节点是否被设置成了undefined,如果是就跳过
    if (oldStartVNode == undefined) {
      oldStartVNode = oldCh[++oldStartIdx];
    } else if (oldEndVNode == undefined) {
      oldEndVNode = oldCh[--oldEndIdx];
    }
    //新前旧前
    else if (sameVNode(oldStartVNode, newStartVNode)) {
      console.log("新前旧前");
      patchVNode(newStartVNode, oldStartVNode);
      oldStartVNode = oldCh[++oldStartIdx];
      newStartVNode = newCh[++newStartIdx];
    }
    //新后旧后
    else if (sameVNode(newEndVNode, oldEndVNode)) {
      console.log("新后旧后");
      patchVNode(newEndVNode, oldEndVNode);
      oldEndVNode = oldCh[--oldEndIdx];
      newEndVNode = newCh[--newEndIdx];
    }
    //新后旧前
    else if (sameVNode(newEndVNode, oldStartVNode)) {
      console.log("新后旧前");
      patchVNode(newEndVNode, oldStartVNode);
      //insert会自动将另一个li删除,相当于移动
      //如果nextSibing为null则insertBefore会将其插到最后
      parentElm.insertBefore(oldStartVNode.elm, oldEndVNode.elm.nextSibling);
      oldStartVNode = oldCh[++oldStartIdx];
      newEndVNode = newCh[--newEndIdx];
    }
    //新前旧后
    else if (sameVNode(newStartVNode, oldEndVNode)) {
      console.log("新前旧后");
      patchVNode(newStartVNode, oldEndVNode);
      parentElm.insertBefore(oldEndVNode.elm, oldStartVNode.elm);
      oldEndVNode = oldCh[--oldEndIdx];
      newStartVNode = newCh[++newStartIdx];
    }
    //都未命中
    else {
      //如果没有keyMap,先创建keyMap
      if (!keyMap) {
        keyMap = {};
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
          const key = oldCh[i].key;
          if (key != undefined) {
            keyMap[key] = i;
          }
        }
      }
      //寻找没有命中的项,新的项(这里newStartVNode指的就是新节点所指的 旧节点可能没有的新项)
      //用map找得更快
      const idxInOlds = keyMap[newStartVNode.key];
      console.log(idxInOlds);
      //idxInOlds如果没找到,那就是undefined
      if (idxInOlds == undefined) {
        //新项,不存在的新项,放在oldstart前
        parentElm.insertBefore(createElm(newStartVNode), oldStartVNode.elm);
      } else {
        //存在项,需要移动
        const elmToMove = oldCh[idxInOlds];
        patchVNode(newStartVNode, elmToMove);
        //标记undefined,表示已处理该项
        oldCh[idxInOlds] = undefined;
        //移动
        console.log(elmToMove);
        parentElm.insertBefore(elmToMove.elm, oldStartVNode.elm);
      }
      newStartVNode = newCh[++newStartIdx];
    }
  }
  //看新节点有无剩余,若有,则为新增的节点
  if (newStartIdx <= newEndIdx) {
    console.log("新增节点");
    //添加标杆,这里标杆使用新节点是因为现在处理的是剩余的节点,所要插入要依照新节点的顺序
    const before =
      newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      parentElm.insertBefore(createElm(newCh[i]), before);
    }
  }
  //看旧节点有无剩余,若有,则为删除的节点
  else if (oldStartIdx <= oldEndIdx) {
    console.log("删除节点");
    console.log(oldCh);
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      if (oldCh[i]) {
        parentElm.removeChild(oldCh[i].elm);
      }
    }
  }
}
复制代码

 

posted @   Jacky02  阅读(252)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示