相关文章推荐
笑点低的荒野  ·  List<t>.FindIndex 方法 ...·  8 月前    · 
重情义的青椒  ·  c++ - OpenCV Error: ...·  1 年前    · 
  • 该系列文章为个人学习总结,如有错误,欢迎指正;尚有不足,请多指教!
  • 阅读源码暂未涉及ssr服务端渲染,直接跳过
  • 部分调试代码(例如:console.warn等),不涉及主要内容,直接跳过
  • 涉及兼容vue2的代码直接跳过(例如:__FEATURE_OPTIONS_API__等)
  • 注意源码里的 __DEV__ 不是指 dev 调试,详情请看 rollup.config.js
  • mount组件挂载

    app.mount('#app') 看似简单的一行代码,但是在 vue 内部做了大量操作。还是回到上文提到的 createApp 函数:

    // @file core/packages/runtime-dom/src/index.ts
    export const createApp = ((...args) => {
      // ...省略
      const { mount } = app
      app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
        // 内部最终调用document.querySelector获取dom元素
        const container = normalizeContainer(containerOrSelector)
        if (!container) return
        // ... 省略
        // clear content before mounting
        container.innerHTML = ''
        // 执行window下dom渲染器的挂载函数
        const proxy = mount(container, false, container instanceof SVGElement)
        if (container instanceof Element) {
          // 移除v-cloak属性
          container.removeAttribute('v-cloak')
          // 跟元素设置data-v-app属性
          container.setAttribute('data-v-app', '')
        return proxy
      // ...省略
    }) as CreateAppFunction<Element>
    

    从上面代码可以看到,appmount主要先校验、获取、清空挂载目标容器,然后再将app挂载到目标容器上。最终还是调用createAppAPI函数里的mount。注意:常规的单文本组件被转换成包含setuprender等属性的对象。

    // @file core/packages/runtime-core/src/apiCreateApp.ts
    export function createAppAPI<HostElement>(
      render: RootRenderFunction<HostElement>,
      hydrate?: RootHydrateFunction
    ): CreateAppFunction<HostElement> {
      return function createApp(rootComponent, rootProps = null) {
        let isMounted = false
        // ...省略
        const app: App = (context.app = {
          // ...省略
          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
              // HMR root reload
              if (__DEV__) {
                context.reload = () => {
                  render(cloneVNode(vnode), rootContainer, isSVG)
              if (isHydrate && hydrate) {
                hydrate(vnode as VNode<Node, Element>, rootContainer as any)
              } else {
                render(vnode, rootContainer, isSVG)
              isMounted = true
              app._container = rootContainer
              // for devtools and telemetry
              ;(rootContainer as any).__vue_app__ = app
              return getExposeProxy(vnode.component!) || vnode.component!.proxy
        // ...省略
    

    app的挂载主要分为以下四步:

  • 构建当前app的虚拟节点,并绑定app上下文至其appContext
  • 注册热更新后重新加载根节点(同3)
  • 使用渲染器渲染生成的虚拟节点
  • 生成app的实例代理,暴露对外接口(这里和上一节没有返回app生成链式调用对应)
  • 生成虚拟节点

    // @file core/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 {
      if (!type || type === NULL_DYNAMIC_COMPONENT) {
        // 无效的type,生成注释节点
        type = Comment
      if (isVNode(type)) {
        // createVNode receiving an existing vnode. This happens in cases like
        // <component :is="vnode"/>
        // #2078 make sure to merge refs during the clone instead of overwriting it
        const cloned = cloneVNode(type, props, true /* mergeRef: true */)
        if (children) {
          normalizeChildren(cloned, children)
        // 以下代码涉及Block vnode,后面讨论
        if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
          if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
            currentBlock[currentBlock.indexOf(type)] = cloned
          } else {
            currentBlock.push(cloned)
        cloned.patchFlag |= PatchFlags.BAIL
        return cloned
      // class component normalization.
      if (isClassComponent(type)) {
        type = type.__vccOpts
      // 格式化处理传参的class和style属性
      if (props) {
        // 如果传入的props是响应式对象,转换为非响应式
        props = guardReactiveProps(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)
      // 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
      return createBaseVNode(
        type,
        props,
        children,
        patchFlag,
        dynamicProps,
        shapeFlag,
        isBlockNode,
    

    这里的_createVNode只是创建虚拟节点之前做的一些预处理:

  • 如果传入的type本身就是已经处理过的虚拟节点(例如自带的组件:<component is="com"></component>),则直接返回其拷贝值。
  • 根据传入的属性props,预处理一下classstyle
  • 使用ShapeFlags标记当前节点类型
  • 注意,创建vnode使用的props是使用组件时传入的参数,和type(即组件)上定义的组件props有区别(后续会用到)。

    // @file core/packages/runtime-core/src/vnode.ts
    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值
        key: props && normalizeKey(props),
        // 这里涉及用ref获取组件实例的操作,后续讨论
        ref: props && normalizeRef(props)
        // 省略
      } as VNode
      // ...省略
      return vnode
    

    调用createBaseVNode函数初始化虚拟节点对象。

    调用render渲染组件

    // @file core/packages/runtime-core/src/render.ts
    const patch: PatchFn = (
      container,
      anchor = null,
      parentComponent = null,
      parentSuspense = null,
      isSVG = false,
      slotScopeIds = null,
      optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
    ) => {
      if (n1 === n2) {
        return
      // 省略...
      const { type, ref, shapeFlag } = n2
      switch (type) {
        // 省略...
        default:
          // 省略...
          else if (shapeFlag & ShapeFlags.COMPONENT) {
            processComponent(
              container,
              anchor,
              parentComponent,
              parentSuspense,
              isSVG,
              slotScopeIds,
              optimized
          // 省略...
      // set ref
      if (ref != null && parentComponent) {
        setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    const render: RootRenderFunction = (vnode, container, isSVG) => {
      if (vnode == null) {
        // 如果虚拟节点返回null,就执行卸载
        if (container._vnode) {
          // 卸载后续讨论
          unmount(container._vnode, null, null, true)
      } else {
         * 传入的参数依次是:
         * 旧的虚拟节点
         * 新的虚拟节点
         * 目标dom
         * 父组件
         * 异步组件父组件
         * 是否是svg
        patch(container._vnode || null, vnode, container, null, null, null, isSVG)
      flushPreFlushCbs
    
    
    
    
        
    ()
      flushPostFlushCbs()
      // 将虚拟节点绑定在当前dom上,用于卸载或对比更新
      container._vnode = vnode
    

    可以看出渲染虚拟节点主要是执行patch方法。由于文章篇幅有限,就以常规组件——虚拟节点类型为ShapeFlags.COMPONENT的执行过程为例,其他类型就不一一列举。

    // @file core/packages/runtime-core/src/render.ts
    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(
            container,
            anchor,
            isSVG,
            optimized
        } else {
          mountComponent(
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
      } else {
        updateComponent(n1, n2, optimized)
    

    这里处理组件的方法processComponent也很简单,旧节点不存在就调用挂载组件(这里存在处理keep-alive的后续讨论),否则就调用更新组件。

    // @file core/packages/runtime-core/src/render.ts
    const mountComponent: MountComponentFn = (
      initialVNode,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    ) => {
      // 2.x compat may pre-create the component instance before actually
      // mounting
      const compatMountInstance =
        __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
      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
  • // @file core/packages/runtime-core/src/component.ts
    let uid = 0
    export function createComponentInstance(
      vnode: VNode,
      parent: ComponentInternalInstance | null,
      suspense: SuspenseBoundary | null
      // 对象组件
      const type = vnode.type as ConcreteComponent
      // 创建当前组件实例的上下文:
      // 如果有父组件就是父组件
      // 没有父组件就是根组件,也即appContext
      const appContext =
        (parent ? parent.appContext : vnode.appContext) || emptyAppContext
      const instance: ComponentInternalInstance = {
        // 组件id递增
        uid: uid++,
        vnode,
        type,
        parent,
        appContext,
        // 省略一些初始化属性...
        // app上的provides注册在空对象的原型上
        // 避免与当前组件定义的provides冲突
        // 关于provide详细定义和使用后续讨论
        provides: parent ? parent.provides : Object.create(appContext.provides),
        // 省略...
        // 格式化组件定义的props并缓存
        propsOptions: normalizePropsOptions(type, appContext),
        // 格式化组件定义的emits并缓存
        emitsOptions: normalizeEmitsOptions(type, appContext),
        // 省略一些初始化属性...
      if (__DEV__) {
        // 在ctx上定义组件自身对外可访问的属性,例如:$slots、$emit、$el、$attrs等
        instance.ctx = createDevRenderContext(instance)
      } else {
        instance.ctx = { _: instance }
      instance.root = parent ? parent.root : instance
      // 绑定事件触发函数并初始第一个参数为当前实例
      // 注意:emit绑定this为null,事件回调函数执行没有this
      instance.emit = emit.bind(null, instance)
      // 省略...
      return instance
    // @file core/packages/runtime-core/src/componentPublicInstance.ts
    export function createDevRenderContext(instance: ComponentInternalInstance) {
      const target: Record<string, any> = {}
      // expose internal instance for proxy handlers
      Object.defineProperty(target, `_`, {
        configurable: true,
        enumerable: false,
        get: () => instance
      // expose public properties
      Object.keys(publicPropertiesMap).forEach(key => {
        Object.defineProperty(target, key, {
          configurable: true,
          enumerable: false,
          get: () => publicPropertiesMap[key](instance),
          // intercepted by the proxy so no need for implementation,
          // but needed to prevent set errors
          set: NOOP
      return target as ComponentRenderContext
    // 组件对外属性
    // sfc中template内可直接使用的属性
    export const publicPropertiesMap: PublicPropertiesMap =
      extend(Object.create(null), {
        $: i => i,
        $el: i => i.vnode.el,
        $data: i => i.data,
        $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
        $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
        $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
        $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
        $parent: i => getPublicInstance(i.parent),
        $root: i => getPublicInstance(i.root),
        $emit: i => i.emit,
        $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
        $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
        $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
        $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
      } as PublicPropertiesMap)
    // @file core/packages/runtime-core/src/componentEmits.ts
    export function emit(
      instance: ComponentInternalInstance,
      event: string,
      ...rawArgs: any[]
      if (instance.isUnmounted) return
      const props = instance.vnode.props || EMPTY_OBJ
      if (__DEV__) {
        const {
          emitsOptions,
          propsOptions: [propsOptions]
        } = instance
        if (emitsOptions) {
          // 省略...
          const validator = emitsOptions[event]
          if (isFunction(validator)) {
            // 组件定义事件时如果传入的函数
            // 函数可以校验事件参数
            // 但是不中断执行
            const isValid = validator(...rawArgs)
            if (!isValid) {
              warn(
                `Invalid event arguments: event validation failed for event "${event}".`
          // 省略...
      let args = rawArgs
      const isModelListener = event.startsWith('update:')
      // for v-model update:xxx events, apply modifiers on args
      const modelArg = isModelListener && event.slice(7)
      if (modelArg && modelArg in props) {
        const modifiersKey = `${
          modelArg === 'modelValue' ? 'model' : modelArg
        }Modifiers`
        const { number, trim } = props[modifiersKey] || EMPTY_OBJ
        if (trim) {
          // trim 修饰符处理事件参数
          args = rawArgs.map(a => (isString(a) ? a.trim() : a))
        if (number) {
          // number 修饰符处理事件参数
          args = rawArgs.map(looseToNumber)
      // 省略...
      let handlerName
      let handler =
        props[(handlerName = toHandlerKey(event))] ||
        // also try camelCase event handler (#2249)
        props[(handlerName = toHandlerKey(camelize(event)))]
      // for v-model update:xxx events, also trigger kebab-case equivalent
      // for props passed via kebab-case
      if (!handler && isModelListener) {
        handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
      if (handler) {
        callWithAsyncErrorHandling(
          handler,
          instance,
          ErrorCodes.COMPONENT_EVENT_HANDLER,
      const onceHandler = props[handlerName + `Once`]
      if (onceHandler) {
        // 执行过的无需再次执行
        if (!instance.emitted) {
          instance.emitted
    
    
    
    
        
     = {} as Record<any, boolean>
        } else if (instance.emitted[handlerName]) {
          return
        instance.emitted[handlerName] = true
        callWithAsyncErrorHandling(
          onceHandler,
          instance,
          ErrorCodes.COMPONENT_EVENT_HANDLER,
    

    以上代码初始化了一些组件实例属性,预处理的组件定义的参数格式等。另外provides属性是从App上定义的原型上的一个对象,往子孙组件传递。ctx是实例上下文,在单文本组件中template内可以直接使用的属性,注意这里的$props$attrs$slots$refs都是响应式变量。

    // @file core/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)
      initSlots(instance, children)
      const setupResult = isStateful
        ? setupStatefulComponent(instance, isSSR)
        : undefined
      isInSSRComponentSetup = false
      return setupResult
    // @file core/packages/runtime-core/src/componentProps.ts
    export function initProps(
      instance: ComponentInternalInstance,
      rawProps: Data | null,
      isStateful: number, // result of bitwise flag comparison
      isSSR = false
      const props: Data = {}
      const attrs: Data = {}
      def(attrs, InternalObjectKey, 1)
      instance.propsDefaults = Object.create(null)
      // 传入的组件属性在组件props上定义的,设置为props
      // 没有定义的属性设置在attrs上,包括class、style和未注册的事件监听等
      setFullProps(instance, rawProps, props, attrs)
      // 定义的属性没有传值设置为undefined
      for (const key in instance.propsOptions[0]) {
        if (!(key in props)) {
          props[key] = undefined
      // ...省略
      if (isStateful) {
        // 注意:实例上的props被处理为shallowReactive
        instance.props = isSSR ? props : shallowReactive(props)
      } else {
        // 同上,组件如果未定义props,则设置为传入的attrs
        // 这点与常规组件有区别
        if (!instance.type.props) {
          // functional w/ optional props, props === attrs
          instance.props = attrs
        } else {
          // functional w/ declared props
          instance.props = props
      // 实例attrs,未作响应式处理
      // 但是组件内置$attrs、useAttrs变量做了响应式处理,后续讨论
      instance.attrs = attrs
    

    initProps方法主要是格式化组件定义属性和组件传入属性,使传入属性对应上定义属性。组件实例的propsshallowReactive而不是reactive本意为了维持数据流从父组件流向子组件这一开发思路。因为只有父组件可以修改props的第一层属性触发子组件响应式更新。至于props的深层属性的值变化,可以来自任何地方。 注意:未在组件内注册,但传入的事件,会被设置在attrs上。

    // @file core/packages/runtime-core/src/component.ts
    function createAttrsProxy(instance: ComponentInternalInstance): Data {
      return new Proxy(
        instance.attrs, {
          // 省略...
          get(target, key: string) {
            // 访问attrs属性进行了依赖收集
            // 注意: 这里收集的是$attrs属性,相当于shallowReadonly(attrs)
            // 与上文createDevRenderContext中$attrs处理方式一致
            track(instance, TrackOpTypes.GET, '$attrs')
            return target[key]
          // 省略...
    export function createSetupContext(
      instance: ComponentInternalInstance
    ): SetupContext {
      const expose: SetupContext['expose'] = exposed => {
        // 省略...
        // 注意:这里是覆盖式更新
        // 同一组件内多处调用expose方法会覆盖前一次的结果
        instance.exposed = exposed || {}
      let attrs: Data
      if (__DEV__) {
        // We use getters in dev in case libs like test-utils overwrite instance
        // properties (overwrites should not be done in prod)
        return Object.freeze({
          get attrs() {
            return attrs || (attrs = createAttrsProxy(instance))
          get slots() {
            // 与上文createDevRenderContext中$slots处理方式一致
            return shallowReadonly(instance.slots)
          get emit() {
            return (event: string, ...args: any[]) => instance.emit(event, ...args)
          expose
      } else {
        return {
          get attrs() {
            return attrs || (attrs = createAttrsProxy(instance))
          slots: instance.slots,
          emit: instance.emit,
          expose
    // @file core/packages/runtime-core/src/apiSetupHelpers.ts
    export function useSlots(): SetupContext['slots'] {
      // 获取响应式slots
      return getContext().slots
    export function useAttrs(): SetupContext['attrs'] {
      // 获取响应式attrs
      return getContext().attrs
    function getContext(): SetupContext {
      const i = getCurrentInstance()!
      // 省略...
      return i.setupContext || (i.setupContext = createSetupContext(i))
    // @file core/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)
      // 生成实例代理,后文讲述
      instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
      if (__DEV__) {
        // 在ctx上暴露组件定义props格式化之后的数据
        exposePropsOnRenderContext(instance)
      // 2. call setup()
      const { setup } = Component
      if (setup) {
        // 细节:setup函数参数小于2个不构建执行上下文
        const setupContext = (instance.setupContext =
          setup.length > 1 ? createSetupContext(instance) : null)
        // 设置当前实例为全局变量
        setCurrentInstance(instance)
        // setup执行期间禁止依赖收集,后续讨论
        pauseTracking()
        // 执行setup
        const setupResult = callWithErrorHandling(
          setup,
          instance,
          ErrorCodes.SETUP_FUNCTION,
          // setup函数执行参数:props(当前传入属性),setupContext
          // setupContext的四个只读属性:attrs,slots,emit,expose
          [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
        // 解除依赖收集限制
        resetTracking()
        // 释放全局设置的当前实例
        unsetCurrentInstance()
        if (isPromise(setupResult)) {
          // 异步setup后续讨论
          setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
          // 省略...
        } else {
          handleSetupResult(instance, setupResult, isSSR)
      } else {
        finishComponentSetup(instance, isSSR)
    // @file core/packages/runtime-core/src/componentPublicInstance.ts
    export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
      get({ _: instance }: ComponentRenderContext, key: string) {
        const { ctx, setupState, data, props, accessCache, type, appContext } =
          instance
        // 省略...
        // accessCache分类别缓存访问键值
        let normalizedProps
        if (key[0] !== '$') {
          // 初次访问,没有缓存值
          const n = accessCache![key]
          if (n !== undefined) {
            switch (n) {
              case AccessTypes.SETUP:
                return setupState[key]
              case AccessTypes.DATA:
                return data[key]
              case AccessTypes.CONTEXT:
                return ctx[key]
              case AccessTypes.PROPS:
                return props![key]
              // default: just fallthrough
          } else if (hasSetupBinding(setupState, key)) {
            // 访问setup返回对象的值
            accessCache![key] = AccessTypes.SETUP
            return setupState[key]
          } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
            // 访问data上值,vue3已放弃
            accessCache![key] = AccessTypes.DATA
            return data[key]
          } else if (
            // only cache other properties when instance has declared (thus stable)
            // props
            (normalizedProps = instance.propsOptions[0]) &&
            hasOwn(normalizedProps, key)
            // 访问props上的值
            accessCache![key] = AccessTypes.PROPS
            return props![key]
          } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
            // 访问ctx上的值
            // ctx由上文createDevRenderContext定义
    
    
    
    
        
    
            // 由exposePropsOnRenderContext、exposeSetupStateOnRenderContext扩展
            // 代理setup、props及publicPropertiesMap属性,
            // 这些属性会走上面逻辑,基本不会走这里
            accessCache![key] = AccessTypes.CONTEXT
            return ctx[key]
          } else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
            // 其他类型,一般用不到
            accessCache![key] = AccessTypes.OTHER
        // 访问$开头的公共属性,与createDevRenderContext一致
        const publicGetter = publicPropertiesMap[key]
        let cssModule, globalProperties
        // public $xxx properties
        if (publicGetter) {
          if (key === '$attrs') {
            // 访问$attrs依赖收集
            track(instance, TrackOpTypes.GET, key)
            __DEV__ && markAttrsAccessed()
          return publicGetter(instance)
        // 省略...
      // 省略...
    

    在执行setup前后分别进行了setCurrentInstanceunsetCurrentInstance操作,目的就是创建组件时提供一个全局的实例变量,这样在setup函数外部也能获取当前instance。这也是为什么一些与instance相关的函数:生命周期钩子函数(例如:onMountedonBeforeMountonUnmounted等)、组合函数(例如:provideinjectuseAttrsuseSlots等)必须在setup函数内执行的原因。 需要注意的是:provide(key, value)方法的key值除了字符、数字类型外还可以使用symbol类型,value值没有做响应式处理,这样做是为了避免响应式更新导致父子孙组件全部更新,从而容易造成性能问题。

    // @file core/packages/runtime-core/src/component.ts
    export function handleSetupResult(
      instance: ComponentInternalInstance,
      setupResult: unknown,
      isSSR: boolean
      if (isFunction(setupResult)) {
        // setup返回函数,直接作为render函数
        instance.render = setupResult as InternalRenderFunction
      } else if (isObject(setupResult)) {
        // 省略...
        // 代理setup执行返回对象
        // 这也是为什么在`template`中访问ref型变量,不用使用.value的原因
        instance.setupState = proxyRefs(setupResult)
        if (__DEV__) {
          // 将setup结果代理在ctx上
          exposeSetupStateOnRenderContext(instance)
      } else if (__DEV__ && setupResult !== undefined) {
        warn(
          `setup() should return an object. Received: ${
            setupResult === null ? 'null' : typeof setupResult
      finishComponentSetup(instance, isSSR)
    

    handleSetupResult函数主要是代理ctxsetup执行结果,setup返回函数用作renderfinishComponentSetup函数主要是设置当前实例的渲染函数,常规组件由SFC生成。 至此,组件装载完成,它主要执行以下任务:

  • 格式化当前组件传参,并shallowReactive代理
  • 格式化当前组件传入插槽
  • 生成当前实例代理:PublicInstanceProxyHandlers -> instance.proxy
  • 生成属性代理:props -> instance.ctx
  • 执行setup函数,执行结果生成ref代理,并代理至:setupResult -> instance.ctx
  • 实例上设置render方法
  • 接下来才是组件挂载最重要的一步:setupRenderEffect

    // @file core/packages/runtime-core/src/renderer.ts
    const setupRenderEffect: SetupRenderEffectFn = (
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    ) => {
      // 触发响应式更新回调函数
      const componentUpdateFn = () => {
        if (!instance.isMounted) {
          let vnodeHook: VNodeHook | null | undefined
          const { el, props } = initialVNode
          const { bm, m, parent } = instance
          const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
          // 禁止触发依赖更新
          toggleRecurse(instance, false)
          // 调用beforeMount钩子函数
          // 挂载前不触发依赖更新,即使有响应式数据变化
          if (bm) {
            invokeArrayFns(bm)
          // onVnodeBeforeMount
            !isAsyncWrapperVNode &&
            (vnodeHook = props && props.onVnodeBeforeMount)
            invokeVNodeHook(vnodeHook, parent, initialVNode)
          // 省略...
          toggleRecurse(instance, true)
          if (el && hydrateNode) {
            // 省略...
          } else {
            if (__DEV__) {
              startMeasure(instance, `render`)
            // 执行render函数,生成虚拟子节点
            const subTree = (instance.subTree = renderComponentRoot(instance))
            if (__DEV__) {
              endMeasure(instance, `render`)
            if (__DEV__) {
              startMeasure(instance, `patch`)
            // 挂载生成的子节点至当前instance
            patch(
              null,
              subTree,
              container,
              anchor,
              instance, // parentInstance
              parentSuspense,
              isSVG
            if (__DEV__) {
              endMeasure(instance, `patch`)
            initialVNode.el = subTree.el
          // 执行mounted挂载钩子函数
          if (m) {
            queuePostRenderEffect(m, parentSuspense)
          // onVnodeMounted
            !isAsyncWrapperVNode &&
            (vnodeHook = props && props.onVnodeMounted)
            const scopedInitialVNode = initialVNode
            queuePostRenderEffect(
              () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
              parentSuspense
          // 省略...
          instance.isMounted = true
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsComponentAdded(instance)
          // #2458: deference mount-only object parameters to prevent memleaks
          initialVNode = container = anchor = null as any
        } else {
          // updateComponent
          // This is triggered by mutation of component's own state (next: null)
          // OR parent calling processComponent (next: VNode)
          let { next, bu, u, parent, vnode } = instance
          let originNext = next
          let vnodeHook: VNodeHook | null | undefined
          if (__DEV__) {
            pushWarningContext(next || instance.vnode)
          // Disallow component effect recursion during pre-lifecycle hooks.
          toggleRecurse(instance, false)
          if (next) {
            next.el = vnode.el
            updateComponentPreRender(instance, next, optimized)
          } else {
            next = vnode
          // beforeUpdate钩子函数
          if (bu) {
            invokeArrayFns(bu)
          // onVnodeBeforeUpdate
          if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
            invokeVNodeHook(vnodeHook, parent, next, vnode)
          // 省略...
          toggleRecurse(instance, true)
          // render
          if (__DEV__) {
            startMeasure(instance, `render`)
          // 可以看出组件更新并不会再次调用setup函数
          const nextTree = renderComponentRoot(instance)
          if (__DEV__) {
            endMeasure(instance, `render`)
          // 暂存instance上的旧的vnode
          const prevTree = instance.subTree
          instance.subTree = nextTree
          if (__DEV__) {
            startMeasure(instance, `patch`)
          // 对比更新节点
          patch(
            prevTree,
            nextTree,
            // parent may have changed if it's in a teleport
            hostParentNode(prevTree.el!)!,
            // anchor may have changed if it's in a fragment
            getNextHostNode(prevTree),
            instance,
            parentSuspense,
            isSVG
          if (__DEV__) {
            endMeasure(instance, `patch`)
          next.el = nextTree.el
          if (originNext === null) {
            // self-triggered update. In case of HOC, update parent component
            // vnode el. HOC is indicated by parent instance's subTree pointing
            // to child component's vnode
            updateHOCHostEl(instance, nextTree.el)
          // updated hook
          if (u) {
            queuePostRenderEffect(u, parentSuspense)
          // onVnodeUpdated
          if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
            queuePostRenderEffect(
              () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
              parentSuspense
          // 省略...
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsComponentUpdated(instance)
          if (__DEV__) {
            popWarningContext()
      // 将`componentUpdateFn`注册成effect的
      // 函数内部有响应式数据更新就会执行该函数
      const effect = (instance.effect = new ReactiveEffect(
        componentUpdateFn,
        // 自定义任务调度,
        // 功能类似防抖,后续讨论
        () => queueJob(update),
        // 关于scope后续讨论
        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)
      if (__DEV__) {
        effect.onTrack = instance.rtc
          ? e => invokeArrayFns(instance.rtc!, e)
          : void 0
        effect.onTrigger = instance.rtg
          ? e => invokeArrayFns(instance.rtg!, e)
          : void 0
        update.ownerInstance = instance
      // 执行`componentUpdateFn`进行依赖收集和初次渲染
      update()
    

    setupRenderEffect主要通过ReactiveEffect类构造包裹componentUpdateFn函数的响应式effect(这个effect后续讨论),它的主要作用就是在componentUpdateFn函数执行时收集响应式数据,后续依赖更新时重新执行之。

    在挂载和更新组件时,都会执行renderComponentRoot方法,其主要内容就是执行组件生成的render方法生成虚拟节点。这里也可以看出组件更新只会执行template内容,并不会重新执行setup方法。

    componentUpdateFn函数主要是执行render函数,其结果作为子组件,继续执行挂载/更新操作,这里篇幅有限,就不一一列举各种类型的组件挂载。挂载流程参考如下简图:

    componentUpdateFn执行过程中触发生命周期钩子函数,根据如上流程图,可以看出组件挂载生命周期执行顺序:

      setup(父) -> beforeMount(父) -> setup(子) -> beforeMount(子) -> mounted(子) -> mounted(父)
                  beforeUpdate
    
    
    
    
        
    (父) -> beforeUpdate(子) -> updated(子) -> updated(父)
    

    最后补充一个细节,patch函数在最后执行了setRef方法,这里回到上面创建虚拟节点函数内

    // @file core/packages/runtime-core/src/vnode.ts
    const normalizeRef = ({
      ref_key,
      ref_for
    }: VNodeProps): VNodeNormalizedRefAtom | null => {
      // ref设置为非null都有效
      return (
        ref != null
          ? isString(ref) || isRef(ref) || isFunction(ref)
            ? { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for }
            : ref
          : null
      ) as any
    function createBaseVNode(
      // 省略...
      const vnode = {
        // 省略...
        ref: props && normalizeRef(props)
        // 省略...
      } as VNode
      // ...省略
      return vnode
    
    // @file core/packages/runtime-core/src/render.ts
    const patch: PatchFn = (
      // 省略...
    ) => {
      // 省略...
      // set ref
      // parentComponent不存在就是根节点,直接用app实例
      if (ref != null && parentComponent) {
        setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    // @file core/packages/runtime-core/src/rendererTemplateRef.ts
    export function setRef(
      rawRef: VNodeNormalizedRef,
      oldRawRef: VNodeNormalizedRef | null,
      parentSuspense: SuspenseBoundary | null,
      vnode: VNode,
      isUnmount = false
      if (isArray(rawRef)) {
        rawRef.forEach((r, i) =>
          setRef(
            oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
            parentSuspense,
            vnode,
            isUnmount
        return
      if (isAsyncWrapper(vnode) && !isUnmount) {
        // when mounting async components, nothing needs to be done,
        // because the template ref is forwarded to inner component
        return
      // ref被设置的值
      const refValue =
        vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
          // 组件类型返回组件实例代理
          // 即expose对象
          ? getExposeProxy(vnode.component!) || vnode.component!.proxy
          // 其他类型设置组件dom
          : vnode.el
      // 卸载时,写入null
      const value = isUnmount ? null : refValue
      const { i: owner, r: ref } = rawRef
      if (__DEV__ && !owner) {
        warn(
          `Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
            `A vnode with ref must be created inside the render function.`
        return
      const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
      const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
      const setupState = owner.setupState
      // dynamic ref changed. unset old ref
      if (oldRef != null && oldRef !== ref) {
        if (isString(oldRef)) {
          refs[oldRef] = null
          if (hasOwn(setupState, oldRef)) {
            setupState[oldRef] = null
        } else if (isRef(oldRef)) {
          oldRef.value = null
      if (isFunction(ref)) {
        // 函数类型ref,传参为value, refs
        callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
      } else {
        const _isString = isString(ref)
        const _isRef = isRef(ref)
        if (_isString || _isRef) {
          const doSet = () => {
            if (rawRef.f) {
              const existing = _isString
                ? hasOwn(setupState, ref)
                  ? setupState[ref]
                  : refs[ref]
                : ref.value
              if (isUnmount) {
                isArray(existing) && remove(existing, refValue)
              } else {
                if (!isArray(existing)) {
                  if (_isString) {
                    refs[ref] = [refValue]
                    if (hasOwn(setupState, ref)) {
                      setupState[ref] = refs[ref]
                  } else {
                    ref.value = [refValue]
                    if (rawRef.k) refs[rawRef.k] = ref.value
                } else if (!existing.includes(refValue)) {
                  existing.push(refValue)
            } else if (_isString) {
              refs[ref] = value
              if (hasOwn(setupState, ref)) {
                setupState[ref] = value
            } else if (_isRef) {
              ref.value = value
              if (rawRef.k) refs[rawRef.k] = value
            } else if (__DEV__) {
              warn('Invalid template ref type:', ref, `(${typeof ref})`)
          if (value) {
            // #1789: for non-null values, set them after render
            // null values means this is unmount and it should not overwrite another
            // ref with the same key
            ;(doSet as SchedulerJob).id = -1
            // 刷新队列后续讨论
            queuePostRenderEffect(doSet, parentSuspense)
          } else {
            doSet()
        } else if (__DEV__) {
          warn('Invalid template ref type:', ref, `(${typeof ref})`)
    

    组件或dom上设置ref,挂载后会将当前实例或dom写入至ref对应的变量,写入规则:

  • 可传入的值:string、ref、function(注意:reactive对象、数值、symbol等类型无效)
  • ref传入string,写入为setupState(及setup返回对象)上对应的值,不存在就写入实例refs上。ref配置在v-for循环上时,写入的值为v-for循环形成的数组。
  • ref传入ref时,直接写入ref.value,v-for循环上时,写入的值为v-for循环形成的数组
  • ref传入函数时,依次传入两个参数:value, refsv-for时会循环执行传入函数。注意:如果需要暴露在$refs上,需要手动设置refs
  • 设置ref后,值会被同步至refs上,对外暴露在$refs
  • 这里总结一下app及组件挂载过程中的一些细节:

  • app挂载返回的是getExposeProxy方法返回的代理,组件挂载后可以通过设置ref获取实例(本质也是getExposeProxy方法返回的代理)。在setup或生命周期内调用vue函数getCurrentInstance获取未代理的内部实例(不推荐)。
  • template内可访问的变量:$props$attrs$slots$refs都是响应式变量;setup函数返回对象、定义的propskey值可作为变量直接访问。$refs是设置了ref的组件实例或dom;其他属性(例如:el、optionsoptions、parent等)不建议直接使用。
  • 生命周期钩子函数(例如:onMounted、onBeforeMount、onUnmounted等)、组合函数(例如:provide、inject、useAttrs、 useSlots等)必须在setup函数内执行
  • 组件更新只会执行template内容,并不会重新执行setup方法
  • 分类:
    前端
  •