Vue3源码 | 如何挂载组件元素?

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

上一小节文章,讲述了 createApp 创建应用实例主流程逻辑,这篇就跟着上章文章继续看看虚拟节点的创建、渲染以及挂载。注意,这篇主要讲述新组件挂载流程,关于组件更新相关内容下篇再继续。

附上应用上下文执行mount的源码:PS:文章目录顺序及执行流程。

    // ... 其他代码省略
     mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          // 根据根组件创建虚拟节点
          const vnode = createVNode(rootComponent as Component, rootProps)
          vnode.appContext = context
          // HMR root reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer)
            }
          }
 					// 渲染虚拟节点
          render(vnode, rootContainer)
          
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app
          return vnode.component!.proxy
        }
      },
复制代码

如果你对上一篇内容不熟悉,建议先熟悉下流程,内容文章如下:

Vue3源码 | createApp都干了什么?

createVNode

这个方法是根据组件和组件属性,生成一个VNode虚拟节点。

虚拟节点是什么,有什么好处呢?

VNode的本质是一个描述DOM的JavaScript对象,是对抽象事物的描述。

  1. 跨平台
  2. 为数据驱动视图提供了媒介
  3. 对于频繁通过JavaScript操作DOM的场景,VNode性能更优,因为它会等收集到足够的改变时,再将这些变化一次性应用到真实的DOM上。

下面看下生成VNode的源码:

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

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) {
    // 不传,默认Comment类型的虚拟节点
    type = Comment
  }
  // 已经是虚拟节点,则克隆一个,返回
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // 类组件标准化。
  if (isFunction(type) && '__vccOpts' in type) {
    type = type.__vccOpts
  }

  // style和class标准化。
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    if (isProxy(props) || InternalObjectKey in props) {
      props = extend({}, 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)
    }
  }

  // 将vnode类型信息编码为位图
  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
	// 创建虚拟节点
  const vnode: VNode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    el: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
		... // 其他属性省略
  }

  // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
  normalizeChildren(vnode, children)
  ...
  
  return vnode
}
复制代码

从代码解析我们可以看出,createVNode做了如下几件事:

  1. 对属性props标准化
  2. 将VNode类型信息进行编码为位图
  3. 创建VNode对象
  4. 对子节点进行标准化

render

存在VNode了,下面看看如果进行渲染。


  const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) { 
      // 虚拟节点为null,则销毁容器内的虚拟节点
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 创建、或者更新节点,创建的时候这里container._vnode是不存在的
      // 第一个参数: 旧的虚拟节点
      // 第二个参数:新的vnode
      // 第三个参数:vnode转化为dom,最终要挂载的dom容器
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
复制代码

render函数很简单,根据传参决定是否销毁、还是创建或者更新组件。下面看看创建具体流程。

patch

包含了组件创建、和更新的相关实现,我们这里先看看创建相关逻辑。

 const patch: PatchFn = (
    n1,  // 旧节点
    n2,  // 新节点
    container, // DOM容器,vNode渲染成dom会挂载到该节点下
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
    // 存在旧节点,且新旧节点不同,则卸载旧节点
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }
		
    // PatchFlags.BAIL:一个特殊标志,表示differ算法应该退出优化模式
    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
 				// 处理文本节点
        break
      case Comment:
        // 处理注释节点
        break
      case Static:
       	// 处理静态节点
        break
      case Fragment:
        // 处理Fragment元素
        break
      default:
        // 处理DOM元素
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // 处理组件元素
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          // 处理TELEPORT
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          // 处理SUSPENSE
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
  }
复制代码

从源码可以看出,当新旧虚拟节点不同,会先卸载旧节点。且vNode存在八种不同的类型,在patch函数中,会根据vNode的类型去做对应的处理,挂载DOM,或者更新DOM。

下面我们看下具备代表性的如何处理组件和DOM元素。

processElement

看看如何挂载DOM元素的。

 const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
   	// 旧节点不存在的话,则挂载新元素,否则更新
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
    }
  }
复制代码

本文只看挂载元素的过程,下面看看 mountElement 方法,因为函数代码多,下面删减下,只关注主流程。

 const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const {
      type,
      props,
      shapeFlag,
      transition,
      scopeId,
      patchFlag,
      dirs
    } = vnode
    // 创建dom元素节点
    if (
      !__DEV__ &&
      vnode.el &&
      hostCloneNode !== undefined &&
      patchFlag === PatchFlags.HOISTED
    ) {
      // vNode.el非空,表示它要被重用,只有静态vNode可以被重用,这里采用克隆。
      // 只在生产中生效,克隆的树不能被HMR更新
      el = vnode.el = hostCloneNode(vnode.el)
    } else {
      // 1. 创建新元素
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is
      )

      // 2. 先挂载子节点,因为节点可以依赖于已经呈现的字节点内容
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        // 处理子节点是文本内容的情况
        hostSetElementText(el, vnode.children as string)
      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 处理子节点是数组的情况
        mountChildren(
          vnode.children as VNodeArrayChildren,
          el,
          null,
          parentComponent,
          parentSuspense,
          isSVG && type !== 'foreignObject',
          optimized || !!vnode.dynamicChildren
        )
      }

      // 当前元素el处理属性相关,如style/class/event等
      if (props) {
        for (const key in props) {
          if (!isReservedProp(key)) {
            hostPatchProp(
              el,
              key,
              null,
              props[key],
              isSVG,
              vnode.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
        // 处理节点挂载前的钩子函数
        if ((vnodeHook = props.onVnodeBeforeMount)) {
          invokeVNodeHook(vnodeHook, parentComponent, vnode)
        }
      }
    	// ... 其余元素特性处理,如,slot/transition
    }
		
   // 把元素挂载到容器上
    hostInsert(el, container, anchor)
  }
复制代码

整个过程我们可以缕清如下:

  1. 创建DOM元素,如果 vNode.el 非空且为静态虚拟节点,则直接克隆一个。
  2. 先挂载元素子节点,因为当前节点可能依赖子节点的属性。如果子节点是文本节点,则直接设置节点内容;如果节点是数组,则遍历子节点,递归执行patch操作。相关代码,可自行查看。
  3. 属性存在,则处理元素的相关属性。
  4. 挂载元素到容器 container 上。
processComponent

这里直接看组件挂载的逻辑。

  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 1. 创建组件实例
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    // 将keepAlive注入渲染器内部
    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }
		// 设置组件实例
    setupComponent(instance)
		
    // 设置并执行带副作用的渲染函数
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }
复制代码

可以看出该函数,执行了一下步骤:

  1. 创建组件实例
  2. 设置组件实例
  3. 执行带副作用的渲染函数

下面看看,如果带副作用的渲染函数是如何执行的。

 const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 创建响应式的副作用函数
    instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, a, parent } = instance
        // 组件实例生成子树vnode
        const subTree = (instance.subTree = renderComponentRoot(instance))
  
        // beforeMount hook
        if (bm) {
          invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if ((vnodeHook = props && props.onVnodeBeforeMount)) {
          invokeVNodeHook(vnodeHook, parent, initialVNode)
        }
        if (el && hydrateNode) {
     			// 省略
        } else {
      		// 把子树挂载到container上
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
      		// 保存渲染生成的子树根节点
          initialVNode.el = subTree.el
        }
        // mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        // onVnodeMounted
        if ((vnodeHook = props && props.onVnodeMounted)) {
          queuePostRenderEffect(() => {
            invokeVNodeHook(vnodeHook!, parent, initialVNode)
          }, parentSuspense)
        }
        //为keep-alive节点激活钩子.
        if (
          a &&
          initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        ) {
          queuePostRenderEffect(a, parentSuspense)
        }
        instance.isMounted = true
      } else {
				// 更新组件相关逻辑
      }
    }, prodEffectOptions)
  }
复制代码

从源码可以处理如下:

  1. effect创建了一个副作用渲染函数 componentEffect,当组件数据变化时,该函数会重新执行一次。
  2. 渲染组件生成子树subTree,并把子树挂载到
  3. 将子树的根节点保存到当前节点
  4. 整个组件挂载过程,执行了一些钩子函数,如beforeMount、Mount,以及 keep-alive 的处理。

总结

至此便阅读了组件挂载过程。首先会创建虚拟节点VNode,然后执行渲染逻辑。若创建的VNode为null,则组件执行卸载过程,否则执行创建或者更新流程。本篇文章讲解挂载过程,创建的VNode存在8种类型,

我们针对组件和元素进行了分析。挂载元素的流程是:创建DOM元素->更新元素属性->递归挂载子节点,这里DOM相关操作,可以参考 nodeOps.ts 文件,底子里也是通过dom api来完成。挂载组件的过程是,创建组件实例->设置组件实例->执行带副作用的渲染函数,渲染组件子树,关于组件渲染更细则实现可以阅读 componentRenderUtils.ts 文件。

相关代码如下:

packages/runtime-dom/src/index.ts

packages/runtime-core/src/vnode.ts // 虚拟节点

packages/runtime-core/src/renderer.ts // 渲染器

packages/runtime-core/src/componentRenderUtils.ts // 组件渲染方法

packages/runtime-dom/src/nodeOps.ts // 描述节点的操作

Guess you like

Origin juejin.im/post/7032600898295726093