虚拟DOM与diff学习总结

594 阅读4分钟

Snabbdom

​ 是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了Snabbdom;

虚拟DOM

​ 用javaScript对象描述DOM的层次结构.DOM中的一切属性都在虚拟DOM中有对应的属性.

diff是发生在虚拟DOM上的

​ 新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上

虚拟DOM是如何被渲染函数(h函数)产生的

  1. h函数用来产生虚拟节点(vnode)

  2. h函数:

    ​ 第一个参数为标签节点

    ​ 第二个参数为一个props对象,里面放html标签属性对象

    ​ 第三个参数为标签文字

  3. 一个虚拟节点有哪些属性

    {
    
    	children:undefined,       //子元素
    
    	data:{ },	     	  //属性,样式
    
    	elm:undefined, 		  //真正DOM节点,如果为undefined则表示该节点还没有上树
    
    	key:undefined,		  //该节点的唯一标识
    
    	sel:'"div",		  //选择器
    
    	text:"我是一个盒子"  //文字
    
    }
    

    ​ 而vnode函数的功能非常简单,就是把传入的5个参数组合成对象返回

    ​ vnode源码

     export function vnode(sel, data, children, text, elm) {
        const key = data === undefined ? undefined : data.key;
        return { sel, data, children, text, elm, key };
     }
    
  4. h函数可以嵌套使用,从而得到虚拟DOM树

diff算法原理

(2个虚拟DOM如何进行差异化比较)

diff心得

  1. 最小量更新,key是节点的唯一标识,告诉diff算法,在更改前后 他们是否为同一个DOM节点
  2. 只能是同一个虚拟节点,才进行精细化比较.否则就暴力删除旧的DOM节点,插入新的DOM节点.
    1. 延伸问题:如何定义是否为同一虚拟节点?? 答:选择器相同且key相同
  3. 只进行同层级比较,不进行跨层级比较.即使是同一片虚拟节点,但是如果跨层级了,不会进行diff精细化比较,而是暴力删除旧DOM节点,重新插入新的DOM节点

diff处理新旧节点不是同一个节点时

patch(oldVnode,element) 源码; oldVnode 老节点; element新节点

return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode)
    }
	//源码中sameVnode(oldVnode,vnode)方法 判断2个节点是否为同一个节点,返回一个布尔值
    if (sameVnode(oldVnode, vnode)) {
      //如果是 再调用patchVnode()精细化比较
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      //如果不是 暴力删除老节点,创建新节点重新插入
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node
	  //创建新节点
      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        //删除老节点
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }
	//一些生命周期的钩子,暂不深入研究
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
  }

如何定义同一个节点

源码中sameVnode(oldVnode,vnode)方法 判断2个节点是否为同一个节点,返回一个布尔值

判断旧节点的key要和新节点的key相同 且 旧节点的选择器要和新节点的选择器相同

    function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
      return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
    }

​ 如果是 再调用patchVnode()精细化比较

​ 如果不是 则暴力删除,重新创建新的直接点.所有新建的子节点都是需要递归出来的

​ (createElm源码递归创建新子节点部分)

 if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i]
          if (ch != null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
          }
        }
      }

源码:createKeyToOldIdx() 缓存key

​ 作用:就是把从开始的idx与结束的idx进行遍历存到一个map里,让map中的key等于下标 i 的项目

function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): 			KeyToIndexMap {
        const map: KeyToIndexMap = {}
        for (let i = beginIdx; i <= endIdx; ++i) {
        const key = children[i]?.key
        if (key !== undefined) {
        map[key] = i
    }
}

流程图

经典的diff算法优化策略

四种命中查找:

  1. 新前与旧前 (此种情况发生,指针要往后移动)
  2. 新后与旧后 (此种情况发生,指针要往前移动)
  3. 旧后与新前 (此种情况发生涉及移动节点,那么新前指向的节点,移动到旧后节点之后)
  4. 新前与旧后 (此种情况发生涉及移动节点,那么新前指向的节点,移动到旧前节点之前)

命中一种就不再进行命中判断了.

如果都没有命中,就需要用循环来寻找了.