Vue3 创建虚拟DOM

279 阅读5分钟

虚拟 DOM

虚拟 DOM (Virtual DOM,简称 VDOM),是使用 Javascript 对象来描述 UI 的方式。这个对象会与真实的 DOM 保持同步,下面我会举个例子来说明:

const vnode = {
  tag: "div",
  props: {
    id: "container",
    class: "header",
    onClick: () => {},
  },
  children: [
    /** 更多 vnode */
  ],
};

上面的 vnode​ 就是一个虚拟 DOM,它代表了一个

​ 元素。

createVNode

createVNode方法主要功能就是创造虚拟dom,在介绍之前先看看js中的与、或运算

export const enum ShapeFlags {
  ELEMENT = 1, // 元素
  FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件  向左移100000010
  STATEFUL_COMPONENT = 1 << 2, // 普通组件      向左移200000100
  TEXT_CHILDREN = 1 << 3, // 孩子是文本         向左移300001000
  ARRAY_CHILDREN = 1 << 4, // 孩子是数组        向左移400010000
  SLOTS_CHILDREN = 1 << 5, // 组件插槽          向左移500100000
  TELEPORT = 1 << 6, // teleport组件            向左移601000000
  SUSPENSE = 1 << 7, // suspense组件            向左移710000000
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT 	// 组件   00000100 | 00000010 = 00000110
}

xport const enum PatchFlags {
  // 动态文本节点
  TEXT = 1,
  // 2 动态class
  CLASS = 1 << 1,
  // 4 动态style
  STYLE = 1 << 2,
  // 8 动态属性,但不好汉class style
  PROPS = 1 << 3,
  // 16 具有动态key属性,当key改变时,需要进行完整的diff
  FULL_PROPS = 1 << 4,
  // 32 带有监听事件的节点
  HYDRATE_EVENTS = 1 << 5,
  // 64 一个不会改变子节点顺序的fragment
  STABLE_FRAGMENT = 1 << 6,
  // 128 带有key的fragment
  KEYED_FRAGMENT = 1 << 7,
  // 256 没有key的fragment
  UNKEYED_FRAGMENT = 1 << 8,
  // 512 一个子节点只会进行非props比较
  NEED_PATCH = 1 << 9,
  // 1024 动态插槽
  DYNAMIC_SLOTS = 1 << 10,
  // 下面是特殊的,即在diff阶段会被跳过的
  // 2048 表示仅因为用户在模板的根级别放置注释而创建的片段,这是一个仅用于开发的标志,因为注释在生产中被剥离
  DEV_ROOT_FRAGMENT = 1 << 11,
  // 静态节点,它的内容永远不会改变,不需要进行diff
  HOISTED = -1,
  // 用来表示一个节点的diff应该结束
  BAIL = -2
}

我们看到,PatchFlags被定义为十几种的枚举类型,用以更精准的定位diff阶段需要对比节点部分,实现更精准的靶向更新​。PatchFlags大致被分为了两类:

  • 值大于0,即代表所对应的element在patch阶段,可以进行优化diff
  • 值小于0,即代表所对应的element在patch阶段,不需要进行diff
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false,
): VNode {
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    type = Comment
  }

  if (isVNode(type)) {
    // createVNode receiving an existing vnode. This happens in cases like
    // <component :is="vnode"/>
    // #2078 make sure to merge refs during the clone instead of overwriting it
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
      if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
        currentBlock[currentBlock.indexOf(type)] = cloned
      } else {
        currentBlock.push(cloned)
      }
    }
    cloned.patchFlag = PatchFlags.BAIL
    return cloned
  }

  // class component normalization.
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 2.x async/functional component compat
  if (__COMPAT__) {
    type = convertLegacyComponent(type, currentRenderingInstance)
  }

  // class & style normalization.
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    props = guardReactiveProps(props)!
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true,
  )
}


function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false,
) {
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetStart: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null,
    ctx: currentRenderingInstance,
  } as VNode

  if (needFullChildrenNormalization) {
    normalizeChildren(vnode, children)
    // normalize suspense children
    if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
      ;(type as typeof SuspenseImpl).normalize(vnode)
    }
  } else if (children) {
    // compiled element vnode - if children is passed, only possible types are
    // string or Array.
    vnode.shapeFlag |= isString(children)
      ? ShapeFlags.TEXT_CHILDREN
      : ShapeFlags.ARRAY_CHILDREN
  }

  // track vnode for block tree
  if (
    isBlockTreeEnabled > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== PatchFlags.NEED_HYDRATION
  ) {
    currentBlock.push(vnode)
  }

  if (__COMPAT__) {
    convertLegacyVModelProps(vnode)
    defineLegacyVNodeProperties(vnode)
  }

  return vnode
}

h()函数

​h()​ 函数用于辅助创建虚拟 DOM 节点,它是 hypescript​ 的简称————能生成 HTML (超文本标记语言) 的 JavaScript,它有另外一个名称,叫做 createVnode()​。

​h()​函数接收参数如下:

  • ​type​:类型参数,必填。内容为字符串或者 Vue 组件定义。
  • ​propsOrChildren​:propsOrChildren参数,非必填。传递内容是一个对象或子节点数组,对象内容包括了即将创建的节点的属性,例如 id​、class​、style​等,节点的事件监听也是通过 props 参数进行传递,并且以 on​ 开头,以 onXxx​ 的格式进行书写,如 onInput​、onClick​ 等,节点数组也是个对象数组。
  • ​children​:子节点,非必填。内容可以是文本、虚拟 DOM 节点和插槽等等。
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
// 判读参数个数决定后续的创建逻辑
  const l = arguments.length
  if (l === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // single vnode without props
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // props without children
      return createVNode(type, propsOrChildren)
    } else {
      // omit props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

使用方法如下:

import { h } from 'vue'

// 只有 type 参数为必填
h('div')

// attribute 和 property 都可以用于 prop 
// Vue 会自动选择正确的方式来分配它
h('div', { id: 'vue3' } )
h('div', { class: 'group', innerHTML: 'hello Vue3' })
h('div', { onClick: () =>{} })

// children 为字符串
h('div', { id: 'vue3' }, 'hello Vue3')
// props 参数为空
h('div', 'hello Vue3')

// children 嵌套使用
h('div', ['hello', h('span', 'Vue3')])