likes
comments
collection
share

vue3源码与应用-mount

作者站长头像
站长
· 阅读数 14

vue3源码与应用-mount

前言

  1. vue版本:3.2.47
  2. 该系列文章为个人学习总结,如有错误,欢迎指正;尚有不足,请多指教!
  3. 阅读源码暂未涉及ssr服务端渲染,直接跳过
  4. 部分调试代码(例如:console.warn等),不涉及主要内容,直接跳过
  5. 涉及兼容vue2的代码直接跳过(例如:__FEATURE_OPTIONS_API__等)
  6. 注意源码里的__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的挂载主要分为以下四步:

  1. 构建当前app的虚拟节点,并绑定app上下文至其appContext
  2. 注册热更新后重新加载根节点(同3)
  3. 使用渲染器渲染生成的虚拟节点
  4. 生成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
    : 0

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

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

  1. 如果传入的type本身就是已经处理过的虚拟节点(例如自带的组件:<component is="com"></component>),则直接返回其拷贝值。
  2. 根据传入的属性props,预处理一下classstyle
  3. 使用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 = (
  n1,
  n2,
  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(
          n1,
          n2,
          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(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      mountComponent(
        n2,
        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
  )
}

这里组件挂载函数经过简化后,依次执行了如下代码:

  1. 创建组件实例:createComponentInstance
  2. 装载组件: setupComponent
  3. 关联响应式更新: 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,
      args
    )
  }

  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,
      args
    )
  }
}

以上代码初始化了一些组件实例属性,预处理的组件定义的参数格式等。另外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的深层属性的值变化,可以来自任何地方。

// @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生成。 至此,组件装载完成,它主要执行以下任务:

  1. 格式化当前组件传参,并shallowReactive代理
  2. 格式化当前组件传入插槽
  3. 生成当前实例代理:PublicInstanceProxyHandlers -> instance.proxy
  4. 生成属性代理:props -> instance.ctx
  5. 执行setup函数,执行结果生成ref代理,并代理至:setupResult -> instance.ctx
  6. 实例上设置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
      if (
        !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
      if (
        !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函数执行时收集响应式数据,后续依赖更新时重新执行之。 componentUpdateFn函数主要是执行render函数,其结果作为子组件,继续执行挂载/更新操作,这里篇幅有限,就不一一列举各种类型的组件挂载。挂载流程参考如下简图:

vue3源码与应用-mount

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

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

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

// @file core/packages/runtime-core/src/vnode.ts

const normalizeRef = ({
  ref,
  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(
        r,
        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对应的变量,写入规则:

  1. 可传入的值:string、ref、function(注意:reactive对象、数值、symbol等类型无效)
  2. ref传入string,写入为setupState(及setup返回对象)上对应的值,不存在就写入实例refs上。ref配置在v-for循环上时,写入的值为v-for循环形成的数组。
  3. ref传入ref时,直接写入ref.value,v-for循环上时,写入的值为v-for循环形成的数组
  4. ref传入函数式,依次传入两个参数:value, refsv-for时会循环执行传入函数