本文转载自微信公众号「全栈修仙之路」,作者阿宝哥。转载本文请联系全栈修仙之路公众号。
本文是 Vue 3.0 进阶系列 的第八篇文章,在这篇文章中,阿宝哥将带大家一起探索 Vue 3 中应用挂载的过程。在开始介绍应用挂载的过程之前,我们先来简单回顾一下第七篇介绍的 应用创建的过程:
一、应用挂载
在创建完 app 对象之后,就会调用 app.mount 方法执行应用挂载操作:
- "app">
-
虽然 app.mount 方法用起来很简单,但它内部涉及的处理逻辑还是蛮复杂的。这里阿宝哥利用 Chrome 开发者工具的 Performance 标签栏,记录了应用挂载的主要过程:
接下来,阿宝哥就会以前面的示例为例,来详细分析一下应用挂载过程中涉及的主要函数。
1.1 app.mount
app.mount 被定义在 runtime-dom/src/index.ts 文件中,具体实现如下所示:
- // packages/runtime-dom/src/index.ts
- app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
- const container = normalizeContainer(containerOrSelector) // ① 同时支持字符串和DOM对象
- if (!container) return
- const component = app._component
- // 若根组件非函数对象且未设置render和template属性,则使用容器的innerHTML作为模板的内容
- if (!isFunction(component) && !component.render && !component.template) { // ②
- component.template = container.innerHTML
- }
- container.innerHTML = '' // 在挂载前清空容器内容
- const proxy = mount(container, false, container instanceof SVGElement) // ③
- if (container instanceof Element) {
- container.removeAttribute('v-cloak') // 避免在网络不好或加载数据过大的情况下,页面渲染的过程中会出现Mustache标签
- container.setAttribute('data-v-app', '')
- }
- return proxy
- }
在 app.mount 方法内部主要分为以下 3 个流程:
- 规范化容器,normalizeContainer 函数参数 container 的类型是一个联合类型:Element | ShadowRoot | string,如果传入参数是字符串类型的话,会通过 document.querySelector API 来获取选择器对应的 DOM 元素。而对于其他类型的话,会直接返回传入的参数。
- 设置根组件的 template 属性,当根组件不是函数组件且根组件配置对象上没有 render 和 template 属性,则会使用容器元素上 innerHTML 的值作为根组件 template 属性的属性值。
- 调用 mount 方法执行真正的挂载操作。
1.2 mount
对于 app.mount 方法来说,最核心的流程是 mount 方法,所以下一步我们就来分析 mount 方法。
- // packages/runtime-core/src/apiCreateApp.ts
- export function createAppAPI
( - render: RootRenderFunction,
- hydrate?: RootHydrateFunction
- ): CreateAppFunction
{ - return function createApp(rootComponent, rootProps = null) {
- const app: App = (context.app = {
- _container: null,
- _context: context,
- // 省略部分代码
-
- mount(
- rootContainer: HostElement,
- isHydrate?: boolean,
- isSVG?: boolean
- ): any {
- if (!isMounted) {
- const vnode = createVNode( // ① 创建根组件对应的VNode对象
- rootComponent as ConcreteComponent,
- rootProps
- )
- vnode.appContext = context // ② 设置VNode对象上的应用上下文属性
- // 省略部分代码
- if (isHydrate && hydrate) {
- hydrate(vnode as VNode
, rootContainer as any) - } else {
- render(vnode, rootContainer, isSVG) // ③ 执行渲染操作
- }
- isMounted = true
- app._container = rootContainer
- ;(rootContainer as any).__vue_app__ = app
- return vnode.component!.proxy
- }
- },
- })
-
- return app
- }
- }
1.3 render
观察以上的 mount 函数可知,在 mount 方法内部会调用继续调用 render 函数执行渲染操作,该函数的具体实现如下:
- const render: RootRenderFunction = (vnode, container) => {
- if (vnode == null) {
- if (container._vnode) {
- unmount(container._vnode, null, null, true)
- }
- } else {
- patch(container._vnode || null, vnode, container)
- }
- flushPostFlushCbs()
- container._vnode = vnode
- }
对于首次渲染来说,此时的 vnode 不为 null(基于根组件创建的 VNode 对象),所以会执行 else 分支的流程,即调用 patch 函数。
1.4 patch
patch 函数被定义在 runtime-core/src/renderer.ts 文件中,该函数的签名如下所示:
- // packages/runtime-core/src/renderer.ts
- const patch: PatchFn = (
- n1, // old VNode
- n2, // new VNode
- container,
- anchor = null,
- parentComponent = null,
- parentSuspense = null,
- isSVG = false,
- slotScopeIds = null,
- optimized = false
- ) => { //...}
在 patch 函数内部,会根据 VNode 对象的类型执行不同的处理逻辑:
在上图中,我们看到了 Text、Comment 、Static 和 Fragment 这些类型,它们的定义如下:
- // packages/runtime-core/src/vnode.ts
- export const Text = Symbol(__DEV__ ? 'Text' : undefined)
- export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
- export const Static = Symbol(__DEV__ ? 'Static' : undefined)
- export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
- __isFragment: true
- new (): {
- $props: VNodeProps
- }
- }
除了上述的类型之外,在 default 分支,我们还看到了 ShapeFlags,该对象是一个枚举:
- // packages/shared/src/shapeFlags.ts
- export const enum ShapeFlags {
- ELEMENT = 1,
- FUNCTIONAL_COMPONENT = 1 << 1,
- STATEFUL_COMPONENT = 1 << 2,
- TEXT_CHILDREN = 1 << 3,
- ARRAY_CHILDREN = 1 << 4,
- SLOTS_CHILDREN = 1 << 5,
- TELEPORT = 1 << 6,
- SUSPENSE = 1 << 7,
- COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
- COMPONENT_KEPT_ALIVE = 1 << 9,
- COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
- }
那么 ShapeFlags 标志是什么时候设置的呢?其实在创建 VNode 对象时,就会设置该对象的 shapeFlag 属性,对应的判断规则如下所示:
- // packages/runtime-core/src/vnode.ts
- 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 {
- // 省略大部分方法
- const shapeFlag = isString(type)// 字符串类型
- ? ShapeFlags.ELEMENT
- : __FEATURE_SUSPENSE__ && isSuspense(type) // SUSPENSE类型
- ? ShapeFlags.SUSPENSE
- : isTeleport(type) // TELEPORT类型
- ? ShapeFlags.TELEPORT
- : isObject(type) // 对象类型
- ? ShapeFlags.STATEFUL_COMPONENT
- : isFunction(type) // 函数类型
- ? ShapeFlags.FUNCTIONAL_COMPONENT
- : 0
-
- const vnode: VNode = {
- __v_isVNode: true,
- [ReactiveFlags.SKIP]: true,
- // 省略大部分属性
- shapeFlag,
- appContext: null
- }
- normalizeChildren(vnode, children)
- return vnode
- }
1.5 processComponent
由以上代码可知,对于我们示例来说,根组件对应的 VNode 对象上 shapeFlag 的值为 ShapeFlags.STATEFUL_COMPONENT。因此,在执行 patch 方法时,将会调用 processComponent 函数:
- // packages/runtime-core/src/renderer.ts
- const processComponent = (
- n1: VNode | null,
- n2: VNode,
- container: RendererElement,
- anchor: RendererNode | null,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- isSVG: boolean, optimized: boolean
- ) => {
- if (n1 == null) { // 首次渲染
- if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
- // 处理keep-alive组件
- } else {
- mountComponent(
- n2, container, anchor,
- parentComponent, parentSuspense,
- isSVG, optimized
- )
- }
- } else { // 更新操作
- updateComponent(n1, n2, optimized)
- }
- }
1.6 mountComponent
对于首次渲染的场景,n1 的值为 null,我们的组件又不是 keep-alive 组件,所以会调用 mountComponent 函数挂载组件:
- // packages/runtime-core/src/renderer.ts
- const mountComponent: MountComponentFn = (
- initialVNode, container, anchor,
- parentComponent, parentSuspense, isSVG, optimized
- ) => {
- // 省略部分代码
- // ① 创建组件实例
- const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
- initialVNode, parentComponent, parentSuspense
- ))
- // ② 初始化组件实例
- setupComponent(instance)
- // ③ 设置渲染副作用函数
- setupRenderEffect(
- instance, initialVNode, container,
- anchor, parentSuspense, isSVG, optimized
- )
- }
在 mountComponent 函数内部,主要含有 3 个步骤:
- 调用 createComponentInstance 函数创建组件实例;
- 调用 setupComponent 函数初始化组件实例;
- 调用 setupRenderEffect 函数,设置渲染副作用函数。
1.7 createComponentInstance
下面我们将会逐一分析上述的 3 个步骤:
- // packages/runtime-core/src/component.ts
- export function createComponentInstance(
- vnode: VNode,
- parent: ComponentInternalInstance | null,
- suspense: SuspenseBoundary | null
- ) {
- const type = vnode.type as ConcreteComponent
- // inherit parent app context - or - if root, adopt from root vnode
- const appContext =
- (parent ? parent.appContext : vnode.appContext) || emptyAppContext
-
- const instance: ComponentInternalInstance = { // 创建组件实例
- uid: uid++, vnode, type, parent, appContext,
- root: null!, next: null, subTree: null!, update: null!,
- render: null, proxy: null, exposed: null, withProxy: null, effects: null,
- provides: parent ? parent.provides : Object.create(appContext.provides),
- // ...
- }
-
- if (__DEV__) {
- instance.ctx = createRenderContext(instance)
- } else {
- instance.ctx = { _: instance } // 设置实例上的上下文属性ctx
- }
- instance.root = parent ? parent.root : instance
- instance.emit = emit.bind(null, instance) // 设置emit属性,用于派发自定义事件
-
- return instance
- }
调用 createComponentInstance 函数后,会返回一个包含了多种属性的组件实例对象。
1.8 setupComponent
此外,在创建完组件实例后,会调用 setupComponent 函数执行组件初始化操作:
- // packages/runtime-core/src/component.ts
- export function setupComponent(
- instance: ComponentInternalInstance,
- isSSR = false
- ) {
- isInSSRComponentSetup = isSSR
- const { props, children } = instance.vnode
- const isStateful = isStatefulComponent(instance) // 判断是否状态组件
- initProps(instance, props, isStateful, isSSR) // 初始化props属性
- initSlots(instance, children) // 初始化slots
-
- const setupResult = isStateful
- ? setupStatefulComponent(instance, isSSR) // 初始化有状态组件
- : undefined
- isInSSRComponentSetup = false
- return setupResult
- }
在 setupComponent 函数中,会分别调用 initProps 和 initSlots 函数来初始化组件实例的 props 属性和 slots 属性。之后会通过 isStatefulComponent 函数来判断组件的类型:
- // packages/runtime-core/src/component.ts
- export function isStatefulComponent(instance: ComponentInternalInstance) {
- return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
- }
-
- // 在createVNode函数内部,会根据组件的type类型设置ShapeFlags标识
- const shapeFlag = isString(type)
- ? ShapeFlags.ELEMENT
- : __FEATURE_SUSPENSE__ && isSuspense(type)
- ? ShapeFlags.SUSPENSE
- : isTeleport(type)
- ? ShapeFlags.TELEPORT
- : isObject(type) // ComponentOptions 类型
- ? ShapeFlags.STATEFUL_COMPONENT
- : isFunction(type) // 函数式组件
- ? ShapeFlags.FUNCTIONAL_COMPONENT
- : 0
很明显,如果 type 是对象类型,则组件是有状态组件。而如果 type 是函数类型的话,则组件是函数组件。
1.9 setupStatefulComponent
对于有状态组件来说,还会继续调用 setupStatefulComponent 函数来初始化有状态组件:
- // packages/runtime-core/src/component.ts
- function setupStatefulComponent(
- instance: ComponentInternalInstance,
- isSSR: boolean
- ) {
- const Component = instance.type as ComponentOptions // 组件配置对象
-
- // 0. create render proxy property access cache
- instance.accessCache = Object.create(null)
- // 1. create public instance / render proxy
- // also mark it raw so it's never observed
- instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) // instance.ctx = { _: instance }
- // 2. call setup()
- const { setup } = Component // 组合式API中配置的setup函数
- if (setup) {
- // 处理组合式API的setup函数
- } else {
- finishComponentSetup(instance, isSSR)
- }
- }
在 setupStatefulComponent 函数内部,主要也可以分为 3 个步骤:
- 在组件实例上设置 accessCache 属性,即创建 render proxy 属性的访问缓存;
- 使用 Proxy API 设置组件实例的 render proxy 属性;
- 判断组件配置对象上是否设置了 setup 属性,如果当前组件配置对象不包含 setup 属性,则会走 else 分支,即调用 finishComponentSetup 函数。
接下来,我们来重点分析后面 2 个步骤。首先,我们先来分析 instance.proxy 属性。如果你对 Proxy API 不了解的话,可以看一下 你不知道的 Proxy 这篇文章。至于 proxy 属性有什么的作用,阿宝哥将在后续的文章中介绍。下面我们来回顾一下 Proxy 构造函数:
- const p = new Proxy(target, handler)
Proxy 构造函数支持两个参数:
- target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
- handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
对于 setupStatefulComponent 函数来说,target 参数指向的是组件实例 ctx 属性,即 { _: instance } 对象。而 handler 参数指向的是 PublicInstanceProxyHandlers 对象,该对象内部包含了 3 种类型的捕捉器:
- // vue-next/packages/runtime-core/src/componentPublicInstance.ts
- export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
- // 属性读取操作的捕捉器。
- get({ _: instance }: ComponentRenderContext, key: string) {
- // ...
- },
- // 属性设置操作的捕捉器。
- set(
- { _: instance }: ComponentRenderContext,
- key: string,
- value: any
- ): boolean {
- // ...
- },
- // in 操作符的捕捉器。
- has(
- {
- _: { data, setupState, accessCache, ctx, appContext, propsOptions }
- }: ComponentRenderContext,
- key: string
- ) {
- // ...
- }
- }
这里我们只要先知道 PublicInstanceProxyHandlers 对象中,包含了 get、set 和 has 这 3 种类型的捕捉器即可。至于捕捉器的内部处理逻辑,阿宝哥将在 Vue 3.0 进阶之应用挂载的过程下篇 中详细介绍。
1.10 finishComponentSetup
在设置好 instance.proxy 属性之后,会判断组件配置对象上是否设置了 setup 属性。对于前面的示例来说,会走 else 分支,即调用 finishComponentSetup 函数,该函数的具体实现如下:
- // packages/runtime-core/src/component.ts
- function finishComponentSetup(
- instance: ComponentInternalInstance,
- isSSR: boolean
- ) {
- const Component = instance.type as ComponentOptions
- // template / render function normalization
- if (__NODE_JS__ && isSSR) { // 服务端渲染的场景
- if (Component.render) {
- instance.render = Component.render as InternalRenderFunction
- }
- } else if (!instance.render) { // 组件实例中不包含render方法
- // could be set from setup()
- if (compile && Component.template && !Component.render) {
- // 编译组件的模板生成渲染函数
- Component.render = compile(Component.template, {
- isCustomElement: instance.appContext.config.isCustomElement,
- delimiters: Component.delimiters
- })
- }
- // 把渲染函数添加到instance实例的render属性中
- instance.render = (Component.render || NOOP) as InternalRenderFunction
- // for runtime-compiled render functions using `with` blocks, the render
- // proxy used needs a different `has` handler which is more performant and
- // also only allows a whitelist of globals to fallthrough.
- if (instance.render._rc) {
- instance.withProxy = new Proxy(
- instance.ctx,
- RuntimeCompiledPublicInstanceProxyHandlers
- )
- }
- }
- }
在分析 finishComponentSetup 函数前,我们来回顾一下示例中的代码:
- const app = createApp({
- data() {
- return {
- name: '我是阿宝哥'
- }
- },
- template: `大家好, {{name}}!`
- })
对于该示例而言,根组件配置对象并没有设置 render 属性。而且阿宝哥引入的是包含编译器的 vue.global.js 文件,所以会走 else if 分支。即会调用 compile 函数来对模板进行编译。那么编译后会生成什么呢?通过断点,我们可以轻易地看到模板编译后生成的渲染函数:
- (function anonymous() {
- const _Vue = Vue
-
- return function render(_ctx, _cache) {
- with (_ctx) {
- const { toDisplayString: _toDisplayString, createVNode: _createVNode,
- openBlock: _openBlock, createBlock: _createBlock } = _Vue
- return (_openBlock(), _createBlock("div", null, "大家好, " + _toDisplayString(name) + "!", 1))
- }
- }
- })
观察以上的代码可知,调用渲染函数之后会返回 createBlock 函数的调用结果,即 VNode 对象。另外,在 render 函数中,会通过 with 来设置渲染上下文。那么该渲染函数什么时候会被调用呢?对于这个问题,感兴趣的小伙伴可以先自行研究一下。
出于篇幅考虑,阿宝哥把应用挂载的过程分为上下两篇,在下一篇文章中阿宝哥将重点介绍 setupRenderEffect 函数。介绍完该函数之后,你将会知道渲染函数什么时候会被调用,到时候也会涉及响应式 API 的一些相关知识,对这部分内容还不熟悉的小伙伴可以先看看 Vue 3 的官方文档。
最后,阿宝哥用一张流程图来总结一下本文介绍的主要内容:
本文主要介绍了在 Vue 3 中组件挂载过程中涉及的一些核心函数,出于篇幅考虑,阿宝哥只介绍其中的一部分函数。此外,为了让大家能够更深入地理解 App 挂载的过程,阿宝哥从源码的角度分析了核心函数中的主要处理逻辑。
在下一篇文章中,阿宝哥将会继续介绍应用挂载过程中剩余的内容,同时也会解答本文留下的问题,感兴趣的小伙伴请继续关注下一篇文章。
二、参考资源
- MDN - Proxy
- Vue 3 官网 - 应用 API