揭开Vue3.0 setup函数的神秘面纱

2,126 阅读7分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

Vue 3.0的使用中我们可以不使用datapropsmethodscomputedOption函数,可以只下在setup函数中进行编写代码逻辑。当然为了和Vue 2.0兼容,也可以继续使用Option函数。

先提出两个问题:

  1. setup函数的执行时机是什么?
  2. setup函数的返回结果为何与模板的渲染建立联系的?

mountComponent 挂载组件

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 1.
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

  // 2. 
  setupComponent(instance)

  // 3. 
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )

}

挂载组件分为三个步骤:

  1. 创建组件实例
  2. 设置组件实例
  3. 创建带副作用的渲染函数

我们本文分析的主角setup函数是在第二个阶段进行处理的,但是鉴于这三个步骤具有关联性,我们也会将第一个和第三个步骤进行和第二个步骤的关联进行分析。

createComponentInstance创建组件实例

  • 我们先来看看组件实例接口ComponentInternalInstance定义的一些属性的含义:
export interface ComponentInternalInstance {
  // 组件的id
  uid: number
  // 组件类型(options对象 或者 函数)
  type: ConcreteComponent
  // 父组件实例
  parent: ComponentInternalInstance | null
  // 根组件实例
  root: ComponentInternalInstance
  // app上下文
  appContext: AppContext
  // 组件VNode
  vnode: VNode
  // 要更新到的VNode
  next: VNode | null
  // 子树VNode
  subTree: VNode
  // 带副作用的渲染函数
  update: SchedulerJob
  // 渲染函数
  render: InternalRenderFunction | null
  // SSR渲染函数
  ssrRender?: Function | null
  // 依赖注入的数据
  provides: Data
  // 收集响应式依赖的作用域 
  scope: EffectScope
  // 读取proxy属性值后的缓存
  accessCache: Data | null
  // 渲染缓存
  renderCache: (Function | VNode)[]
  // 注册的组件
  components: Record<string, ConcreteComponent> | null
  // 注册的指令
  directives: Record<string, Directive> | null
  // 过滤器
  filters?: Record<string, Function>
  // props 
  propsOptions: NormalizedPropsOptions
  // emits
  emitsOptions: ObjectEmitsOptions | null
  // attrs
  inheritAttrs?: boolean
  // 是否是自定义组件(custom element)
  isCE?: boolean
  // 自定义组件(custom element)相关方法 
  ceReload?: () => void

  // the rest are only for stateful components ---------------------------------
  
  // 渲染上下文代理对象,当使用`this`时就是指的这个对象
  proxy: ComponentPublicInstance | null

  // 组件暴露的对象
  exposed: Record<string, any> | null
  // 组件暴露对象的代理对象
  exposeProxy: Record<string, any> | null

  // 带有with区块(block)的渲染上下文代理对象
  withProxy: ComponentPublicInstance | null
  // 渲染上下文---即组件对象的信息 { _: instance }
  ctx: Data

  // data数据
  data: Data
  // props数据
  props: Data
  // attrs数据
  attrs: Data
  // slot数据
  slots: InternalSlots
  // 组件或者DOM的ref引用
  refs: Data
  // emit函数
  emit: EmitFn
  // 记录被v-once修饰已经触发的事件
  emitted: Record<string, boolean> | null
  // 工厂函数生成的默认props数据
  propsDefaults: Data
  // setup函数返回的响应式结果
  setupState: Data
  // setup函数上下文数据
  setupContext: SetupContext | null
  
  // 异步组件 
  suspense: SuspenseBoundary | null
  // 异步组件ID
  suspenseId: number
  // setup函数返回的异步函数结果
  asyncDep: Promise<any> | null
  // 异步函数调用已完成
  asyncResolved: boolean

  // 是否已挂载
  isMounted: boolean
  // 是否已卸载
  isUnmounted: boolean
  // 是否已去激活
  isDeactivated: boolean
  
  // 各种钩子函数
  // bc
  [LifecycleHooks.BEFORE_CREATE]: LifecycleHook
  // c
  [LifecycleHooks.CREATED]: LifecycleHook
  // bm
  [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
  // m
  [LifecycleHooks.MOUNTED]: LifecycleHook
  // bu
  [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
  // u
  [LifecycleHooks.UPDATED]: LifecycleHook
  // bum
  [LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
  // um
  [LifecycleHooks.UNMOUNTED]: LifecycleHook
  // rtc
  [LifecycleHooks.RENDER_TRACKED]: LifecycleHook
  // rtg
  [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
  // a
  [LifecycleHooks.ACTIVATED]: LifecycleHook
  // da
  [LifecycleHooks.DEACTIVATED]: LifecycleHook
  // ec
  [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
  // sp
  [LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
}
  • 我们接下来看看创建组件实例时主要设置了哪些属性值:
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    appContext,
    
    provides: parent ? parent.provides : Object.create(appContext.provides),

    propsOptions: normalizePropsOptions(type, appContext),
    emitsOptions: normalizeEmitsOptions(type, appContext),

    inheritAttrs: type.inheritAttrs,
    // 省略...
  }
  
  instance.ctx = { _: instance }
  instance.root = parent ? parent.root : instance

  return instance
}

我们看到创建组件实例的时候主要设置了uidvnodeappContextprovidespropsOptionsemitsOptionsctx等这些属性。

setupComponent设置组件实例流程

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  // 1.
  const { props, children } = instance.vnode
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  // 2.
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  return setupResult
}

该方法主要有两个步骤:

  1. vnode中获得到一些属性,然后初始化propsslots(后续章节介绍);
  2. 调用setupStatefulComponent方法设置有状态组件实例(本文分析的主要内容)。
setupStatefulComponent方法
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  // 1. create render proxy property access cache
  instance.accessCache = Object.create(null)
  
  // 2. create public instance / render proxy
  // also mark it raw so it's never observed
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

  
  const { setup } = Component
  if (setup) {
    // 3. call setup()
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)
    
    // 4.
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [instance.props, setupContext]
    )
    
    // 5.
    handleSetupResult(instance, setupResult, isSSR)
  } else {
    // 6.
    finishComponentSetup(instance, isSSR)
  }
}
  1. 首先初始化了一个accessCache对象,用来缓存查找ctx后得到的值,避免重复查找ctx中的属性。 后面的每步我们分开来说明。

  2. 建立了一个ctx的代理对象proxy, 当访问或者修改proxy的属性时会触发PublicInstanceProxyHandlers方法,而此方法的操作对象是ctx

这里先提出一个问题:为什么设置代理?

我们来看看PublicInstanceProxyHandlers方法:

export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const { ctx, setupState, data, props, accessCache, type, appContext } =
      instance

    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]
        }
      } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache![key] = AccessTypes.SETUP
        return setupState[key]
      } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache![key] = AccessTypes.DATA
        return data[key]
      } else if (
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)
      ) {
        accessCache![key] = AccessTypes.PROPS
        return props![key]
      } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache![key] = AccessTypes.CONTEXT
        return ctx[key]
      } else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
        accessCache![key] = AccessTypes.OTHER
      }
    }

    const publicGetter = publicPropertiesMap[key]
    let cssModule, globalProperties
    // public $xxx properties
    if (publicGetter) {
      if (key === '$attrs') {
        track(instance, TrackOpTypes.GET, key)
        __DEV__ && markAttrsAccessed()
      }
      return publicGetter(instance)
    } else if (
      // css module (injected by vue-loader)
      (cssModule = type.__cssModules) &&
      (cssModule = cssModule[key])
    ) {
      return cssModule
    } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
      // user may set custom properties to `this` that start with `$`
      accessCache![key] = AccessTypes.CONTEXT
      return ctx[key]
    } else if (
      // global properties
      ((globalProperties = appContext.config.globalProperties),
      hasOwn(globalProperties, key))
    ) {
      if (__COMPAT__) {
        const desc = Object.getOwnPropertyDescriptor(globalProperties, key)!
        if (desc.get) {
          return desc.get.call(instance.proxy)
        } else {
          const val = globalProperties[key]
          return isFunction(val) ? val.bind(instance.proxy) : val
        }
      } else {
        return globalProperties[key]
      }
    }
  },

  set(
    { _: instance }: ComponentRenderContext,
    key: string,
    value: any
  ): boolean {
    const { data, setupState, ctx } = instance
    if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
      setupState[key] = value
    } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
      data[key] = value
    } else if (hasOwn(instance.props, key)) {
      return false
    }
    if (key[0] === '$' && key.slice(1) in instance) {
      return false
    } else {
      if (__DEV__ && key in instance.appContext.config.globalProperties) {
        Object.defineProperty(ctx, key, {
          enumerable: true,
          configurable: true,
          value
        })
      } else {
        ctx[key] = value
      }
    }
    return true
  },

  has(
    {
      _: { data, setupState, accessCache, ctx, appContext, propsOptions }
    }: ComponentRenderContext,
    key: string
  ) {
    let normalizedProps
    return (
      accessCache![key] !== undefined ||
      (data !== EMPTY_OBJ && hasOwn(data, key)) ||
      (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
      ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
      hasOwn(ctx, key) ||
      hasOwn(publicPropertiesMap, key) ||
      hasOwn(appContext.config.globalProperties, key)
    )
  }
}

我们分别对三个方法进行讲解:

  • get 获取方法

    • 如果key不以$开头,直接从accessCache获取,获取到就直接返回,如果获取不到则依次从setupState,data,ctx,props中获取,并且设置accessCache

    • 如果key$开头,譬如,, el, data,data, props, attrs,attrs, slots, refs,refs, parent, root,root, emit, options,options, forceUpdate, nextTick,nextTick, watch等则通过对应的方法进行获取,如果不是上述key值,则依次从ctxappContext.config.globalProperties,最后如果找不到就获取失败。

  • set 获取方法

    • 只允许对setupStatedata的key进行赋值,且优先给setupState赋值,如果前两者都没有对应的key直接赋值在ctx上。

  • has判断是否有值的方法

    • 判断accessCache,data,setupState, propsOptions,ctx,publicPropertiesMapappContext.config.globalProperties 有没有对应的key。

前面问题的答案:方便用户的使用,只需要访问instance.proxy就能访问和修改data,setupState,propsappContext.config.globalProperties等属性中的值。

  1. 如果setup参数大于1,则创建setupContext
export function createSetupContext(
  instance: ComponentInternalInstance
): SetupContext {
  const expose: SetupContext['expose'] = exposed => {
    instance.exposed = exposed || {}
  }

  let attrs: Data
  return {
    get attrs() {
      return attrs || (attrs = createAttrsProxy(instance))
    },
    slots: instance.slots,
    emit: instance.emit,
    expose
  }
}

setupContextsetup函数的第二个参数,从方法来看我们就知道了setupContext包括attrs,slots,emitexpose,这就解释了为什么我们能在setup函数中拿到对应的这些值了。第四个参数可能比较陌生,表示的是组件需要对外暴露的值。

  1. 执行setup函数,第一个参数是props,第二个参数是setupConstext。(用callWithErrorHandling封装了一层,可以捕获执行错误)
const setupResult = callWithErrorHandling(
    setup,
     nstance,
    ErrorCodes.SETUP_FUNCTION,
    [instance.props, setupContext]
)

export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}
  1. 处理setup函数的返回结果;
export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
      instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    instance.setupState = proxyRefs(setupResult)
  }
  finishComponentSetup(instance, isSSR)
}
  • 如果返回结果是函数,则将其作为渲染函数render,这个函数就是用来生成subTreeVNode的函数。

  • 如果返回结果是对象,则变成响应式然后赋值给setupState属性,这里就解释了ctx的代理对象proxy中的setupState是如何得到的。

  1. 完成组件实例的设置
export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
  const Component = instance.type as ComponentOptions

  // template / render function normalization
   if (!instance.render) {
    // could be set from setup()
    if (compile && !Component.render) {
      const template =
        (__COMPAT__ &&
          instance.vnode.props &&
          instance.vnode.props['inline-template']) ||
        Component.template
      if (template) {
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const { delimiters, compilerOptions: componentCompilerOptions } =
          Component
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        Component.render = compile(template, finalCompilerOptions)
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    if (installWithProxy) {
      installWithProxy(instance)
    }
  }

  // support for 2.x options
  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    setCurrentInstance(instance)
    pauseTracking()
    applyOptions(instance)
    resetTracking()
    unsetCurrentInstance()
  }

}
  • 标准化模板和render函数,将render函数赋值给instance.render属性。
  • 兼容2.0 Options API, 3.0 兼容 2.0 就是在这里实现的。

这里解释下render函数:

我们常见的使用方式是使用SFC (Single File Components)去编写组件,我们知道浏览器是无法识别Vue文件的,在编译阶段使用Vue loader将Vue文件的代码转换成JS对象,其中会将template模板转换成render函数。所以我们几乎不太会自己去实现render函数。当然前面也提到了,可以在setup函数中返回函数结果作为render函数。

renderComponentRoot生成subTreeVNode

export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  const {
    type: Component,
    vnode,
    proxy,
    withProxy,
    props,
    propsOptions: [propsOptions],
    slots,
    attrs,
    emit,
    render,
    renderCache,
    data,
    setupState,
    ctx,
    inheritAttrs
  } = instance

  let result = normalizeVNode(
    render!.call(
      proxyToUse,
      proxyToUse!,
      renderCache,
      props,
      setupState,
      data,
      ctx
    )
  )
  return result
}
  • 在副作用渲染函数中的renderComponentRoot就是用 render生成subTreeVNode,然后继续递归patch进行挂载和更新。