关于前端:vue3源码十二认识虚拟DOM

7次阅读

共计 13018 个字符,预计需要花费 33 分钟才能阅读完成。

【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 = /;(?![^(]*\))/g
const 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
正文完
 0