mount都发生了什么

前言

这是vue3系列源码的第二章,使用的vue3版本是3.2.45

mount的流程

js 复制代码
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

这是我们在vue3项目中main.js文件中经常出现的代码,在上一篇,我们分析了createApp的整个流程, 这个函数最终返回的是一个有mount属性的对象。

这里我们老规矩,先把打个断点,看一下调用了mount的这个app对象长啥样。

那么我们就看一下,这个mount函数到底干了什么。

runtime-dom部分中的createApp函数中,有这么一段:

js 复制代码
 const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

这里的normalizeContainer函数其实是将我们传入的#app字符串,变成了DOM对象。

接下来,调用了在之前在runtime-core部分中createAppAPI函数中定义好的mount函数。

mount

js 复制代码
     mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) 
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

          if (isHydrate && hydrate) {
             ...
          } else {
            render(vnode, rootContainer, isSVG)
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }

          return getExposeProxy(vnode.component!) || vnode.component!.proxy
        } else if (__DEV__) {
          warn(...)
        }
      }

在上一篇createApp的文章中,我们有看到函数中执行了这么一句代码

js 复制代码
let isMounted = false

所以,在这里的if条件语句是true,会执行createVNode函数。

从代码上看也能很明白的看清楚,

js 复制代码
const vnode = createVNode( rootComponent as ConcreteComponent, rootProps )

createVNode函数应该是返回了一个vnode对象。

然后把我们上一篇文章中提到的这个context对象挂载到vnode中。

js 复制代码
vnode.appContext = context

接着就执行了render函数。

js 复制代码
 render(vnode, rootContainer, isSVG)

render函数执行完之后会把isMounted置为true。

在最后,mount返回了getExposeProxy函数的执行,它主要用于实现 Composition API 中的 expose 方法。

下面我们按个去仔细看看上面提到的这些函数到底是长啥样。

首先createVNode函数的实质其实是 _createVNode()函数

_createVNode

js 复制代码
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 {
  // encode the vnode type information into a bitmap
  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
  }

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}

这里传进来的参数中,type 其实就是我们的App.vue

这个函数中真正执行了的代码,其实就上面这么点。

shapeFlag 是一个位掩码(bitmask),用于表示虚拟节点(VNode)的类型和特征,

它的每个比特位代表了不同的虚拟节点类型或特征。在 Vue 3 中,它被用于快速判断和识别虚拟节点的类型,从而在处理虚拟节点时进行相应的优化和处理。

我们的type这里是一个object,所以是ShapeFlags.STATEFUL_COMPONENT, 值是4。

_createVNode函数的最后,返回了另一个函数的执行,createBaseVNode函数。

createBaseVNode

js 复制代码
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,
    ctx: currentRenderingInstance
  } as VNode

  return vnode
}

这个函数其实就是返回了一个vnode对象。

render

这个render函数是定义在runtime-core部分的函数

js 复制代码
const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }

我们先确认一下传进来的参数:

vnode是上一个_createVNode函数生成的对象,container是根据我们传进来的#app生成的DOM对象,isSVG是false。

所以我们直接走到了patch函数,对组件进行各种处理。

接下来是flushPreFlushCbs函数,用于执行预处理的回调函数。

接下来是flushPostFlushCbs函数,用于执行后处理回调的函数。

这两个函数我们将在后面的部分详解。

最后,把vnode挂载到DOM对象上。

patch

patch函数是在上一篇文章中说的baseCreateRenderer中定义的。

js 复制代码
 const patch: PatchFn = (
   n1,
   n2,
   container,
   anchor = null,
   parentComponent = null,
   parentSuspense = null,
   isSVG = false,
   slotScopeIds = null,
   optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
 ) => {
   const { type, ref, shapeFlag } = n2
   switch (type) {
     case Text:
       processText(n1, n2, container, anchor)
       break
     case Comment:
       ...
       break
     case Static:
       ...
       break
     case Fragment:
       ...
       break
     default:
       if (shapeFlag & ShapeFlags.ELEMENT) {
         processElement(...)
       } else if (shapeFlag & ShapeFlags.COMPONENT) {
         processComponent(
           n1,
           n2,
           container,
           anchor,
           parentComponent,
           parentSuspense,
           isSVG,
           slotScopeIds,
           optimized
         )
       } else if (shapeFlag & ShapeFlags.TELEPORT) {
         ;(type as typeof TeleportImpl).process(...)
       } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
         ;(type as typeof SuspenseImpl).process(...)
       } else if (__DEV__) {
         warn('Invalid VNode type:', type, `(${typeof type})`)
       }
   }
 }

老规矩,我们先看一下传入的参数:

  • n1为null
  • n2为生成的vnode
  • container为DOM对象

后面的参数都是默认取值

上面已经说过,我们的type其实就是App.vue文件, 所以switch流程会走到default里面进行判断。

上面也提过,我们的shapeFlag的值是4,在这里做的位运算里面,和ShapeFlags.COMPONENT的值6,进行按位与得到的值是4,为true

按位与:将各个数位的数字进行逻辑与,都是 1 才为 1,否则为 0。

100 & 110 = 100

那么接下来就进入到processComponent,这个函数对组件进行一系列的处理。

processComponent

processComponent函数和patch一样,也是在baseCreateRenderer函数中定义的

js 复制代码
  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(...)
      } else {
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

看一下参数:

  • n1为null
  • n2为vnode
  • container为DOM对象

我们这里的n1为null,ShapeFlags.COMPONENT_KEPT_ALIVE的值是512,按位与的结果是0,所以直接进入mountComponent函数。

mountComponent

大家肯定都能猜得到,mountComponent函数还是在baseCreateRenderer函数中定义的,就在processComponent的下一个。

js 复制代码
 const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {

    const instance: ComponentInternalInstance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

    // resolve props and slots for setup context
    if (!(__COMPAT__ && compatMountInstance)) {
      setupComponent(instance)
    }
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }

这里先调用了createComponentInstance函数,得到了组件实例。

接着调用了setupComponent,主要负责在创建组件实例时,初始化组件的各项配置,设置组件的状态、属性、事件等内容。

最后调用了setupRenderEffect ,他是 Vue 3 源码中负责设置渲染效果的一部分。在 Vue 中,渲染函数是由 setupRenderEffect 来管理的,它的作用是确保组件的渲染能够响应数据的变化并进行更新。

createComponentInstance

createComponentInstance函数是定义在runtime-core中。

js 复制代码
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!, // to be immediately set
    next: null,
    subTree: null!, // will be set synchronously right after creation
    effect: null!,
    update: null!, // will be set synchronously right after creation
    scope: new EffectScope(true /* detached */),
    render: null,
    proxy: null,
    exposed: null,
    exposeProxy: null,
    withProxy: null,
    provides: parent ? parent.provides : Object.create(appContext.provides),
    accessCache: null!,
    renderCache: [],

    // local resolved assets
    components: null,
    directives: null,

    // resolved props and emits options
    propsOptions: normalizePropsOptions(type, appContext),
    emitsOptions: normalizeEmitsOptions(type, appContext),

    // emit
    emit: null!, // to be set immediately
    emitted: null,

    // props default value
    propsDefaults: EMPTY_OBJ,

    // inheritAttrs
    inheritAttrs: type.inheritAttrs,

    // state
    ctx: EMPTY_OBJ,
    data: EMPTY_OBJ,
    props: EMPTY_OBJ,
    attrs: EMPTY_OBJ,
    slots: EMPTY_OBJ,
    refs: EMPTY_OBJ,
    setupState: EMPTY_OBJ,
    setupContext: null,

    // suspense related
    suspense,
    suspenseId: suspense ? suspense.pendingId : 0,
    asyncDep: null,
    asyncResolved: false,

    // lifecycle hooks
    // not using enums here because it results in computed properties
    isMounted: false,
    isUnmounted: false,
    isDeactivated: false,
    bc: null,
    c: null,
    bm: null,
    m: null,
    bu: null,
    u: null,
    um: null,
    bum: null,
    da: null,
    a: null,
    rtg: null,
    rtc: null,
    ec: null,
    sp: null
  }
  if (__DEV__) {
    instance.ctx = createDevRenderContext(instance)
  } else {
    instance.ctx = { _: instance }
  }
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  return instance
}

这个函数的主要作用就是生成了一个实例对象并返回。 实例中的scope属性

js 复制代码
scope: new EffectScope(true /* detached */)

这个EffectScope类是定义在reactivity部分的。

EffectScope 类是 Vue 3 源码中用于管理副作用函数执行的环境和依赖关系的重要组成部分。它主要用于跟踪和管理副作用函数,确保它们能够按照正确的顺序执行,并正确处理它们之间的依赖关系。

我们将在后面的文章中展开分析,这里不做展开。 normalizePropsOptionsnormalizeEmitsOptions分别是对propsemits做规范化处理的。

instance的ctx字段指向的其实是instance自己。

这里因为是根组件,所以root字段指向的也是自己。

setupComponent

setupComponent函数是定义在runtime-core部分的

js 复制代码
function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

setupComponent 主要负责:

  1. 创建组件实例:在组件初始化时创建组件实例,处理组件的状态、方法以及响应式数据等。
  2. 设置组件的 propsslotsattrs:处理组件的属性、插槽和特性等,确保它们被正确地传递和管理。
  3. 处理生命周期钩子 :包括 beforeCreatecreatedbeforeMountmounted 等生命周期钩子的调用和管理。
  4. 准备渲染上下文:为组件的渲染提供上下文环境,并准备好响应式更新的机制。
  5. 处理渲染函数 :设置组件的 render 函数,确定组件如何渲染和更新。

这里我们不做展开。

setupRenderEffect

setupRenderEffect函数也是在baseCreateRenderer函数中定义的。

js 复制代码
  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const componentUpdateFn = () => {...}

    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))

    const update: SchedulerJob = (instance.update = () => effect.run())
    update.id = instance.uid
    // allowRecurse
    // #1801, #2043 component render effects should allow recursive updates
    toggleRecurse(instance, true)
    update()
  }

在 Vue 3 的源码中,setupRenderEffect 主要负责以下工作:

  1. 建立响应式关联:将组件的渲染函数与数据之间建立响应式的关联,以确保当数据发生变化时能够触发重新渲染。
  2. 收集依赖:在渲染函数中收集数据的依赖,当这些依赖的数据发生变化时,触发重新渲染。
  3. 设置副作用函数:建立数据变化时的副作用函数,这个函数用于执行实际的更新操作,例如调用渲染函数来生成新的虚拟 DOM 并进行比对,最终更新视图。
  4. 处理更新时机:管理组件的更新时机,包括何时触发首次渲染、何时进行数据变化时的更新等。

这里我们不做展开。

getExposeProxy

getExposeProxy定义在runtime-core部分。

js 复制代码
function getExposeProxy(instance: ComponentInternalInstance) {
  if (instance.exposed) {
    return (
      instance.exposeProxy ||
      (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
        get(target, key: string) {
          if (key in target) {
            return target[key]
          } else if (key in publicPropertiesMap) {
            return publicPropertiesMap[key](instance)
          }
        },
        has(target, key: string) {
          return key in target || key in publicPropertiesMap
        }
      }))
    )
  }
}

这里有两个点注意一下:

  • markRaw 函数是用于标记对象,使其成为"非响应式"的。这意味着当对象被标记为 markRaw 后,Vue 的响应式系统将不会对其进行代理,也不会进行响应式追踪。
  • proxyRefs 函数是 Vue 3 源码中的一个实用工具函数,用于将 ref 对象的包装进行代理。这个函数的作用是为了确保在访问 ref 对象中的值时,能够自动获取其 value 属性,同时保持响应式。

所以proxyRefs(markRaw(instance.exposed)) 用于处理通过 expose 方法暴露给组件外部的属性和方法,确保这些属性和方法既不会触发响应式更新,又能在访问时自动获取其值。

至此,就是mount的全部过程。

流程图

graph TB subgraph mount["mount(rootContainer, isHydrate, isSVG)"] direction TB createVnode("createVnode(rootComponent, rootProps)") --vnode--> render("render(vnode, rootContainer, isSVG)") --> isMounted("isMounted = true") --return--> getExposeProxy("getExposeProxy(vnode.component))") end
graph LR subgraph funcreateVnode["createVnode(rootComponent, rootProps)"] direction LR createdVNode --return--> _createVNode --return--> createBaseVNode --return--> vnode end
graph LR subgraph funrender["render(vnode, rootContainer, isSVG)"] direction LR render --> patch --> processComponent --> mountComponent --> flushPreFlushCbs --> flushPostFlushCbs end
相关推荐
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
天天进步20152 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端
程序猿阿伟3 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒3 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript