揭開Vue3.0 setup函數(shù)的神秘面紗

Vue 3.0的使用中我們可以不使用dataprops火窒、methods硼补、computedOption函數(shù),可以只下在setup函數(shù)中進行編寫代碼邏輯熏矿。當然為了和Vue 2.0兼容已骇,也可以繼續(xù)使用Option函數(shù)。

先提出兩個個問題:

  1. setup函數(shù)的執(zhí)行時機是什么票编?
  2. setup函數(shù)的返回結(jié)果為何與模板的渲染建立聯(lián)系的褪储?

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. 創(chuàng)建組件實例
  2. 設(shè)置組件實例
  3. 創(chuàng)建帶副作用的渲染函數(shù)

我們本文分析的主角setup函數(shù)是在第二個階段進行處理的,但是鑒于這三個步驟具有關(guān)聯(lián)性慧域,我們也會將第一個和第三個步驟進行和第二個步驟的關(guān)聯(lián)進行分析鲤竹。

createComponentInstance創(chuàng)建組件實例

  • 我們先來看看組件實例接口ComponentInternalInstance定義的一些屬性的含義:
export interface ComponentInternalInstance {
  // 組件的id
  uid: number
  // 組件類型(options對象 或者 函數(shù))
  type: ConcreteComponent
  // 父組件實例
  parent: ComponentInternalInstance | null
  // 根組件實例
  root: ComponentInternalInstance
  // app上下文
  appContext: AppContext
  // 組件VNode
  vnode: VNode
  // 要更新到的VNode
  next: VNode | null
  // 子樹VNode
  subTree: VNode
  // 帶副作用的渲染函數(shù)
  update: SchedulerJob
  // 渲染函數(shù)
  render: InternalRenderFunction | null
  // SSR渲染函數(shù)
  ssrRender?: Function | null
  // 依賴注入的數(shù)據(jù)
  provides: Data
  // 收集響應(yīng)式依賴的作用域 
  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)相關(guān)方法 
  ceReload?: () => void

  // the rest are only for stateful components ---------------------------------
  
  // 渲染上下文代理對象,當使用`this`時就是指的這個對象
  proxy: ComponentPublicInstance | null

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

  // 帶有with區(qū)塊(block)的渲染上下文代理對象
  withProxy: ComponentPublicInstance | null
  // 渲染上下文---即組件對象的信息 { _: instance }
  ctx: Data

  // data數(shù)據(jù)
  data: Data
  // props數(shù)據(jù)
  props: Data
  // attrs數(shù)據(jù)
  attrs: Data
  // slot數(shù)據(jù)
  slots: InternalSlots
  // 組件或者DOM的ref引用
  refs: Data
  // emit函數(shù)
  emit: EmitFn
  // 記錄被v-once修飾已經(jīng)觸發(fā)的事件
  emitted: Record<string, boolean> | null
  // 工廠函數(shù)生成的默認props數(shù)據(jù)
  propsDefaults: Data
  // setup函數(shù)返回的響應(yīng)式結(jié)果
  setupState: Data
  // setup函數(shù)上下文數(shù)據(jù)
  setupContext: SetupContext | null
  
  // 異步組件 
  suspense: SuspenseBoundary | null
  // 異步組件ID
  suspenseId: number
  // setup函數(shù)返回的異步函數(shù)結(jié)果
  asyncDep: Promise<any> | null
  // 異步函數(shù)調(diào)用已完成
  asyncResolved: boolean

  // 是否已掛載
  isMounted: boolean
  // 是否已卸載
  isUnmounted: boolean
  // 是否已去激活
  isDeactivated: boolean
  
  // 各種鉤子函數(shù)
  // 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>>
}
  • 我們接下來看看創(chuàng)建組件實例時主要設(shè)置了哪些屬性值:
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
}

我們看到創(chuàng)建組件實例的時候主要設(shè)置了uidvnode昔榴、appContext辛藻、providespropsOptions互订、emitsOptionsctx等這些屬性吱肌。

setupComponent設(shè)置組件實例流程

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(后續(xù)章節(jié)介紹)仰禽;
  2. 調(diào)用setupStatefulComponent方法設(shè)置有狀態(tài)組件實例(本文分析的主要內(nèi)容)氮墨。
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的屬性時會觸發(fā)PublicInstanceProxyHandlers方法温峭,而此方法的操作對象是ctx猛铅。

這里先提出一個問題:為什么設(shè)置代理?

我們來看看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中獲取祥款,并且設(shè)置accessCache

    • 如果key開頭月杉,譬如, el,data, props,attrs, slots,refs, parent,root, emit,options, forceUpdate,nextTick, $watch等則通過對應(yīng)的方法進行獲取刃跛,如果不是上述key值,則依次從ctxappContext.config.globalProperties苛萎,最后如果找不到就獲取失敗桨昙。

  • set 獲取方法

    • 只允許對setupStatedata的key進行賦值,且優(yōu)先給setupState賦值腌歉,如果前兩者都沒有對應(yīng)的key直接賦值在ctx上蛙酪。
  • has判斷是否有值的方法

    • 判斷accessCache,data,setupState, propsOptions,ctx,publicPropertiesMapappContext.config.globalProperties 有沒有對應(yīng)的key。

前面問題的答案:方便用戶的使用翘盖,只需要訪問instance.proxy就能訪問和修改data,setupState,props桂塞,appContext.config.globalProperties等屬性中的值。

  1. 如果setup參數(shù)大于1馍驯,則創(chuàng)建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函數(shù)的第二個參數(shù),從方法來看我們就知道了setupContext包括attrs,slots,emitexpose汰瘫,這就解釋了為什么我們能在setup函數(shù)中拿到對應(yīng)的這些值了狂打。第四個參數(shù)可能比較陌生,表示的是組件需要對外暴露的值混弥。

  1. 執(zhí)行setup函數(shù)趴乡,第一個參數(shù)是props,第二個參數(shù)是setupConstext蝗拿。(用callWithErrorHandling封裝了一層晾捏,可以捕獲執(zhí)行錯誤)
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函數(shù)的返回結(jié)果;
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)
}
  • 如果返回結(jié)果是函數(shù)蛹磺,則將其作為渲染函數(shù)render粟瞬,這個函數(shù)就是用來生成subTreeVNode的函數(shù)。

  • 如果返回結(jié)果是對象萤捆,則變成響應(yīng)式然后賦值給setupState屬性,這里就解釋了ctx的代理對象proxy中的setupState是如何得到的俗批。

  1. 完成組件實例的設(shè)置
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函數(shù)俗或,將render函數(shù)賦值給instance.render屬性。
  • 兼容2.0 Options API, 3.0 兼容 2.0 就是在這里實現(xiàn)的岁忘。

這里解釋下render函數(shù):

我們常見的使用方式是使用SFC (Single File Components)去編寫組件辛慰,我們知道瀏覽器是無法識別Vue文件的,在編譯階段使用Vue loader將Vue文件的代碼轉(zhuǎn)換成JS對象干像,其中會將template模板轉(zhuǎn)換成render函數(shù)帅腌。所以我們幾乎不太會自己去實現(xiàn)render函數(shù)驰弄。當然前面也提到了,可以在setup函數(shù)中返回函數(shù)結(jié)果作為render函數(shù)速客。

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
}
  • 在副作用渲染函數(shù)中的renderComponentRoot就是用render生成subTreeVNode戚篙,然后繼續(xù)遞歸patch進行掛載和更新。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載溺职,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者岔擂。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浪耘,隨后出現(xiàn)的幾起案子乱灵,更是在濱河造成了極大的恐慌,老刑警劉巖七冲,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痛倚,死亡現(xiàn)場離奇詭異,居然都是意外死亡澜躺,警方通過查閱死者的電腦和手機状原,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苗踪,“玉大人颠区,你說我怎么就攤上這事⊥ú” “怎么了毕莱?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長颅夺。 經(jīng)常有香客問我朋截,道長,這世上最難降的妖魔是什么吧黄? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任部服,我火速辦了婚禮,結(jié)果婚禮上拗慨,老公的妹妹穿的比我還像新娘廓八。我一直安慰自己,他們只是感情好赵抢,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布剧蹂。 她就那樣靜靜地躺著,像睡著了一般烦却。 火紅的嫁衣襯著肌膚如雪宠叼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天其爵,我揣著相機與錄音冒冬,去河邊找鬼伸蚯。 笑死,一個胖子當著我的面吹牛简烤,可吹牛的內(nèi)容都是我干的剂邮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼乐埠,長吁一口氣:“原來是場噩夢啊……” “哼抗斤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丈咐,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤瑞眼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后棵逊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伤疙,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年辆影,在試婚紗的時候發(fā)現(xiàn)自己被綠了徒像。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡蛙讥,死狀恐怖锯蛀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情次慢,我是刑警寧澤旁涤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站迫像,受9級特大地震影響劈愚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜闻妓,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一菌羽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧由缆,春花似錦注祖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浸卦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間案糙,已是汗流浹背限嫌。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工靴庆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怒医。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓炉抒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親稚叹。 傳聞我的和親對象是個殘疾皇子焰薄,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容