• 虚拟DOM详解


    虚拟DOM简介

      Virtual Dom可以看做一棵模拟了DOM树的JavaScript对象树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发Virtual Dom数据的变化,然后通过Virtual Dom和真实DOM的比对,再对真实DOM更新。虚拟DOM其实就是一种模拟DOM的JavaScript数据结构。

      像SnabbDOM这种库的虚拟DOM是如下的数据结构:

    • sel 元素选择器 
    • data 元素属性 ●
    • children 元素子节点 ●
    • text 元素文本 ●
    • elm 对应dom元素 ●
    • key

    SnabbDOM源码概述

      说到SnabbDOM可能大家不太知道,但是大名鼎鼎的VUE就是使用SnabbDOM来提供虚拟DOM。SnabbDOM中的VNode结构如下:

    export interface VNodeData {
      props?: Props;
      attrs?: Attrs;
      class?: Classes;
      style?: VNodeStyle;
      dataset?: Dataset;
      on?: On;
      hero?: Hero;
      attachData?: AttachData;
      hook?: Hooks;
      key?: Key;
      ns?: string; // for SVGs
      fn?: () => VNode; // for thunks
      args?: Array<any>; // for thunks
      [key: string]: any; // for any other 3rd party module
    }
    
    export function vnode(sel: string | undefined,
                          data: any | undefined,
                          children: Array<VNode | string> | undefined,
                          text: string | undefined,
                          elm: Element | Text | undefined): VNode {
      let key = data === undefined ? undefined : data.key;
      return {sel: sel, data: data, children: children,
              text: text, elm: elm, key: key};
    }
    
    export default vnode;

      但是这里并没有直接提供对外接口,而是提供了h方法来创建VNode:

    export function h(sel: string): VNode;
    export function h(sel: string, data: VNodeData): VNode;
    export function h(sel: string, text: string): VNode;
    export function h(sel: string, children: Array<VNode | undefined | null>): VNode;
    export function h(sel: string, data: VNodeData, text: string): VNode;
    export function h(sel: string, data: VNodeData, children: Array<VNode | undefined | null>): VNode;
    export function h(sel: any, b?: any, c?: any): VNode {
      var data: VNodeData = {}, children: any, text: any, i: number;
      if (c !== undefined) {
        data = b;
        if (is.array(c)) { children = c; }
        else if (is.primitive(c)) { text = c; }
        else if (c && c.sel) { children = [c]; }
      } else if (b !== undefined) {
        if (is.array(b)) { children = b; }
        else if (is.primitive(b)) { text = b; }
        else if (b && b.sel) { children = [b]; }
        else { data = b; }
      }
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
        }
      }
      if (
        sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
        (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
      ) {
        addNS(data, children, sel);
      }
      return vnode(sel, data, children, text, undefined);
    };
    View Code

      那么在具体使用场景如下:

    const overviewView = (movies) =>
      h('div.page', {style: fadeInOutStyle}, [
        h('div.header', [
          h('div.header-content.overview', {
            style: fadeInOutStyle,
          }, [
            h('div.header-title', {
              style: {transform: 'translateY(-2em)',
                      delayed: {transform: 'translate(0)'},
                      destroy: {transform: 'translateY(-2em)'}}
            }, 'Top 10 movies'),
            h('div.spacer'),
          ]),
        ]),
        h('div.page-content', [
          h('div.list', {
            style: {opacity: '0', delayed: {opacity: '1'},
                    remove: {opacity: '0', position: 'absolute', top: '0', left: '0'}}
          }, movies.map((movie) =>
            h('div.row', {
              on: {click: [select, movie]},
            }, [
              h('div.hero.rank', [
                h('span.hero', {hero: {id: 'rank'+movie.rank}}, movie.rank)
              ]),
              h('div.hero', {hero: {id: movie.title}}, movie.title)
            ])
          )),
        ]),
      ]);
    View Code

      其中跟虚拟DOM各个周期中需要使用的hook方法都放在Hook.ts这个文件下:

    import {VNode} from './vnode';
    
    export type PreHook = () => any;
    export type InitHook = (vNode: VNode) => any;
    export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any;
    export type InsertHook = (vNode: VNode) => any;
    export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any;
    export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any;
    export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any;
    export type DestroyHook = (vNode: VNode) => any;
    export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any;
    export type PostHook = () => any;
    
    export interface Hooks {
      pre?: PreHook;
      init?: InitHook;
      create?: CreateHook;
      insert?: InsertHook;
      prepatch?: PrePatchHook;
      update?: UpdateHook;
      postpatch?: PostPatchHook;
      destroy?: DestroyHook;
      remove?: RemoveHook;
      post?: PostHook;
    }
    View Code

      具体的钩子方法则是放在src/modules目录下的各个文件中。

      其中核心代码,包括整个Diff算法都放在snabbdom.ts文件中:

    /* global module, document, Node */
    import {Module} from './modules/module';
    import {Hooks} from './hooks';
    import vnode, {VNode, VNodeData, Key} from './vnode';
    import * as is from './is';
    import htmlDomApi, {DOMAPI} from './htmldomapi';
    
    function isUndef(s: any): boolean { return s === undefined; }
    function isDef(s: any): boolean { return s !== undefined; }
    
    type VNodeQueue = Array<VNode>;
    
    const emptyNode = vnode('', {}, [], undefined, undefined);
    
    function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
      return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
    }
    
    function isVnode(vnode: any): vnode is VNode {
      return vnode.sel !== undefined;
    }
    
    type KeyToIndexMap = {[key: string]: number};
    
    type ArraysOf<T> = {
      [K in keyof T]: (T[K])[];
    }
    
    type ModuleHooks = ArraysOf<Module>;
    
    function createKeyToOldIdx(children: Array<VNode>, beginIdx: number, endIdx: number): KeyToIndexMap {
      let i: number, map: KeyToIndexMap = {}, key: Key | undefined, ch;
      for (i = beginIdx; i <= endIdx; ++i) {
        ch = children[i];
        if (ch != null) {
          key = ch.key;
          if (key !== undefined) map[key] = i;
        }
      }
      return map;
    }
    
    const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
    
    export {h} from './h';
    export {thunk} from './thunk';
    
    export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
      let i: number, j: number, cbs = ({} as ModuleHooks);
    
      const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
      // 将各个模块的增删改查钩子方法挂在模块钩子集合中
      for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
          const hook = modules[j][hooks[i]];
          if (hook !== undefined) {
            (cbs[hooks[i]] as Array<any>).push(hook);
          }
        }
      }
    
      function emptyNodeAt(elm: Element) {
        const id = elm.id ? '#' + elm.id : '';
        const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
        return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
      }
    
      function createRmCb(childElm: Node, listeners: number) {
        return function rmCb() {
          if (--listeners === 0) {
            const parent = api.parentNode(childElm);
            api.removeChild(parent, childElm);
          }
        };
      }
    
      function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
        let i: any, data = vnode.data;
        if (data !== undefined) {
          if (isDef(i = data.hook) && isDef(i = i.init)) {
            i(vnode);
            data = vnode.data;
          }
        }
        let children = vnode.children, sel = vnode.sel;
        if (sel === '!') {
          if (isUndef(vnode.text)) {
            vnode.text = '';
          }
          vnode.elm = api.createComment(vnode.text as string);
        } else if (sel !== undefined) {
          // Parse selector
          const hashIdx = sel.indexOf('#');
          const dotIdx = sel.indexOf('.', hashIdx);
          const hash = hashIdx > 0 ? hashIdx : sel.length;
          const dot = dotIdx > 0 ? dotIdx : sel.length;
          const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
          const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                                   : api.createElement(tag);
          if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
          if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/./g, ' '));
          for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
          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));
              }
            }
          } else if (is.primitive(vnode.text)) {
            api.appendChild(elm, api.createTextNode(vnode.text));
          }
          i = (vnode.data as VNodeData).hook; // Reuse variable
          if (isDef(i)) {
            if (i.create) i.create(emptyNode, vnode);
            if (i.insert) insertedVnodeQueue.push(vnode);
          }
        } else {
          vnode.elm = api.createTextNode(vnode.text as string);
        }
        return vnode.elm;
      }
    
      function addVnodes(parentElm: Node,
                         before: Node | null,
                         vnodes: Array<VNode>,
                         startIdx: number,
                         endIdx: number,
                         insertedVnodeQueue: VNodeQueue) {
        for (; startIdx <= endIdx; ++startIdx) {
          const ch = vnodes[startIdx];
          if (ch != null) {
            api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
          }
        }
      }
    
      function invokeDestroyHook(vnode: VNode) {
        let i: any, j: number, data = vnode.data;
        if (data !== undefined) {
          if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
          for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
          if (vnode.children !== undefined) {
            for (j = 0; j < vnode.children.length; ++j) {
              i = vnode.children[j];
              if (i != null && typeof i !== "string") {
                invokeDestroyHook(i);
              }
            }
          }
        }
      }
    
      function removeVnodes(parentElm: Node,
                            vnodes: Array<VNode>,
                            startIdx: number,
                            endIdx: number): void {
        for (; startIdx <= endIdx; ++startIdx) {
          let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
          if (ch != null) {
            if (isDef(ch.sel)) {
              invokeDestroyHook(ch);
              listeners = cbs.remove.length + 1;
              rm = createRmCb(ch.elm as Node, listeners);
              for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
              if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
                i(ch, rm);
              } else {
                rm();
              }
            } else { // Text node
              api.removeChild(parentElm, ch.elm as Node);
            }
          }
        }
      }
    
      function updateChildren(parentElm: Node,
                              oldCh: Array<VNode>,
                              newCh: Array<VNode>,
                              insertedVnodeQueue: VNodeQueue) {
        // 声明指针
        let oldStartIdx = 0, newStartIdx = 0;
        let oldEndIdx = oldCh.length - 1;
    
        let oldStartVnode = oldCh[0];
        let oldEndVnode = oldCh[oldEndIdx];
        let newEndIdx = newCh.length - 1;
        let newStartVnode = newCh[0];
        let newEndVnode = newCh[newEndIdx];
        let oldKeyToIdx: any;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;
    
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          // 如果节点已经被标记处理过,直接跳过
          if (oldStartVnode == null) {
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
          } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
          } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
          } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
    
          } else if (sameVnode(oldStartVnode, newStartVnode)) {// 处理头头相同情况
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
          } else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾相同
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldStartVnode, newEndVnode)) {// 头尾相同 // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            // 将oldStart指向的节点插入到oldEnd指向节点前面
            api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 尾头相同
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            // 将oldEnd指向节点插入到oldStart之前
            api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
          } else {
            if (oldKeyToIdx === undefined) { // 制作key-index的哈西集合
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = oldKeyToIdx[newStartVnode.key as string];
            if (isUndef(idxInOld)) { // New element
              // 新节点则插入到oldStart指向的dom节点之前
              api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
              newStartVnode = newCh[++newStartIdx];
            } else {
              elmToMove = oldCh[idxInOld];
              if (elmToMove.sel !== newStartVnode.sel) {
                api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
              } else {
                // 打补丁
                // 对旧的位置坐标记设为undefined
                // 将更新后的节点移动到oldStart之前
                patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                oldCh[idxInOld] = undefined as any;
                api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
              }
              newStartVnode = newCh[++newStartIdx];
            }
          }
        }
        if (oldStartIdx > oldEndIdx) {// 新的有剩余则需要将这些节点插入到dom树中
          before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
          addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
        } else if (newStartIdx > newEndIdx) {// 老的有节点则应该移除
          removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
        }
      }
    
      function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
        let i: any, hook: any;
        // 调用prepatch钩子
        if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
          i(oldVnode, vnode);
        }
        const elm = vnode.elm = (oldVnode.elm as Node);
        let oldCh = oldVnode.children;
        let ch = vnode.children;
        if (oldVnode === vnode) return;// 没有变化直接返回
        if (vnode.data !== undefined) { // 对两个虚拟dom树的根节点进行更新
          for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
          i = vnode.data.hook;
          if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
        }
        if (isUndef(vnode.text)) { // 是否有文本节点
          if (isDef(oldCh) && isDef(ch)) { // 都有子节点对子节点进行diff算法
            if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
          } else if (isDef(ch)) { // 新虚拟dom有子节点旧的没有,则把新的子节点挂在dom上
            if (isDef(oldVnode.text)) api.setTextContent(elm, '');
            addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
          } else if (isDef(oldCh)) {// 老的里面有子节点,新的没有直接干掉老的
            removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
          } else if (isDef(oldVnode.text)) { // 老的有文本信息直接设置为空字符串
            api.setTextContent(elm, '');
          }
        } else if (oldVnode.text !== vnode.text) {// 使用新的文本信息替换旧的
          api.setTextContent(elm, vnode.text as string);
        }
        if (isDef(hook) && isDef(i = hook.postpatch)) { // 调用postpatch方法
          i(oldVnode, vnode);
        }
      }
    
      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]();// 调用所有模块的pre钩子方法
    
        if (!isVnode(oldVnode)) { // 非虚拟dom直接清空
          oldVnode = emptyNodeAt(oldVnode);
        }
    
        if (sameVnode(oldVnode, vnode)) { // 两个虚拟dom树的根节点完全一样才会进行打补丁
          patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else { // 如果跟节点不同直接干掉旧的根节点,重新创建dom元素插入到dom树中
          elm = oldVnode.elm as Node;
          parent = api.parentNode(elm);
    
          createElm(vnode, insertedVnodeQueue);
    
          if (parent !== null) {
            api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
            removeVnodes(parent, [oldVnode], 0, 0);
          }
        }
    
        // 对每个插入的元素使用相应模块中的insert钩子方法进行更新
        for (i = 0; i < insertedVnodeQueue.length; ++i) {
          (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
        }
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 调用所有钩子模块的post方法
        return vnode;
      };
    }
    View Code

    Diff算法

      SnabbDOM的diff算法主要有两个特点:

    • 同级比较
    • 就近复用

      

      同级比较的意思是,对于两颗DOM树,只会比较同一层级的节点,如果节点类型不同直接干掉旧的节点,而不是继续比较。那么就近复用意味着如果节点类型相同就会对这个节点进行改造,而不是严格的比较各个属性书否相同。那么这里涉及snabbdom中的两个主要函数:

    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]();// 调用所有模块的pre钩子方法
    
        if (!isVnode(oldVnode)) { // 非虚拟dom直接清空
          oldVnode = emptyNodeAt(oldVnode);
        }
    
        if (sameVnode(oldVnode, vnode)) { // 两个虚拟dom树的根节点完全一样才会进行打补丁
          patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else { // 如果跟节点不同直接干掉旧的根节点,重新创建dom元素插入到dom树中
          elm = oldVnode.elm as Node;
          parent = api.parentNode(elm);
    
          createElm(vnode, insertedVnodeQueue);
    
          if (parent !== null) {
            api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
            removeVnodes(parent, [oldVnode], 0, 0);
          }
        }
    
        // 对每个插入的元素使用相应模块中的insert钩子方法进行更新
        for (i = 0; i < insertedVnodeQueue.length; ++i) {
          (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
        }
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 调用所有钩子模块的post方法
        return vnode;
      };
    function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
        let i: any, hook: any;
        // 调用prepatch钩子
        if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
          i(oldVnode, vnode);
        }
        const elm = vnode.elm = (oldVnode.elm as Node);
        let oldCh = oldVnode.children;
        let ch = vnode.children;
        if (oldVnode === vnode) return;// 没有变化直接返回
        if (vnode.data !== undefined) { // 对两个虚拟dom树的根节点进行更新
          for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
          i = vnode.data.hook;
          if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
        }
        if (isUndef(vnode.text)) { // 是否有文本节点
          if (isDef(oldCh) && isDef(ch)) { // 都有子节点对子节点进行diff算法
            if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
          } else if (isDef(ch)) { // 新虚拟dom有子节点旧的没有,则把新的子节点挂在dom上
            if (isDef(oldVnode.text)) api.setTextContent(elm, '');
            addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
          } else if (isDef(oldCh)) {// 老的里面有子节点,新的没有直接干掉老的
            removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
          } else if (isDef(oldVnode.text)) { // 老的有文本信息直接设置为空字符串
            api.setTextContent(elm, '');
          }
        } else if (oldVnode.text !== vnode.text) {// 使用新的文本信息替换旧的
          api.setTextContent(elm, vnode.text as string);
        }
        if (isDef(hook) && isDef(i = hook.postpatch)) { // 调用postpatch方法
          i(oldVnode, vnode);
        }
      }

      其中diff的核心算法就在updateChildren中

    function updateChildren(parentElm: Node,
                              oldCh: Array<VNode>,
                              newCh: Array<VNode>,
                              insertedVnodeQueue: VNodeQueue) {
        // 声明指针
        let oldStartIdx = 0, newStartIdx = 0;
        let oldEndIdx = oldCh.length - 1;
    
        let oldStartVnode = oldCh[0];
        let oldEndVnode = oldCh[oldEndIdx];
        let newEndIdx = newCh.length - 1;
        let newStartVnode = newCh[0];
        let newEndVnode = newCh[newEndIdx];
        let oldKeyToIdx: any;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;
    
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          // 如果节点已经被标记处理过,直接跳过
          if (oldStartVnode == null) {
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
          } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
          } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
          } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
    
          } else if (sameVnode(oldStartVnode, newStartVnode)) {// 处理头头相同情况
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
          } else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾相同
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldStartVnode, newEndVnode)) {// 头尾相同 // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            // 将oldStart指向的节点插入到oldEnd指向节点前面
            api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 尾头相同
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            // 将oldEnd指向节点插入到oldStart之前
            api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
          } else {
            if (oldKeyToIdx === undefined) { // 制作key-index的哈西集合
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = oldKeyToIdx[newStartVnode.key as string];
            if (isUndef(idxInOld)) { // New element
              // 新节点则插入到oldStart指向的dom节点之前
              api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
              newStartVnode = newCh[++newStartIdx];
            } else {
              elmToMove = oldCh[idxInOld];
              if (elmToMove.sel !== newStartVnode.sel) {
                api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
              } else {
                // 打补丁
                // 对旧的位置坐标记设为undefined
                // 将更新后的节点移动到oldStart之前
                patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                oldCh[idxInOld] = undefined as any;
                api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
              }
              newStartVnode = newCh[++newStartIdx];
            }
          }
        }
        if (oldStartIdx > oldEndIdx) {// 新的有剩余则需要将这些节点插入到dom树中
          before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
          addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
        } else if (newStartIdx > newEndIdx) {// 老的有节点则应该移除
          removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
        }
      }

      这个方法有些复杂,下面文字描述未必那么清晰,大家可以看我录制的视频——虚拟DOM详解

      下图中白色图表代表目前实际的DOM节点,oldStart和oldEnd指向之前的虚拟DOM树,newStart和newEnd分别指向新的虚拟DOM节点。

      现在二者进行比较,首先处理头头、尾尾相同的节点,如果头尾尾头相同则同时移动新旧的指针

      接下来处理头尾尾头相同的节点,把newStartIdx和oldEndIdx相同的节点插入到oldStartIdx指向节点之前,newStartIdx向后移动,oldEndIdx向前移动;把newEndIdx和oldStartIdx相同的节点插入到oldEndIdx指向的节点之后,同时oldStartIdx向后移动,newEndIdx向前移动。

      处理完毕后的指针状态:

      接下来需要处理newStartIdx指向的节点11,那么这时候先去oldStartIdx和oldEndIdx的区间内寻找有没有这个节点,如果没有那么这个节点属于插入节点,这个节点会被插入到oldStartIdx指向的节点的前面,同时newStartIdx向后移动

      处理完11后,newStartIdx指向4,这个时候从oldStartIdx和oldEndIdx中能够找到这个节点,这说明它的位置被移动了,那么这时候只需要移动这个节点,把它移动到oldStartIdx所指向的节点之前,同时对就的虚拟DOM节点的位置进行标记,这里是设置为undefined。继续移动newStartIdx。

      接下来对7、 5、 6 都进行相同操作,这时候newStartIdx指向3的位置

      那么这个时候又变成了头头相同的情况,只需要同时将newStartIdx和oldStartIdx向后移动。

      那么这时候newStartIdx越过了newEndIdx,到这里循环结束,这时候oldStartIdx和oldEndIdx中剩下的节点都是需要删除的节点。因为之前都已经打上了标记,所以这里只要节点8是需要删除的。当然也有时候会遇到oldStartIdx和oldEndIdx先相遇,这时候在newStartIdx和newEndIdx中的节点都是需要插入的。

      至此整个比较算法结束。

    参考资料

    深入 Vue2.x 的虚拟 DOM diff 原理

    vue的Virtual Dom实现snabbdom解密

    snabbdom

    vue中Virtual DOM源码学习

    下面是我录制的视频,有动画的可以更清晰的展示这个过程。

  • 相关阅读:
    Sql日期时间格式转换;取年 月 日,函数:DateName()、DATEPART()
    @@ROWCOUNT (Transact-SQL)
    C#调用存储过程简单完整例子
    C# Ajax 手机发送短信验证码 校验验证码 菜鸟级别实现方法
    C#反射技术的简单操作(读取和设置类的属性)
    .NET调用Java写的WebService
    蓝牙错误提示
    sql开启xp_cmdshell
    网页手机宽度
    对称加密算法比较
  • 原文地址:https://www.cnblogs.com/dojo-lzz/p/8047742.html
Copyright © 2020-2023  润新知