文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Vue 3.0 进阶之应用挂载的过程之一

2024-12-03 10:27

关注

本文转载自微信公众号「全栈修仙之路」,作者阿宝哥。转载本文请联系全栈修仙之路公众号。  

 本文是 Vue 3.0 进阶系列 的第八篇文章,在这篇文章中,阿宝哥将带大家一起探索 Vue 3 中应用挂载的过程。在开始介绍应用挂载的过程之前,我们先来简单回顾一下第七篇介绍的 应用创建的过程:

一、应用挂载

在创建完 app 对象之后,就会调用 app.mount 方法执行应用挂载操作:

  1. "app">
 
  •  
  • 虽然 app.mount 方法用起来很简单,但它内部涉及的处理逻辑还是蛮复杂的。这里阿宝哥利用 Chrome 开发者工具的 Performance 标签栏,记录了应用挂载的主要过程:

    接下来,阿宝哥就会以前面的示例为例,来详细分析一下应用挂载过程中涉及的主要函数。

    1.1 app.mount

    app.mount 被定义在 runtime-dom/src/index.ts 文件中,具体实现如下所示:

    1. // packages/runtime-dom/src/index.ts 
    2. app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { 
    3.   const container = normalizeContainer(containerOrSelector) // ① 同时支持字符串和DOM对象 
    4.   if (!container) return 
    5.   const component = app._component 
    6.   // 若根组件非函数对象且未设置render和template属性,则使用容器的innerHTML作为模板的内容 
    7.   if (!isFunction(component) && !component.render && !component.template) { // ② 
    8.     component.template = container.innerHTML 
    9.   } 
    10.   container.innerHTML = ''  // 在挂载前清空容器内容 
    11.   const proxy = mount(container, false, container instanceof SVGElement) // ③ 
    12.   if (container instanceof Element) { 
    13.     container.removeAttribute('v-cloak') // 避免在网络不好或加载数据过大的情况下,页面渲染的过程中会出现Mustache标签 
    14.     container.setAttribute('data-v-app'''
    15.   } 
    16.   return proxy 

    在 app.mount 方法内部主要分为以下 3 个流程:

    1.2 mount

    对于 app.mount 方法来说,最核心的流程是 mount 方法,所以下一步我们就来分析 mount 方法。

    1. // packages/runtime-core/src/apiCreateApp.ts 
    2. export function createAppAPI
    3.   render: RootRenderFunction, 
    4.   hydrate?: RootHydrateFunction 
    5. ): CreateAppFunction { 
    6.   return function createApp(rootComponent, rootProps = null) { 
    7.     const app: App = (context.app = { 
    8.       _container: null
    9.       _context: context, 
    10.      // 省略部分代码 
    11.        
    12.       mount( 
    13.         rootContainer: HostElement, 
    14.         isHydrate?: boolean, 
    15.         isSVG?: boolean 
    16.       ): any { 
    17.         if (!isMounted) { 
    18.           const vnode = createVNode( // ① 创建根组件对应的VNode对象 
    19.             rootComponent as ConcreteComponent, 
    20.             rootProps 
    21.           ) 
    22.           vnode.appContext = context // ② 设置VNode对象上的应用上下文属性 
    23.      // 省略部分代码 
    24.           if (isHydrate && hydrate) { 
    25.             hydrate(vnode as VNode, rootContainer as any
    26.           } else {  
    27.             render(vnode, rootContainer, isSVG) // ③ 执行渲染操作 
    28.           } 
    29.           isMounted = true 
    30.           app._container = rootContainer 
    31.           ;(rootContainer as any).__vue_app__ = app 
    32.           return vnode.component!.proxy 
    33.         } 
    34.       }, 
    35.     }) 
    36.  
    37.     return app 
    38.   } 

    1.3 render

    观察以上的 mount 函数可知,在 mount 方法内部会调用继续调用 render 函数执行渲染操作,该函数的具体实现如下:

    1. const render: RootRenderFunction = (vnode, container) => { 
    2.   if (vnode == null) { 
    3.     if (container._vnode) { 
    4.       unmount(container._vnode, nullnulltrue
    5.     } 
    6.   } else { 
    7.       patch(container._vnode || null, vnode, container) 
    8.   } 
    9.   flushPostFlushCbs() 
    10.   container._vnode = vnode 

    对于首次渲染来说,此时的 vnode 不为 null(基于根组件创建的 VNode 对象),所以会执行 else 分支的流程,即调用 patch 函数。

    1.4 patch

    patch 函数被定义在 runtime-core/src/renderer.ts 文件中,该函数的签名如下所示:

    1. // packages/runtime-core/src/renderer.ts 
    2. const patch: PatchFn = ( 
    3.     n1, // old VNode 
    4.     n2, // new VNode 
    5.     container, 
    6.     anchor = null
    7.     parentComponent = null
    8.     parentSuspense = null
    9.     isSVG = false
    10.     slotScopeIds = null
    11.     optimized = false 
    12. ) => { //...} 

    在 patch 函数内部,会根据 VNode 对象的类型执行不同的处理逻辑:

    在上图中,我们看到了 Text、Comment 、Static 和 Fragment 这些类型,它们的定义如下:

    1. // packages/runtime-core/src/vnode.ts 
    2. export const Text = Symbol(__DEV__ ? 'Text' : undefined) 
    3. export const Comment = Symbol(__DEV__ ? 'Comment' : undefined) 
    4. export const Static = Symbol(__DEV__ ? 'Static' : undefined) 
    5. export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as anyas { 
    6.   __isFragment: true 
    7.   new (): { 
    8.     $props: VNodeProps 
    9.   } 

    除了上述的类型之外,在 default 分支,我们还看到了 ShapeFlags,该对象是一个枚举:

    1. // packages/shared/src/shapeFlags.ts 
    2. export const enum ShapeFlags { 
    3.   ELEMENT = 1, 
    4.   FUNCTIONAL_COMPONENT = 1 << 1, 
    5.   STATEFUL_COMPONENT = 1 << 2, 
    6.   TEXT_CHILDREN = 1 << 3, 
    7.   ARRAY_CHILDREN = 1 << 4, 
    8.   SLOTS_CHILDREN = 1 << 5, 
    9.   TELEPORT = 1 << 6, 
    10.   SUSPENSE = 1 << 7, 
    11.   COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, 
    12.   COMPONENT_KEPT_ALIVE = 1 << 9, 
    13.   COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT 

    那么 ShapeFlags 标志是什么时候设置的呢?其实在创建 VNode 对象时,就会设置该对象的 shapeFlag 属性,对应的判断规则如下所示:

    1. // packages/runtime-core/src/vnode.ts 
    2. function _createVNode( 
    3.   type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, 
    4.   props: (Data & VNodeProps) | null = null
    5.   children: unknown = null
    6.   patchFlag: number = 0, 
    7.   dynamicProps: string[] | null = null
    8.   isBlockNode = false 
    9. ): VNode { 
    10.   // 省略大部分方法 
    11.   const shapeFlag = isString(type)// 字符串类型 
    12.     ? ShapeFlags.ELEMENT 
    13.     : __FEATURE_SUSPENSE__ && isSuspense(type) // SUSPENSE类型 
    14.       ? ShapeFlags.SUSPENSE 
    15.       : isTeleport(type) // TELEPORT类型 
    16.         ? ShapeFlags.TELEPORT 
    17.         : isObject(type) // 对象类型 
    18.           ? ShapeFlags.STATEFUL_COMPONENT 
    19.           : isFunction(type) // 函数类型 
    20.             ? ShapeFlags.FUNCTIONAL_COMPONENT 
    21.             : 0 
    22.  
    23.   const vnode: VNode = { 
    24.     __v_isVNode: true
    25.     [ReactiveFlags.SKIP]: true
    26.   // 省略大部分属性 
    27.     shapeFlag, 
    28.     appContext: null 
    29.   } 
    30.   normalizeChildren(vnode, children) 
    31.   return vnode 

    1.5 processComponent

    由以上代码可知,对于我们示例来说,根组件对应的 VNode 对象上 shapeFlag 的值为 ShapeFlags.STATEFUL_COMPONENT。因此,在执行 patch 方法时,将会调用 processComponent 函数:

    1. // packages/runtime-core/src/renderer.ts   
    2. const processComponent = ( 
    3.   n1: VNode | null,  
    4.   n2: VNode, 
    5.   container: RendererElement, 
    6.   anchor: RendererNode | null
    7.   parentComponent: ComponentInternalInstance | null
    8.   parentSuspense: SuspenseBoundary | null
    9.   isSVG: boolean, optimized: boolean 
    10.   ) => { 
    11.     if (n1 == null) { // 首次渲染 
    12.       if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { 
    13.         // 处理keep-alive组件 
    14.       } else { 
    15.         mountComponent( 
    16.           n2, container, anchor, 
    17.           parentComponent, parentSuspense, 
    18.           isSVG, optimized 
    19.         ) 
    20.       } 
    21.     } else { // 更新操作 
    22.       updateComponent(n1, n2, optimized) 
    23.     } 

    1.6 mountComponent

    对于首次渲染的场景,n1 的值为 null,我们的组件又不是 keep-alive 组件,所以会调用 mountComponent 函数挂载组件:

    1. // packages/runtime-core/src/renderer.ts   
    2. const mountComponent: MountComponentFn = ( 
    3.   initialVNode, container, anchor,  
    4.   parentComponent, parentSuspense, isSVG, optimized 
    5. ) => { 
    6.     // 省略部分代码 
    7.     // ① 创建组件实例 
    8.     const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( 
    9.       initialVNode, parentComponent, parentSuspense 
    10.     )) 
    11.     // ② 初始化组件实例 
    12.     setupComponent(instance) 
    13.     // ③ 设置渲染副作用函数 
    14.     setupRenderEffect( 
    15.       instance, initialVNode, container, 
    16.       anchor, parentSuspense, isSVG, optimized 
    17.     ) 

    在 mountComponent 函数内部,主要含有 3 个步骤:

    1.7 createComponentInstance

    下面我们将会逐一分析上述的 3 个步骤:

    1. // packages/runtime-core/src/component.ts 
    2. export function createComponentInstance( 
    3.   vnode: VNode, 
    4.   parent: ComponentInternalInstance | null
    5.   suspense: SuspenseBoundary | null 
    6. ) { 
    7.   const type = vnode.type as ConcreteComponent 
    8.   // inherit parent app context - or - if root, adopt from root vnode 
    9.   const appContext = 
    10.     (parent ? parent.appContext : vnode.appContext) || emptyAppContext 
    11.  
    12.   const instance: ComponentInternalInstance = { // 创建组件实例 
    13.     uid: uid++, vnode, type, parent, appContext, 
    14.     root: null!, nextnull, subTree: null!, updatenull!,  
    15.     render: null, proxy: null, exposed: null, withProxy: null, effects: null
    16.     provides: parent ? parent.provides : Object.create(appContext.provides), 
    17.     // ...  
    18.   } 
    19.    
    20.   if (__DEV__) { 
    21.     instance.ctx = createRenderContext(instance) 
    22.   } else { 
    23.     instance.ctx = { _: instance } // 设置实例上的上下文属性ctx 
    24.   } 
    25.   instance.root = parent ? parent.root : instance 
    26.   instance.emit = emit.bind(null, instance) // 设置emit属性,用于派发自定义事件 
    27.  
    28.   return instance 

    调用 createComponentInstance 函数后,会返回一个包含了多种属性的组件实例对象。

    1.8 setupComponent

    此外,在创建完组件实例后,会调用 setupComponent 函数执行组件初始化操作:

    1. // packages/runtime-core/src/component.ts 
    2. export function setupComponent( 
    3.   instance: ComponentInternalInstance, 
    4.   isSSR = false 
    5. ) { 
    6.   isInSSRComponentSetup = isSSR 
    7.   const { props, children } = instance.vnode 
    8.   const isStateful = isStatefulComponent(instance) // 判断是否状态组件 
    9.   initProps(instance, props, isStateful, isSSR) // 初始化props属性 
    10.   initSlots(instance, children) // 初始化slots 
    11.  
    12.   const setupResult = isStateful 
    13.     ? setupStatefulComponent(instance, isSSR) // 初始化有状态组件 
    14.     : undefined 
    15.   isInSSRComponentSetup = false 
    16.   return setupResult 

    在 setupComponent 函数中,会分别调用 initProps 和 initSlots 函数来初始化组件实例的 props 属性和 slots 属性。之后会通过 isStatefulComponent 函数来判断组件的类型:

    1. // packages/runtime-core/src/component.ts 
    2. export function isStatefulComponent(instance: ComponentInternalInstance) { 
    3.   return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT 
    4.  
    5. // 在createVNode函数内部,会根据组件的type类型设置ShapeFlags标识 
    6. const shapeFlag = isString(type) 
    7.     ? ShapeFlags.ELEMENT 
    8.     : __FEATURE_SUSPENSE__ && isSuspense(type) 
    9.       ? ShapeFlags.SUSPENSE 
    10.       : isTeleport(type) 
    11.         ? ShapeFlags.TELEPORT 
    12.         : isObject(type) // ComponentOptions 类型 
    13.           ? ShapeFlags.STATEFUL_COMPONENT 
    14.           : isFunction(type) // 函数式组件 
    15.             ? ShapeFlags.FUNCTIONAL_COMPONENT 
    16.             : 0 

    很明显,如果 type 是对象类型,则组件是有状态组件。而如果 type 是函数类型的话,则组件是函数组件。

    1.9 setupStatefulComponent

    对于有状态组件来说,还会继续调用 setupStatefulComponent 函数来初始化有状态组件:

    1. // packages/runtime-core/src/component.ts 
    2. function setupStatefulComponent( 
    3.   instance: ComponentInternalInstance, 
    4.   isSSR: boolean 
    5. ) { 
    6.   const Component = instance.type as ComponentOptions // 组件配置对象 
    7.    
    8.   // 0. create render proxy property access cache 
    9.   instance.accessCache = Object.create(null
    10.   // 1. create public instance / render proxy 
    11.   // also mark it raw so it's never observed 
    12.   instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) // instance.ctx = { _: instance } 
    13.   // 2. call setup() 
    14.   const { setup } = Component // 组合式API中配置的setup函数 
    15.   if (setup) { 
    16.     // 处理组合式API的setup函数 
    17.   } else { 
    18.     finishComponentSetup(instance, isSSR) 
    19.   } 

    在 setupStatefulComponent 函数内部,主要也可以分为 3 个步骤:

    接下来,我们来重点分析后面 2 个步骤。首先,我们先来分析 instance.proxy 属性。如果你对 Proxy API 不了解的话,可以看一下 你不知道的 Proxy 这篇文章。至于 proxy 属性有什么的作用,阿宝哥将在后续的文章中介绍。下面我们来回顾一下 Proxy 构造函数:

    1. const p = new Proxy(target, handler) 

    Proxy 构造函数支持两个参数:

    对于 setupStatefulComponent 函数来说,target 参数指向的是组件实例 ctx 属性,即 { _: instance } 对象。而 handler 参数指向的是 PublicInstanceProxyHandlers 对象,该对象内部包含了 3 种类型的捕捉器:

    1. // vue-next/packages/runtime-core/src/componentPublicInstance.ts 
    2. export const PublicInstanceProxyHandlers: ProxyHandler<any> = { 
    3.   // 属性读取操作的捕捉器。 
    4.   get({ _: instance }: ComponentRenderContext, key: string) { 
    5.     // ... 
    6.   }, 
    7.   // 属性设置操作的捕捉器。 
    8.   set
    9.     { _: instance }: ComponentRenderContext, 
    10.     key: string, 
    11.     value: any 
    12.   ): boolean { 
    13.     // ... 
    14.   }, 
    15.   // in 操作符的捕捉器。 
    16.   has( 
    17.     { 
    18.       _: { data, setupState, accessCache, ctx, appContext, propsOptions } 
    19.     }: ComponentRenderContext, 
    20.     key: string 
    21.   ) { 
    22.    // ... 
    23.   } 

    这里我们只要先知道 PublicInstanceProxyHandlers 对象中,包含了 get、set 和 has 这 3 种类型的捕捉器即可。至于捕捉器的内部处理逻辑,阿宝哥将在 Vue 3.0 进阶之应用挂载的过程下篇 中详细介绍。

    1.10 finishComponentSetup

    在设置好 instance.proxy 属性之后,会判断组件配置对象上是否设置了 setup 属性。对于前面的示例来说,会走 else 分支,即调用 finishComponentSetup 函数,该函数的具体实现如下:

    1. // packages/runtime-core/src/component.ts 
    2. function finishComponentSetup( 
    3.   instance: ComponentInternalInstance, 
    4.   isSSR: boolean 
    5. ) { 
    6.   const Component = instance.type as ComponentOptions 
    7.   // template / render function normalization 
    8.   if (__NODE_JS__ && isSSR) { // 服务端渲染的场景 
    9.     if (Component.render) { 
    10.       instance.render = Component.render as InternalRenderFunction 
    11.     } 
    12.   } else if (!instance.render) { // 组件实例中不包含render方法 
    13.     // could be set from setup() 
    14.     if (compile && Component.template && !Component.render) { 
    15.       // 编译组件的模板生成渲染函数 
    16.       Component.render = compile(Component.template, { 
    17.         isCustomElement: instance.appContext.config.isCustomElement, 
    18.         delimiters: Component.delimiters 
    19.       }) 
    20.     } 
    21.     // 把渲染函数添加到instance实例的render属性中 
    22.     instance.render = (Component.render || NOOP) as InternalRenderFunction 
    23.     // for runtime-compiled render functions using `with` blocks, the render 
    24.     // proxy used needs a different `has` handler which is more performant and 
    25.     // also only allows a whitelist of globals to fallthrough. 
    26.     if (instance.render._rc) { 
    27.       instance.withProxy = new Proxy( 
    28.         instance.ctx, 
    29.         RuntimeCompiledPublicInstanceProxyHandlers 
    30.       ) 
    31.     } 
    32.   } 

    在分析 finishComponentSetup 函数前,我们来回顾一下示例中的代码:

    1. const app = createApp({ 
    2.   data() { 
    3.     return { 
    4.       name'我是阿宝哥' 
    5.     } 
    6.   }, 
    7.   template: `
      大家好, {{name}}!
    8. }) 

    对于该示例而言,根组件配置对象并没有设置 render 属性。而且阿宝哥引入的是包含编译器的 vue.global.js 文件,所以会走 else if 分支。即会调用 compile 函数来对模板进行编译。那么编译后会生成什么呢?通过断点,我们可以轻易地看到模板编译后生成的渲染函数:

    1. (function anonymous() { 
    2. const _Vue = Vue 
    3.  
    4. return function render(_ctx, _cache) { 
    5.   with (_ctx) { 
    6.     const { toDisplayString: _toDisplayString, createVNode: _createVNode,  
    7.       openBlock: _openBlock, createBlock: _createBlock } = _Vue 
    8.     return (_openBlock(), _createBlock("div"null"大家好, " + _toDisplayString(name) + "!", 1)) 
    9.   } 
    10. }) 

    观察以上的代码可知,调用渲染函数之后会返回 createBlock 函数的调用结果,即 VNode 对象。另外,在 render 函数中,会通过 with 来设置渲染上下文。那么该渲染函数什么时候会被调用呢?对于这个问题,感兴趣的小伙伴可以先自行研究一下。

    出于篇幅考虑,阿宝哥把应用挂载的过程分为上下两篇,在下一篇文章中阿宝哥将重点介绍 setupRenderEffect 函数。介绍完该函数之后,你将会知道渲染函数什么时候会被调用,到时候也会涉及响应式 API 的一些相关知识,对这部分内容还不熟悉的小伙伴可以先看看 Vue 3 的官方文档。

    最后,阿宝哥用一张流程图来总结一下本文介绍的主要内容:

    本文主要介绍了在 Vue 3 中组件挂载过程中涉及的一些核心函数,出于篇幅考虑,阿宝哥只介绍其中的一部分函数。此外,为了让大家能够更深入地理解 App 挂载的过程,阿宝哥从源码的角度分析了核心函数中的主要处理逻辑。

    在下一篇文章中,阿宝哥将会继续介绍应用挂载过程中剩余的内容,同时也会解答本文留下的问题,感兴趣的小伙伴请继续关注下一篇文章。

    二、参考资源

     

     

    来源:全栈修仙之路内容投诉

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    软考中级精品资料免费领

    • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

      难度     813人已做
      查看
    • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

      难度     354人已做
      查看
    • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

      难度     318人已做
      查看
    • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

      难度     435人已做
      查看
    • 2024年上半年系统架构设计师考试综合知识真题

      难度     224人已做
      查看

    相关文章

    发现更多好内容

    猜你喜欢

    AI推送时光机
    位置:首页-资讯-后端开发
    咦!没有更多了?去看看其它编程学习网 内容吧
    首页课程
    资料下载
    问答资讯