Part3-1-4 Vue.js Virtual DOM 的实现原理

Posted 沿着路走到底

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Part3-1-4 Vue.js Virtual DOM 的实现原理相关的知识,希望对你有一定的参考价值。

Virtual DOM

Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象

使用 Virtual DOM 来描述 真实DOM

创建 虚拟DOM 比创建 真实DOM 的开销小很多

虚拟 DOM 的作用

维护视图和状态的关系

• 复杂视图情况下提升渲染性能

• 跨平台

        • 浏览器平台渲染DOM
        • 服务端渲染 SSR(Nuxt.js/Next.js)

        • 原生应用(Weex/React Native)
        • 小程序(mpvue/uni-app)等

使用 Virtual

虚拟 DOM 库  Snabbdom

• Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom

• 大约 200 SLOC (single line of code)
• 通过模块可扩展
• 源码使用 TypeScript 开发

• 最快的 Virtual DOM 之一

Snabbdom 基本使用

创建项目

• 安装 parcel

• 配置 scripts

• 目录结构

安装 parcel

npm install parcel-bundler -D

配置 scripts

"scripts": {
    "dev": "parcel index.html --open",
    "build": "parcel build index.html"
  },

Snabbdom 文档

看文档的意义

• 学习任何一个库都要先看文档
• 通过文档了解库的作用
• 看文档中提供的示例,自己快速实现一个 demo

• 通过文档查看 API 的使用

Snabbdom 文档
• https://github.com/snabbdom/snabbdom

• 当前版本 v2.1.0

安装 Snabbdom

npm intall snabbdom@2.1.0

导入 Snabbdom

Snabbdom 的两个核心函数 init 和 h()

init() 是一个高阶函数,返回 patch()

h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

const patch = init([])

node 12,webpack5 支持在 package.json 中 简化路径

Snabbdom 基本使用

文本

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

const patch = init([])

// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls',{
  hook: {
    init (vnode) {
      console.log(vnode.elm)
    },
    create (emptyNode, vnode) {
      console.log(vnode.elm)
    }
  }
}, 'Hello World')
let app = document.querySelector('#app')
// 第一个参数:旧的 VNode,可以是 DOM 元素
// 第二个参数:新的 VNode
// 返回新的 VNode
let oldVnode = patch(app, vnode)

vnode = h('div#container.xxx', 'Hello Snabbdom')
patch(oldVnode, vnode)

包含子元素

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

const patch = init([])

let vnode = h('div#container', [
  h('h1', 'Hello Snabbdom'),
  h('p', '这是一个p')
])

let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

setTimeout(() => {
  // vnode = h('div#container', [
  //   h('h1', 'Hello World'),
  //   h('p', 'Hello P')
  // ])
  // patch(oldVnode, vnode)

  // 清除div中的内容
  patch(oldVnode, h('!'))
}, 2000);

模块的作用

• Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等, 可以通过注册 Snabbdom 默认提供的模块来实现

• Snabbdom 中的模块可以用来扩展 Snabbdom的功能
• Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的

官方提供的模块

• attributes

• props
• dataset
• class

• style
• eventlisteners

模块使用步骤

• 导入需要的模块
• init() 中注册模块
• h() 函数的第二个参数处使用模块

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

// 1. 导入模块
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'

// 2. 注册模块
const patch = init([
  styleModule,
  eventListenersModule
])

// 3. 使用h() 函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
  h('h1', { style: { backgroundColor: 'red' } }, 'Hello World'),
  h('p', { on: { click: eventHandler } }, 'Hello P')
])

function eventHandler () {
  console.log('别点我,疼')
}

let app = document.querySelector('#app')
patch(app, vnode)

如何学习源码

• 宏观了解
• 带着目标看源码
• 看源码的过程要不求甚解

• 调试
• 参考资料

Snabbdom 的核心

• init() 设置模块,创建 patch() 函数
• 使用 h() 函数创建 javascript 对象(VNode)描述真实 DOM

• patch() 比较新旧两个 Vnode
• 把变化的内容更新到真实 DOM 树

Snabbdom 源码

源码地址

• https://github.com/snabbdom/snabbdom

• 当前版本:v2.1.0

克隆代码

• git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git

Snabbdom 源码解析

h 函数介绍

• 作用:创建 VNode 对象

• Vue 中的 h 函数

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

• h 函数最早见于 hyperscript,使用 JavaScript 创建超文本

函数重载

• 参数个数或参数类型不同的函数
• JavaScript 中没有重载的概念
• TypeScript 中有重载,不过重载的实现还是通过代码调整参数

• 重载和参数相关,和返回值无关

函数重载-参数个数

函数重载-参数类型

snabbdom/src/h.ts 

import { vnode, VNode, VNodeData } from "./vnode";
import * as is from "./is";

export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;

function addNS(
  data: any,
  children: VNodes | undefined,
  sel: string | undefined
): void {
  data.ns = "http://www.w3.org/2000/svg";
  if (sel !== "foreignObject" && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      const childData = children[i].data;
      if (childData !== undefined) {
        addNS(childData, children[i].children as VNodes, children[i].sel);
      }
    }
  }
}


// h 函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
  sel: string,
  data: VNodeData | null,
  children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
  let data: VNodeData = {};
  let children: any;
  let text: any;
  let i: number;

  // 处理参数,实现重载的机制
  if (c !== undefined) {
    // 处理三个参数的情况
    // sel、data、children/text
    if (b !== null) {
      data = b;
    }
    if (is.array(c)) {
      children = c;
    } else if (is.primitive(c)) { // 如果 c 是字符串或者数字
      text = c;
    } else if (c && c.sel) { // 如果 c 是 vNode
      children = [c];
    }
  } else if (b !== undefined && b !== null) {
    // 处理两个参数的情况
    if (is.array(b)) {
      children = b;
    } else if (is.primitive(b)) { 如果 b 是字符串或者数字
      text = b;
    } else if (b && b.sel) { // 如果 b 是 vNode
      children = [b];
    } else {
      data = b;
    }
  }
  if (children !== undefined) {
    // 处理 children 中的原始值 (string/number)
    for (i = 0; i < children.length; ++i) {
      // 如果 child 是 string/number, 创建文本节点
      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] === "#")
  ) {
    // 如果是 svg,添加命名空间
    addNS(data, children, sel);
  }

  // 返回 VNode
  return vnode(sel, data, children, text, undefined);
}

VNode

snabbdom/src/vnode.ts

import { Hooks } from "./hooks";
import { AttachData } from "./helpers/attachto";
import { VNodeStyle } from "./modules/style";
import { On } from "./modules/eventlisteners";
import { Attrs } from "./modules/attributes";
import { Classes } from "./modules/class";
import { Props } from "./modules/props";
import { Dataset } from "./modules/dataset";

export type Key = string | number | symbol;


// children 与 text 互斥
export interface VNode {
  sel: string | undefined;
  data: VNodeData | undefined;
  children: Array<VNode | string> | undefined; // 记录子节点
  elm: Node | undefined;
  text: string | undefined; // 记录对应文本节点中的文本内容
  key: Key | undefined;
}

export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: any[]; // for thunks
  is?: string; // for custom elements v1
  [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 {
  const key = data === undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}

patch 整体过程分析

• patch(oldVnode, newVnode)

• 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点

• 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)

• 如果不是相同节点,删除之前的内容,重新渲染

• 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容

• 如果新的 VNode 有 children,判断子节点是否有变化,

snabbdom/src/init.ts

import { Module } from "./modules/module";
import { vnode, VNode } from "./vnode";
import * as is from "./is";
import { htmlDomApi, DOMAPI } from "./htmldomapi";

type NonUndefined<T> = T extends undefined ? never : T;

function isUndef(s: any): boolean {
  return s === undefined;
}
function isDef<A>(s: A): s is NonUndefined<A> {
  return s !== undefined;
}

type VNodeQueue = VNode[];

const emptyNode = vnode("", {}, [], undefined, undefined);

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = vnode1.data?.is === vnode2.data?.is;
  const isSameSel = vnode1.sel === vnode2.sel;

  return isSameSel && isSameKey && isSameIs;
}

function isVnode(vnode: any): vnode is VNode {
  return vnode.sel !== undefined;
}

type KeyToIndexMap = { [key: string]: number };

type ArraysOf<T> = {
  [K in keyof T]: Array<T[K]>;
};

type ModuleHooks = ArraysOf<Required<Module>>;

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 as string] = i;
    }
  }
  return map;
}

const hooks: Array<keyof Module> = [
  "create",
  "update",
  "remove",
  "destroy",
  "pre",
  "post",
];


// domApi 把 VNode 对象转换为其他平台下对应的元素,默认为 DOM 操作的 API
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number;
  let j: number;
  const cbs: ModuleHooks = {
    create: [],
    update: [],
    remove: [],
    destroy: [],
    pre: [],
    post: [],
  };

  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 any[]).push(hook);
      }
    }
  }

  function emptyNodeAt(elm: Element) {
    const id = elm.id ? "#" + elm.id : "";

    // elm.className doesn't return a string when elm is an SVG element inside a shadowRoot.
    // https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring
    const classes = elm.getAttribute("class");

    const c = classes ? "." + classes.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) as Node;
        api.removeChild(parent, childElm);
      }
    };
  }

  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    // 执行用户设置的 init 钩子函数
    let i: any;
    let data = vnode.data;
    if (data !== undefined) {
      const init = data.hook?.init;
      if (isDef(init)) {
        init(vnode);
        data = vnode.data;
      }
    }
    const children = vnode.children;
    const sel = vnode.sel;

    // 把 vnode 转换成真实 dom 对象(没有渲染到页面)
    if (sel === "!") {
      // 如果选择器是 ! ,创建注释节点
      if (isUndef(vnode.text)) {
        vnode.text = "";
      }
      vnode.elm = api.createComment(vnode.text!);
    } 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.ns))
          ? api.createElementNS(i, tag, data)
          : api.createElement(tag, data));
      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);

      // 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
      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));
      }
      const hook = vnode.data!.hook;
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode);
        if (hook.insert) {
          insertedVnodeQueue.push(vnode);
        }
      }
    } else {
      // 如果选择器为空,创建文本节点
      vnode.elm = api.createTextNode(vnode.text!);
    }

    // 返回创建的 dom
    return vnode.elm;
  }

  function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: 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) {
    const data = vnode.data;
    if (data !== undefined) {
      data?.hook?.destroy?.(vnode);
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      if (vnode.children !== undefined) {
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j];
          if (child != null && typeof child !== "string") {
            invokeDestroyHook(child);
          }
        }
      }
    }
  }

  function removeVnodes(
    parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number
  ): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number;
      let rm: () => void;
      const ch = vnodes[startIdx];
      if (ch != null) {
        if (isDef(ch.sel)) {
          invokeDestroyHook(ch);
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm!, listeners);
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          const removeHook = ch?.data?.hook?.remove;
          if (isDef(removeHook)) {
            removeHook(ch, rm);
          } else {
            rm();
          }
        } else {
          // Text node
          api.removeChild(parentElm, ch.elm!);
        }
      }
    }
  }

  function updateChildren(
    parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue
  ) {
    let oldStartIdx = 0;
    let 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: KeyToIndexMap | undefined;
    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);
        api.insertBefore(
          parentElm,
          oldStartVnode.elm!,
          api.nextSibling(oldEndVnode.elm!)
        );
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        if (isUndef(idxInOld)) {
          // New element
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );
        } else {
          elmToMove = oldCh[idxInOld];
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(
              parentElm,
              createElm(newStartVnode, insertedVnodeQueue),
              oldStartVnode.elm!
            );
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
        }
        newStartVnode = newCh[++newStartIdx];
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(
          parentElm,
          before,
          newCh,
          newStartIdx,
          newEndIdx,
          insertedVnodeQueue
        );
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

  function patchVnode(
    oldVnode: VNode,
    vnode: VNode,
    insertedVnodeQueue: VNodeQueue
  ) {
    // 第一个过程:触发 prepatch 和 update 钩子函数
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);
    const elm = (vnode.elm = oldVnode.elm)!;
    const oldCh = oldVnode.children as VNode[];
    const ch = vnode.children as VNode[];
    if (oldVnode === vnode) return;
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i)
        cbs.update[i](oldVnode, vnode);
      vnode.data.hook?.update?.(oldVnode, vnode);
    }


    // 第二个过程:真正对比新旧 vnode 差异的地方
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) api.setTextContent(elm, "");
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, "");
      }
    } else if (oldVnode.text !== vnode.text) {
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      api.setTextContent(elm, vnode.text!);
    }

    // 第三个过程:触发 postpatch 钩子函数
    hook?.postpatch?.(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]();

    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);
    }

    if (sameVnode(oldVnode, vnode)) {
      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;
  };
}
snabbdom/src/htmldomapi.ts
export interface DOMAPI {
  createElement: (
    tagName: any,
    options?: ElementCreationOptions
  ) => HTMLElement;
  createElementNS: (
    namespaceURI: string,
    qualifiedName: string,
    options?: ElementCreationOptions
  ) => Element;
  createTextNode: (text: string) => Text;
  createComment: (text: string) => Comment;
  insertBefore: (
    parentNode: Node,
    newNode: Node,
    referenceNode: Node | null
  ) => void;
  removeChild: (node: Node, child: Node) => void;
  appendChild: (node: Node, child: Node) => void;
  parentNode: (node: Node) => Node | null;
  nextSibling: (node: Node) => Node | null;
  tagName: (elm: Element) => string;
  setTextContent: (node: Node, text: string | null) => void;
  getTextContent: (node: Node) => string | null;
  isElement: (node: Node) => node is Element;
  isText: (node: Node) => node is Text;
  isComment: (node: Node) => node is Comment;
}

function createElement(
  tagName: any,
  options?: ElementCreationOptions
): HTMLElement {
  return document.createElement(tagName, options);
}

function createElementNS(
  namespaceURI: string,
  qualifiedName: string,
  options?: ElementCreationOptions
): Element {
  return document.createElementNS(namespaceURI, qualifiedName, options);
}

function createTextNode(text: string): Text {
  return document.createTextNode(text);
}

function createComment(text: string): Comment {
  return document.createComment(text);
}

function insertBefore(
  parentNode: Node,
  newNode: Node,
  referenceNode: Node | null
): void {
  parentNode.insertBefore(newNode, referenceNode);
}

function removeChild(node: Node, child: Node): void {
  node.removeChild(child);
}

function appendChild(node: Node, child: Node): void {
  node.appendChild(child);
}

function parentNode(node: Node): Node | null {
  return node.parentNode;
}

function nextSibling(node: Node): Node | null {
  return node.nextSibling;
}

function tagName(elm: Element): string {
  return elm.tagName;
}

function setTextContent(node: Node, text: string | null): void {
  node.textContent = text;
}

function getTextContent(node: Node): string | null {
  return node.textContent;
}

function isElement(node: Node): node is Element {
  return node.nodeType === 1;
}

function isText(node: Node): node is Text {
  return node.nodeType === 3;
}

function isComment(node: Node): node is Comment {
  return node.nodeType === 8;
}

export const htmlDomApi: DOMAPI = {
  createElement,
  createElementNS,
  createTextNode,
  createComment,
  insertBefore,
  removeChild,
  appendChild,
  parentNode,
  nextSibling,
  tagName,
  setTextContent,
  getTextContent,
  isElement,
  isText,
  isComment,
};

patchVnode
对比新旧2个 vnode 节点,找到他们的差异,更新到真实 DOM 上

Diff 算法

渲染真实 DOM 的开销很大,DOM 操作会引起浏览器的重排和重绘,浏览器重新渲染页面是非常耗性能的。
Diff 的核心是当数据发生变化时,不直接操作 DOM,而是用 JS对象 来描述真实 DOM,当数据变化后,会先比较 JS对象 是否发生变化,找到所有变化后的位置,只去最小化的更新变化的位置,从而提高性能。
Snbbdom 根据 DOM 的特点对传统的diff算法做了优化

• DOM 操作时候很少会跨级别操作节点

• 只比较同级别的节点

执行过程


在对开始和结束节点比较的时候,总共有四种情况

• oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)

• oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
• oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
• oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)

旧开始节点 和 新开始节点之间的比较

首先将新开始节点与旧开始节点 调用 sameVnode 函数进行比较,如果是相同节点(key 和 sel 相同),调用 patchVnode 函数比较这2个节点内部的差异,然后更新到真实 DOM。

比较完成后,将索引移动到下一个节点的位置(oldStartIdx++ / newStartIdx++),再去比较第二个节点,把第二个节点作为开始节点进行比较。

旧结束节点 和 新结束节点 之间的比较

如果开始节点不是相同节点,会从后往前比较,比较旧的结束节点和新的结束节点是否是相同节点,如果是相同节点,调用 patchVnode 函数比较这2个节点内部的差异,然后更新到真实 DOM。

比较完成后,将索引移动到倒数第二个节点的位置(oldEndIdx-- / newEndIdx--),再去比较倒数第二个节点,把倒数第二个节点作为结束节点进行比较。

旧开始节点 和 新结束节点 之间的比较

如果旧的开始节点和新的结束节点是相同节点的话,调用 patchVnode 函数比较这2个节点内部的差异,然后更新到真实 DOM。当内部差异更新完成之后,将旧开始节点移动到最后。

然后移动索引,旧开始节点的索引移动到下一个位置,新结束索引的位置移动到前一个位置。

旧结束节点 和 新开始节点 之间的比较

如果旧的结束节点和新的开始节点是相同节点的话,调用 patchVnode 函数比较这2个节点内部的差异,然后更新到真实 DOM。当内部差异更新完成之后,将旧结束节点移动到最开始的位置。

如果以上四种情况都不满足时,说明开始和结束节点都不相同

首先遍历新的开始节点,在旧节点数组中,查找是否有相同节点,如果没有找到,说明此时的新开始节点是一个新的节点,此时需要创建一个新的 DOM 元素,插入到旧节点最前面的位置。

如果新开始节点在旧节点数组中找到了相同 key 值的节点,先判断 sel 是否相同,

如果 sel 不同,说明不是相同的节点,需要创建新的 DOM 元素,并插入到旧节点最前面的位置。如果 sel 相同,说明是相同节点,找到的这个旧节点与新开始节点通过patchVnode 函数进行比较,并更新到真实 DOM,然后将这个旧节点移动到最开始的位置。

当循环结束之后

当老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,将这些剩余的节点创建对应的 DOM 元素,并插入到 老节点的末尾。

如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,将老节点剩余的节点批量删除。

总结

如果是相同节点的话,会重用之前旧节点对应的 DOM 元素,在 patchVnode 函数中会对比新旧节点之间的差异,然后把差异更新到重用的 DOM 元素上。这个差异可能是文本内容不同,也可能是子元素不同,而当前 DOM 是不需要重新创建的,如果文本内容或子元素也都相同的话,是不会进行 DOM 操作的。虚拟 DOM 是通过这种方式提高性能的。

1

以上是关于Part3-1-4 Vue.js Virtual DOM 的实现原理的主要内容,如果未能解决你的问题,请参考以下文章

揭秘 Vue 中的 Virtual Dom

如何理解vue,virtual DOM

Vuejs528- 揭秘Vue中的Virtual Dom

基于Vue认识虚拟DOM(Virtual DOM)

Vue3.0 年中上?听说已跳出了virtual dom性能的瓶颈!!

Virtual DOM 和 Shadow DOM 有啥不同? [复制]