【vue3源码】十二、意识虚构DOM

什么是虚构DOM?

虚构DOM(也能够称为vnode)形容了一个实在的DOM构造,它和实在DOM一样都是由很多节点组成的一个树形构造。实质其实就是一个JS对象,如下就是一个vnode

{  type: 'div',  props: {    id: 'container'  },  children: [    {      type: 'span',      props: {        class: 'span1'      },      children: 'Hello '    },    {      type: 'span',      props: {        class: 'span2'      },      children: 'World'    },  ]}

下面这个vnode形容的实在DOM构造如下:

<div id="container">  <span class="text1">Hello </span>  <span class="text2">World</span></div>

能够发现,虚构节点的type形容了标签的类型,props形容了标签的属性,children形容了标签的子节点。当然一个vnode不仅只有这三个属性。vue3中对vnode的类型定义如下:

export interface VNode<  HostNode = RendererNode,  HostElement = RendererElement,  ExtraProps = { [key: string]: any }> {  // 标记为一个VNode  __v_isVNode: true  // 禁止将VNode解决为响应式对象  [ReactiveFlags.SKIP]: true  // 节点类型  type: VNodeTypes  // 节点的属性  props: (VNodeProps & ExtraProps) | null  // 便与DOM的复用,次要用在diff算法中  key: string | number | symbol | null  // 被用来给元素或子组件注册援用信息  ref: VNodeNormalizedRef | null  scopeId: string | null  slotScopeIds: string[] | null  // 子节点  children: VNodeNormalizedChildren  // 组件实例  component: ComponentInternalInstance | null  // 指令信息  dirs: DirectiveBinding[] | null  transition: TransitionHooks<HostElement> | null  // DOM  // vnode对应的DOM  el: HostNode | null  anchor: HostNode | null // fragment anchor  // teleport须要挂载的指标DOM  target: HostElement | null  // teleport挂载所需的锚点  targetAnchor: HostNode | null     // 对于Static vnode所蕴含的动态节点数量  staticCount: number  // suspense组件的边界  suspense: SuspenseBoundary | null  // suspense的default slot对应的vnode  ssContent: VNode | null  // suspense的fallback slot对应的vnode  ssFallback: VNode | null  // 用于优化的标记,次要用于判断节点类型  shapeFlag: number  // 用于diff优化的补丁标记  patchFlag: number  dynamicProps: string[] | null  dynamicChildren: VNode[] | null  // application root node only  appContext: AppContext | null  /**   * @internal attached by v-memo   */  memo?: any[]  /**   * @internal __COMPAT__ only   */  isCompatRoot?: true  /**   * @internal custom element interception hook   */  ce?: (instance: ComponentInternalInstance) => void}

如何创立虚构DOM

vue3对外提供了h()办法用于创立虚构DOM。所在文件门路:packages/runtime-core/src/h.ts

export function h(type: any, propsOrChildren?: any, children?: any): VNode {  const l = arguments.length  if (l === 2) {    // propsOrChildren是对象且不是数组    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {      // propsOrChildren是vnode      if (isVNode(propsOrChildren)) {        return createVNode(type, null, [propsOrChildren])      }      // 有props无子节点      return createVNode(type, propsOrChildren)    } else {      // 有子节点      return createVNode(type, null, propsOrChildren)    }  } else {    // 如果参数大于3,那么第三个参数及之后的参数都会被作为子节点解决    if (l > 3) {      children = Array.prototype.slice.call(arguments, 2)    } else if (l === 3 && isVNode(children)) {      children = [children]    }    return createVNode(type, propsOrChildren, children)  }}

h函数会应用createVNode函数创立虚构DOM。

export const createVNode = (  __DEV__ ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode

能够看到createVNode在开发环境下会应用createVNodeWithArgsTransform,其余环境下会应用_createVNode。这里咱们只看下_createVNode的实现。

<details>
<summary>_createVNode残缺代码</summary>

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) {    if (__DEV__ && !type) {      warn(`Invalid vnode type when creating vnode: ${type}.`)    }    type = Comment  }  // 如果type曾经是个vnode,则复制个新的vnode  if (isVNode(type)) {    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组件的type  if (isClassComponent(type)) {    type = type.__vccOpts  }  //  兼容2.x的异步及函数式组件  if (__COMPAT__) {    type = convertLegacyComponent(type, currentRenderingInstance)  }  // class、style的标准化  if (props) {    props = guardReactiveProps(props)!    let { class: klass, style } = props    if (klass && !isString(klass)) {      props.class = normalizeClass(klass)    }    if (isObject(style)) {      if (isProxy(style) && !isArray(style)) {        style = extend({}, style)      }      props.style = normalizeStyle(style)    }  }  // 依据type属性确定patchFlag  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  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {    type = toRaw(type)    warn(      `Vue received a Component which was made a reactive object. This can ` +        `lead to unnecessary performance overhead, and should be avoided by ` +        `marking the component with \`markRaw\` or using \`shallowRef\` ` +        `instead of \`ref\`.`,      `\nComponent that was made reactive: `,      type    )  }  return createBaseVNode(    type,    props,    children,    patchFlag,    dynamicProps,    shapeFlag,    isBlockNode,    true  )}

</details>

_createVNode能够承受6个参数:

  • typevnode类型
  • propsvnode的属性
  • children:子vnode
  • patchFlag:补丁标记,由编译器生成vnode时的优化提醒,在diff期间会进入对应优化
  • dynamicProps:动静属性
  • isBlockNode:是否是个Block节点

首先会对type进行校验,如果type是空的动静组件,进行提醒,并将type指定为一个Comment正文DOM。

if (!type || type === NULL_DYNAMIC_COMPONENT) {  if (__DEV__ && !type) {    warn(`Invalid vnode type when creating vnode: ${type}.`)  }  type = Comment}

如果type曾经是个vnode,会从type复制出一个新的vnode。这种状况次要在<component :is="vnode"/>状况下产生

if (isVNode(type)) {  const cloned = cloneVNode(type, props, true /* mergeRef: true */)  if (children) {    // 批改其children属性及欠缺shapeFlag属性    normalizeChildren(cloned, children)  }  // 将被拷贝的对象存入currentBlock中  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}

对于cloneVNode的实现:

export function cloneVNode<T, U>(  vnode: VNode<T, U>,  extraProps?: (Data & VNodeProps) | null,  mergeRef = false): VNode<T, U> {  const { props, ref, patchFlag, children } = vnode  // 如果存在extraProps,须要将extraProps和vnode的props进行合并  const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props  const cloned: VNode = {    __v_isVNode: true,    __v_skip: true,    type: vnode.type,    props: mergedProps,    // 如果过mergedProps中不存在key,则设置为null    key: mergedProps && normalizeKey(mergedProps),    //  如果过存在额定的ref    //      如果过须要合并ref    //          如果被拷贝节点中的ref是个数组,将调用normalizeRef解决ref,并将后果合并到被拷贝节点中的ref中    //          否则,创立一个新的数组,存储ref和normalizeRef(extraProps)的后果    //      否则间接调用normalizeRef(extraProps)解决新的ref    // 否则ref不变    ref:      extraProps && extraProps.ref        ? mergeRef && ref          ? isArray(ref)            ? ref.concat(normalizeRef(extraProps)!)            : [ref, normalizeRef(extraProps)!]          : normalizeRef(extraProps)        : ref,    scopeId: vnode.scopeId,    slotScopeIds: vnode.slotScopeIds,    children:      __DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children)        ? (children as VNode[]).map(deepCloneVNode)        : children,    target: vnode.target,    targetAnchor: vnode.targetAnchor,    staticCount: vnode.staticCount,    shapeFlag: vnode.shapeFlag,    // 如果 vnode 应用额定的 props 克隆,咱们不能再假如其现有的补丁标记是牢靠的,须要增加 FULL_PROPS 标记    // 如果存在extraProps,并且vnode.type不是是Fragment片段的状况下:    //    如果patchFlag为-1,阐明是动态节点,它的内容不会发生变化。新的vnode的patchFlag为PatchFlags.FULL_PROPS,示意props中存在动静key    //    如果patchFlag不为-1,将patchFlag与PatchFlags.FULL_PROPS进行或运算    // 否则patchFlag放弃不变    patchFlag:      extraProps && vnode.type !== Fragment        ? patchFlag === -1 // hoisted node          ? PatchFlags.FULL_PROPS          : patchFlag | PatchFlags.FULL_PROPS        : patchFlag,    dynamicProps: vnode.dynamicProps,    dynamicChildren: vnode.dynamicChildren,    appContext: vnode.appContext,    dirs: vnode.dirs,    transition: vnode.transition,    component: vnode.component,    suspense: vnode.suspense,    ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),    ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),    el: vnode.el,    anchor: vnode.anchor  }  // 用于兼容vue2  if (__COMPAT__) {    defineLegacyVNodeProperties(cloned)  }  return cloned as any}

在复制节点的过程中次要解决经验以下步骤:

  1. 被拷贝节点的props与额定的props的合并
  2. 创立新的vnode

    • key的解决:取合并后的props中的key,如果不存在,取null
    • ref的合并:依据是否须要合并ref,决定是否合并ref
    • patchFlag的解决:如果vnode应用额定的props克隆,补丁标记不再牢靠的,须要增加FULL_PROPS标记
    • ssContent的解决:应用cloneVNode复制被拷贝节点的ssContent
    • ssFallback的解决:应用cloneVNode复制被拷贝节点的ssFallback
  3. 兼容vue2
  4. 返回新的vnode

在克隆vnode时,props会应用mergeProps进行合并:

export function mergeProps(...args: (Data & VNodeProps)[]) {  const ret: Data = {}  for (let i = 0; i < args.length; i++) {    const toMerge = args[i]    for (const key in toMerge) {      if (key === 'class') {        if (ret.class !== toMerge.class) {          // 建设一个数组并调用normalizeClass,最终class会是字符串的模式          ret.class = normalizeClass([ret.class, toMerge.class])        }      } else if (key === 'style') {        // 建设style数组并调用normalizeStyle,最终style是对象模式        ret.style = normalizeStyle([ret.style, toMerge.style])      } else if (isOn(key)) { // 以on结尾的属性,对立按事件处理        const existing = ret[key]        const incoming = toMerge[key]        // 如果曾经存在的key对应事件与incoming不同,并且曾经存在的key对应事件中不蕴含incoming        if (          incoming &&          existing !== incoming &&          !(isArray(existing) && existing.includes(incoming))        ) {          // 如果过存在existing,将existing、incoming合并到一个新的数组中          ret[key] = existing            ? [].concat(existing as any, incoming as any)            : incoming        }      } else if (key !== '') { // 其余状况间接对ret[key]进行赋值,靠后合并的值会取代之前的值        ret[key] = toMerge[key]      }    }  }  return ret}

对于normalizeClassnormalizeStyle的实现:

export function normalizeClass(value: unknown): string {  let res = ''  if (isString(value)) {    res = value  } else if (isArray(value)) {    for (let i = 0; i < value.length; i++) {      const normalized = normalizeClass(value[i])      if (normalized) {        res += normalized + ' '      }    }  } else if (isObject(value)) {    for (const name in value) {      if (value[name]) {        res += name + ' '      }    }  }  return res.trim()}export function normalizeStyle(  value: unknown): NormalizedStyle | string | undefined {  if (isArray(value)) {    const res: NormalizedStyle = {}    for (let i = 0; i < value.length; i++) {      const item = value[i]      const normalized = isString(item)        ? parseStringStyle(item)        : (normalizeStyle(item) as NormalizedStyle)      if (normalized) {        for (const key in normalized) {          res[key] = normalized[key]        }      }    }    return res  } else if (isString(value)) {    return value  } else if (isObject(value)) {    return value  }}const listDelimiterRE = /;(?![^(]*\))/gconst propertyDelimiterRE = /:(.+)/export function parseStringStyle(cssText: string): NormalizedStyle {  const ret: NormalizedStyle = {}  cssText.split(listDelimiterRE).forEach(item => {    if (item) {      const tmp = item.split(propertyDelimiterRE)      tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim())    }  })  return ret}

回到_createVNode中,当复制出一个新的vnode后,调用了一个normalizeChildren办法,该办法的作用是对新复制的vnode,批改其children属性及欠缺shapeFlag属性

export function normalizeChildren(vnode: VNode, children: unknown) {  let type = 0  const { shapeFlag } = vnode  // 如果children为null或undefined,children取null  if (children == null) {    children = null  } else if (isArray(children)) {    // 如果过children数数组,type改为ShapeFlags.ARRAY_CHILDREN    type = ShapeFlags.ARRAY_CHILDREN  } else if (typeof children === 'object') { // 如果children是对象    // 如果vndoe是element或teleport    if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.TELEPORT)) {      // 取默认插槽      const slot = (children as any).default      if (slot) {        // _c 标记由 withCtx() 增加,示意这是一个已编译的插槽        slot._c && (slot._d = false)        // 将默认插槽的后果作为vnode的children        normalizeChildren(vnode, slot())        slot._c && (slot._d = true)      }      return    } else {      type = ShapeFlags.SLOTS_CHILDREN      const slotFlag = (children as RawSlots)._      if (!slotFlag && !(InternalObjectKey in children!)) {         // 如果槽未规范化,则附加上下文实例(编译过或规范话的slots曾经有上下文)        ;(children as RawSlots)._ctx = currentRenderingInstance      } else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {        // 子组件接管来自父组件的转发slots。        // 它的插槽类型由其父插槽类型决定。        if (          (currentRenderingInstance.slots as RawSlots)._ === SlotFlags.STABLE        ) {          ;(children as RawSlots)._ = SlotFlags.STABLE        } else {          ;(children as RawSlots)._ = SlotFlags.DYNAMIC          vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS        }      }    }  } else if (isFunction(children)) { // 如果过children是function    children = { default: children, _ctx: currentRenderingInstance }    type = ShapeFlags.SLOTS_CHILDREN  } else {    children = String(children)    // force teleport children to array so it can be moved around    if (shapeFlag & ShapeFlags.TELEPORT) {      type = ShapeFlags.ARRAY_CHILDREN      children = [createTextVNode(children as string)]    } else {      type = ShapeFlags.TEXT_CHILDREN    }  }  vnode.children = children as VNodeNormalizedChildren  vnode.shapeFlag |= type}

而后判断vnode是否应该被收集到Block中,并返回拷贝的节点。

如果type不是vnode,在办法最初会调用一个createBaseVNode创立vnode

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,    targetAnchor: null,    staticCount: 0,    shapeFlag,    patchFlag,    dynamicProps,    dynamicChildren: null,    appContext: null  } 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  }  // validate key  if (__DEV__ && vnode.key !== vnode.key) {    warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)  }  // 收集vnode到block树中  if (    isBlockTreeEnabled > 0 &&    // 防止block本人收集本人    !isBlockNode &&    // 存在父block    currentBlock &&    // vnode.patchFlag须要大于0或shapeFlag中存在ShapeFlags.COMPONENT    // patchFlag的存在表明该节点须要修补更新。    // 组件节点也应该总是打补丁,因为即便组件不须要更新,它也须要将实例长久化到下一个 vnode,以便当前能够正确卸载它    (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.HYDRATE_EVENTS  ) {    currentBlock.push(vnode)  }  if (__COMPAT__) {    convertLegacyVModelProps(vnode)    defineLegacyVNodeProperties(vnode)  }  return vnode}

总结

虚构DOM的创立流程:

  1. 如果type是个空的动静组件,将vnode.type指定为Comment正文节点。
  2. 如果type曾经是个vnode,则拷贝一个新的vnode返回。
  3. 解决class component
  4. 兼容vue2的异步组件及函数式组件
  5. classstyle的标准化
  6. 依据type属性初步确定patchFlag
  7. 调用createBaseVNode办法创立vnode并返回

createBaseVNode

  1. 先创立一个vnode对象
  2. 欠缺childrenpatchFlag属性
  3. 判断是否应该被父Block收集
  4. 解决兼容vue2
  5. 返回vnode