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

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, props,attrs, slots,refs, parent,root, emit,options, forceUpdate,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进行挂载和更新。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容