虚拟DOM:VNode的设计与创建

0 阅读1分钟

经过前几篇文章的深入探索,我们完整地构建了 Vue3 的响应式系统。但响应式数据最终要渲染到页面上,这中间的桥梁就是虚拟DOM。今天,我们将深入 Vue3 虚拟 DOM 的设计与实现,看看它如何为高效的页面更新奠定基础。

前言:为什么需要虚拟DOM?

在传统的 jQuery 时代,我们直接操作真实 DOM:

$('#app').html('<div>Hello World</div>');

这种方式虽然直观,但有几个致命问题:

  • 性能开销大:DOM 操作是浏览器中最昂贵的操作之一,频繁的 DOM 操作会严重影响系统性能
  • 难以追踪:复杂应用的状态变化难以管理
  • 手动操作:开发者需要手动维护 DOM 与状态的一致性

虚拟 DOM 的出现解决了这些问题:

// 虚拟 DOM 描述
const vnode = {
  type: 'div',
  props: { class: 'container' },
  children: 'Hello World'
};

// 渲染器将虚拟 DOM 转换为真实 DOM
render(vnode, document.getElementById('app'));

虚拟 DOM 的本质:用 JavaScript 对象来描述真实 DOM 结构,通过比较新旧虚拟 DOM 的差异(diff),最小化地更新真实 DOM。

注:虚拟 DOM 相比真实 DOM 的优势在于:频繁操作 DOM 时,虚拟 DOM 可以先将操作收集,再一次性转成真实 DOM,渲染到页面上;而不需要每次操作都修改真实 DOM。

注:虚拟 DOM 不一定比真实 DOM 快,毕竟没有什么操作的性能能比 document.createElement('div') 更优了!

虚拟 DOM 的结构变化

Vue2 的 VNode 结构

// Vue2 的 VNode 结构(简化)
interface VNode {
  tag?: string;           // 标签名
  data?: VNodeData;       // 属性、事件等
  children?: VNode[];      // 子节点
  text?: string;          // 文本内容
  elm?: Node;             // 对应的真实 DOM
  key?: string | number;  // 唯一标识
  // ... 其他属性
}

Vue3 的 VNode 结构

// Vue3 的 VNode 结构(简化)
interface VNode {
  __v_isVNode: true;      // 标记为 VNode
  type: any;              // 类型:元素标签、组件、Fragment等
  props: any;             // 属性
  children: any;          // 子节点
  shapeFlag: number;      // 节点类型标志(位掩码)
  patchFlag: number;      // 优化标志(位掩码)
  dynamicProps: string[] | null;  // 动态属性列表
  staticCount: number;    // 静态节点计数
  
  key: any;               // 唯一标识
  ref: any;               // 引用
  el: HostNode | null;    // 真实 DOM 节点
  anchor: HostNode | null; // 锚点(Fragment 使用)
  
  // 组件相关
  component: any;         // 组件实例
  suspense: any;          // Suspense 相关
  ssContent: any;         // SSR 内容
  ssFallback: any;        // SSR 回退
  
  // 优化相关
  scopeId: string | null; // 作用域 ID
  slotScopeIds: string[] | null; // 插槽作用域 ID
}

Vue3 VNode 结构的主要变化

1. 更明确的类型标识

type: 'div' | 'span' | MyComponent | Fragment | Text | Comment | Static;

2. 使用 shapeFlag 位掩码标记类型

const enum ShapeFlags {
  ELEMENT = 1,              // 元素节点
  FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
  STATEFUL_COMPONENT = 1 << 2,    // 状态组件
  TEXT_CHILDREN = 1 << 3,   // 文本子节点
  ARRAY_CHILDREN = 1 << 4,  // 数组子节点
  SLOTS_CHILDREN = 1 << 5,  // 插槽子节点
  TELEPORT = 1 << 6,        // Teleport
  SUSPENSE = 1 << 7,        // Suspense
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9
}

3. 使用 patchFlag 标记动态内容

export const enum PatchFlags {
  TEXT = 1,                 // 动态文本内容
  CLASS = 1 << 1,           // 动态 class
  STYLE = 1 << 2,           // 动态 style
  PROPS = 1 << 3,           // 动态属性
  FULL_PROPS = 1 << 4,      // 全量比较
  HYDRATE_EVENTS = 1 << 5,  // 事件监听
  STABLE_FRAGMENT = 1 << 6, // 稳定 Fragment
  KEYED_FRAGMENT = 1 << 7,  // 带 key 的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 无 key 的 Fragment
  NEED_PATCH = 1 << 9,      // 需要非 props 比较
  DYNAMIC_SLOTS = 1 << 10,  // 动态插槽
  DEV_ROOT_FRAGMENT = 1 << 11, // 开发环境根 Fragment
  
  // 特殊标志
  HOISTED = -1,             // 静态提升节点
  BAIL = -2                 // 退出优化
}

VNode 的核心属性

1. type:节点类型

元素节点
const elementVNode = {
  type: 'div',
  props: { class: 'box' },
  children: 'Hello'
};
组件节点
const MyComponent = {
  setup() {
    return () => h('div', '组件内容');
  }
};

const componentVNode = {
  type: MyComponent,
  props: { title: '标题' }
};
文本节点
const textVNode = {
  type: Text,
  props: null,
  children: '文本内容'
};

Fragment(片段)
const fragmentVNode = {
  type: Fragment,
  children: [
    h('div', '子节点1'),
    h('div', '子节点2')
  ]
};
静态节点
const staticVNode = {
  type: 'div',
  props: { class: 'static' },
  children: '静态内容',
  patchFlag: PatchFlags.HOISTED // 标记为提升
};

2. props:属性

function createVNode(type, props, children) {
  const vnode = {
    type,
    props: props || {},
    children,
    // 提取关键属性
    key: props && props.key,
    ref: props && props.ref,
    // 清理 props 中的特殊属性
    ...normalizeProps(props)
  };
  
  return vnode;
}

function normalizeProps(props) {
  if (!props) return {};
  
  // 分离特殊属性
  const { key, ref, ...pureProps } = props;
  
  return {
    props: pureProps,
    key,
    ref
  };
}

3. children:子节点

文本子节点
const vnode1 = {
  type: 'div',
  children: '纯文本',
  shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
};
数组子节点
const vnode2 = {
  type: 'div',
  children: [
    h('span', '子节点1'),
    h('span', '子节点2')
  ],
  shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
};
插槽子节点
const vnode3 = {
  type: MyComponent,
  children: {
    default: () => h('div', '默认插槽'),
    header: () => h('div', '头部插槽')
  },
  shapeFlag: ShapeFlags.COMPONENT | ShapeFlags.SLOTS_CHILDREN
};
空子节点
const vnode4 = {
  type: 'div',
  children: null,
  shapeFlag: ShapeFlags.ELEMENT
};

多种 VNode 类型

元素节点

function createElementVNode(tag, props, children) {
  const vnode = {
    type: tag,
    props,
    children,
    shapeFlag: ShapeFlags.ELEMENT
  };
  
  // 设置子节点类型标志
  if (typeof children === 'string') {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  } else if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }
  
  return vnode;
}

组件节点

function createComponentVNode(component, props, children) {
  const vnode = {
    type: component,
    props,
    children,
    shapeFlag: ShapeFlags.STATEFUL_COMPONENT
  };
  
  // 处理插槽
  if (typeof children === 'object') {
    vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
  }
  
  // 组件实例(稍后填充)
  vnode.component = null;
  
  return vnode;
}

文本节点

const Text = Symbol('Text');

function createTextVNode(text) {
  return {
    type: Text,
    props: null,
    children: String(text),
    shapeFlag: ShapeFlags.TEXT_CHILDREN
  };
}

Fragment 节点

const Fragment = Symbol('Fragment');

function createFragmentVNode(children) {
  return {
    type: Fragment,
    props: null,
    children,
    shapeFlag: Array.isArray(children) 
      ? ShapeFlags.ARRAY_CHILDREN 
      : ShapeFlags.TEXT_CHILDREN
  };
}

静态节点

function createStaticVNode(content, count) {
  return {
    type: 'div',
    props: null,
    children: content,
    shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
    patchFlag: PatchFlags.HOISTED,
    staticCount: count
  };
}

静态提升(Static Hoisting)

静态提升的原理

我们先来看一段模版代码:

<div>
  <span>静态文本</span>
  <span>{{ dynamic }}</span>
</div>

没有静态提升下的渲染函数:

function render(ctx) {
  return h('div', [
    h('span', '静态文本'), // 每次渲染都创建
    h('span', ctx.dynamic)
  ]);
}

没有静态提升下,对于 <span>静态文本</span> 这段代码,每次渲染时都会创建。

静态提升下的渲染函数:

const _hoisted_1 = h('span', '静态文本'); // 提升到函数外

function render(ctx) {
  return h('div', [
    _hoisted_1, // 直接复用
    h('span', ctx.dynamic)
  ]);
}

静态提升下,对于 <span>静态文本</span> 这段代码,会将静态文本的 VNode 提升到函数外,在需要的时候直接复用即可!

实现静态提升

// 编译器生成的代码示例
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

// 静态节点提升
const _hoisted_1 = _createVNode("span", null, "静态文本", PatchFlags.HOISTED)
const _hoisted_2 = _createVNode("div", { class: "static-class" }, [
  _hoisted_1,
  _createVNode("span", null, "另一个静态节点", PatchFlags.HOISTED)
], PatchFlags.HOISTED)

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_2,  // 直接使用提升的节点
    _createVNode("span", null, _ctx.dynamic, PatchFlags.TEXT)
  ]))
}

Patch Flags 的作用

为什么要用 Patch Flags?

无 Patch Flags:需要全量比较:

function patch(oldVNode, newVNode) {
  // 比较所有属性
  if (oldVNode.props.class !== newVNode.props.class) {
    updateClass();
  }
  if (oldVNode.props.style !== newVNode.props.style) {
    updateStyle();
  }
  if (oldVNode.props.id !== newVNode.props.id) {
    updateId();
  }
  // ... 比较所有可能的属性
}

有 Patch Flags:只比较动态部分:

function patch(oldVNode, newVNode) {
  if (newVNode.patchFlag & PatchFlags.CLASS) {
    // 只有 class 是动态的
    updateClass();
  }
  if (newVNode.patchFlag & PatchFlags.STYLE) {
    // 只有 style 是动态的
    updateStyle();
  }
  // 只比较标记为动态的属性
}

Patch Flags 的实现

// 动态节点标记
function createVNodeWithFlags(type, props, children, flag) {
  const vnode = createVNode(type, props, children);
  vnode.patchFlag = flag;
  
  // 记录动态属性名
  if (flag & PatchFlags.PROPS) {
    vnode.dynamicProps = Object.keys(props).filter(
      key => !isStaticProperty(key)
    );
  }
  
  return vnode;
}

// 使用示例
const dynamicClassVNode = createVNodeWithFlags(
  'div',
  { class: dynamicClass }, // class 动态
  '内容',
  PatchFlags.CLASS
);

const dynamicTextVNode = createVNodeWithFlags(
  'span',
  null,
  dynamicText,
  PatchFlags.TEXT
);

const multipleDynamicsVNode = createVNodeWithFlags(
  'div',
  {
    class: dynamicClass,
    style: dynamicStyle,
    id: 'static-id' // 静态属性
  },
  '内容',
  PatchFlags.CLASS | PatchFlags.STYLE
);
// dynamicProps: ['class', 'style']

h 函数的实现

h 函数的基本实现

/**
 * h 函数:创建 VNode 的辅助函数
 * @param {string|object} type - 节点类型
 * @param {object} props - 属性
 * @param {array|string} children - 子节点
 * @returns {object} VNode
 */
function h(type, props, children) {
  // 处理参数重载
  const args = normalizeArgs(type, props, children);
  
  return createVNode(args.type, args.props, args.children);
}

function normalizeArgs(type, props, children) {
  // 如果没有 props
  if (arguments.length === 2) {
    if (isObject(props) && !isArray(props)) {
      // h('div', { class: 'box' })
      return { type, props, children: null };
    } else {
      // h('div', '文本内容')
      return { type, props: null, children: props };
    }
  }
  
  // 完整参数
  return { type, props, children };
}

function isObject(val) {
  return val !== null && typeof val === 'object';
}

function isArray(val) {
  return Array.isArray(val);
}

完整的 createVNode 实现

/**
 * 创建 VNode
 * @param {any} type - 节点类型
 * @param {object} props - 属性
 * @param {any} children - 子节点
 * @param {number} patchFlag - 优化标志
 * @param {object} dynamicProps - 动态属性列表
 * @returns {object} VNode
 */
function createVNode(type, props, children, patchFlag, dynamicProps) {
  // 处理 props
  props = normalizeProps(props);
  
  // 提取 key 和 ref
  const { key, ref } = props || {};
  
  // 计算 shapeFlag
  const shapeFlag = getShapeFlag(type, children);
  
  // 创建基础 VNode
  const vnode = {
    __v_isVNode: true,
    type,
    props: props || null,
    children,
    shapeFlag,
    
    // 优化相关
    patchFlag: patchFlag || 0,
    dynamicProps: dynamicProps || null,
    
    // 核心属性
    key,
    ref,
    
    // 运行时相关
    el: null,          // 真实 DOM
    anchor: null,      // 锚点(Fragment)
    component: null,   // 组件实例
    parent: null,      // 父 VNode
    
    // 其他
    scopeId: null,
    slotScopeIds: null
  };
  
  // 处理子节点
  normalizeChildren(vnode, children);
  
  // 如果有动态 children,记录
  if (shouldTrackDynamicChildren(vnode)) {
    vnode.dynamicChildren = [];
  }
  
  return vnode;
}

function normalizeProps(props) {
  if (!props) return null;
  
  // 移除 Vue 内部使用的特殊属性
  const { class: klass, style, ...rest } = props;
  
  // 合并 class
  if (klass) {
    rest.class = normalizeClass(klass);
  }
  
  // 合并 style
  if (style) {
    rest.style = normalizeStyle(style);
  }
  
  return rest;
}

function getShapeFlag(type, children) {
  let shapeFlag = 0;
  
  // 判断类型
  if (typeof type === 'string') {
    shapeFlag = ShapeFlags.ELEMENT;
  } else if (type === Text) {
    shapeFlag = ShapeFlags.TEXT_CHILDREN;
  } else if (type === Fragment) {
    shapeFlag = ShapeFlags.FRAGMENT;
  } else {
    shapeFlag = ShapeFlags.STATEFUL_COMPONENT;
  }
  
  // 判断子节点类型
  if (children) {
    if (typeof children === 'string') {
      shapeFlag |= ShapeFlags.TEXT_CHILDREN;
    } else if (Array.isArray(children)) {
      shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
    } else if (isObject(children)) {
      shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
    }
  }
  
  return shapeFlag;
}

function normalizeChildren(vnode, children) {
  if (!children) return;
  
  // 标准化文本子节点
  if (typeof children === 'string' || typeof children === 'number') {
    vnode.children = String(children);
  }
  
  // 标准化数组子节点
  if (Array.isArray(children)) {
    vnode.children = children.map(child => {
      if (typeof child === 'string') {
        return createTextVNode(child);
      }
      return child;
    });
  }
}

function shouldTrackDynamicChildren(vnode) {
  return vnode.patchFlag > 0 || 
         vnode.patchFlag === PatchFlags.HOISTED ||
         vnode.shapeFlag & ShapeFlags.COMPONENT;
}

// 工具函数:规范化 class
function normalizeClass(value) {
  if (typeof value === 'string') return value;
  if (Array.isArray(value)) {
    return value.map(normalizeClass).filter(Boolean).join(' ');
  }
  if (isObject(value)) {
    return Object.keys(value)
      .filter(key => value[key])
      .join(' ');
  }
  return '';
}

// 工具函数:规范化 style
function normalizeStyle(value) {
  if (typeof value === 'string') return value;
  if (Array.isArray(value)) {
    return Object.assign({}, ...value.map(normalizeStyle));
  }
  if (isObject(value)) return value;
  return {};
}

h 函数的完整版本

/**
 * 完整的 h 函数实现
 * 支持多种调用方式:
 * h('div')
 * h('div', { class: 'box' })
 * h('div', '文本')
 * h('div', {}, ['子节点1', '子节点2'])
 * h(Component, { props })
 */
function h(type, propsOrChildren, children) {
  const args = arguments.length;
  
  // h('div')
  if (args === 1) {
    return createVNode(type, null, null);
  }
  
  // h('div', {})
  if (args === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 第二个参数是 props
      return createVNode(type, propsOrChildren, null);
    } else {
      // 第二个参数是 children
      return createVNode(type, null, propsOrChildren);
    }
  }
  
  // h('div', {}, '文本')
  // h('div', {}, [])
  // h('div', {}, h('span'))
  if (args === 3) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 有 props
      return createVNode(type, propsOrChildren, children);
    } else {
      // 无 props
      return createVNode(type, null, propsOrChildren);
    }
  }
  
  // 更多参数(不常见)
  const props = propsOrChildren;
  const _children = Array.from(arguments).slice(2);
  return createVNode(type, props, _children);
}

实战:使用 h 函数创建组件

// 定义组件
const MyComponent = {
  setup(props) {
    const count = ref(0);
    
    return () => h('div', { class: 'counter' }, [
      h('h3', props.title),
      h('p', `计数: ${count.value}`),
      h('button', {
        onClick: () => count.value++
      }, '增加')
    ]);
  }
};

// 创建 VNode
const vnode = h(MyComponent, {
  title: '我的计数器'
});

// 模拟渲染
function render(vnode, container) {
  if (typeof vnode.type === 'object') {
    // 组件
    const component = vnode.type;
    const subTree = component.setup(vnode.props);
    render(subTree, container);
  } else if (typeof vnode.type === 'string') {
    // 元素
    const el = document.createElement(vnode.type);
    
    // 设置属性
    if (vnode.props) {
      Object.entries(vnode.props).forEach(([key, value]) => {
        if (key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), value);
        } else {
          el.setAttribute(key, value);
        }
      });
    }
    
    // 处理子节点
    if (typeof vnode.children === 'string') {
      el.textContent = vnode.children;
    } else if (Array.isArray(vnode.children)) {
      vnode.children.forEach(child => render(child, el));
    }
    
    container.appendChild(el);
    vnode.el = el;
  }
}

// 挂载
render(vnode, document.getElementById('app'));

结语

Vue3 的虚拟 DOM 在设计上进行了大量的优化,理解虚拟 DOM 的设计与实现,不仅帮助我们写出更高效的 Vue 应用,也为后续学习 diff 算法和渲染器打下坚实基础。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!