写点什么

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

用户头像
梁龙先森
关注
发布于: 2021 年 03 月 27 日
Vue3源码 | 如何挂载组件元素?

上一小节文章,讲述了 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 // 描述节点的操作


发布于: 2021 年 03 月 27 日阅读数: 17
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
Vue3源码 | 如何挂载组件元素?