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

【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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理