执行 setup 时,组件实例真的如官方所说未创建?

557 阅读3分钟

Vue.js 官网 结论的一处的错误

一、前言

为什么说是对Vue.js官网一处结论描述错误的探究呢?主要是在学习 Composition API 的 setup()函数发现官网有一段这样的模式

Whensetup is executed, the component instance has not been created yet. As a result, you will only be able to access the following properties:

其含义就是:执行 setup 时,组件实例尚未被创建。因此,你只能访问以下 property,下图就是官方对此的描述,真实情况如其所说吗?

截屏2021-06-18 下午4.30.39.png

二、验证这个错误

正所谓了解真相,方能自由。探索这个问题的对错必须依赖 Vue3源码

  • 下载 vue-next 源码

在整个源码中核心文件: runtime-core runtime-dom compiler-core runtime-dom 几个文件

  • 进入到 runtime-core 文件夹
  • 打开 renderer.ts文件

我们可以从544行中的这段代码开始一步步分析

2.1 处理组件节点,调用 processComponent
} else if (shapeFlag & ShapeFlags.COMPONENT) {          
  // 处理组件节点
  processComponent(
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized
  )
}

这里我们来看看 processComponent 的实现中很明显 已经创建了 instance 这么个实例, 接着在1335行中开始调用 setupComponent(instance) 将 上面创建的 instance 传递了出去

2.2 组件挂载,调用 mountComponent
  // 组件挂载
const mountComponent: MountComponentFn = (
  initialVNode,
  container, 
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 2.x compat may pre-creaate the component instance before actually
  // mounting
  const compatMountInstance = __COMPAT__ && initialVNode.component
  // 在这里创建了 ComponentInternalInstance 
  const instance: ComponentInternalInstance =
  compatMountInstance ||
    (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ))
2.2 开始处理 setupComponent

该函数在 component.ts中进行了处理,那么它又是如何处理呢?

) => {
 // 2.x compat may pre-creaate the component instance before actually
 // mounting
 const compatMountInstance = __COMPAT__ && initialVNode.component
 const instance: ComponentInternalInstance =
   compatMountInstance ||(initialVNode.component = createComponentInstance(
   initialVNode,
   parentComponent,
   parentSuspense
    ))
  if (__DEV__ && instance.type.__hmrId) {
    registerHMR(instance)
  }
  if (__DEV__) {
    pushWarningContext(initialVNode)
    startMeasure(instance, `mount`)
  }

  // inject renderer internals for keepAlive
  if (isKeepAlive(initialVNode)) {
    ;(instance.ctx as KeepAliveContext).renderer = internals
  }

  // resolve props and slots for setup context
   if (!(__COMPAT__ && compatMountInstance)) {
   if (__DEV__) {
     startMeasure(instance, `init`)
   }
   // 开始调用 setup,将 instance 实例传递进入
   setupComponent(instance)
    if (__DEV__) {
      endMeasure(instance, `init`)
   }
}

调转到它的实现文件中,我们可以看到这样一段代码实现,在这个函数中处理几件事情

  • 调用 setupStatefulComponent 初始化有状态的组件
  • 初始化 props slots
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
}
2.3 提取 setup函数

在这里通过 const { setup } = Component拿到 该函数,看到这段代码也就能得出一个结论

  • 如果实现了 setup函数,那么通过vue2 options api 是不会生效的
  • vue3是兼容 vue2 options api
// 2. call setup()
// 在这里开始处理 setup()函数
const { setup } = Component
if (setup) {
  const setupContext = (instance.setupContext =
  setup.length > 1 ? createSetupContext(instance) : null)

  currentInstance = instance
  pauseTracking()
  const setupResult = callWithErrorHandling(
    setup,
    instance,
    ErrorCodes.SETUP_FUNCTION,
    [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
  )
  resetTracking()
  currentInstance = null

  if (isPromise(setupResult)) {
    if (isSSR) {
      // return the promise so server-renderer can wait on it
    return setupResult
    .then((resolvedResult: unknown) => {
       handleSetupResult(instance, resolvedResult, isSSR)
      })
     .catch(e => {
       handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
      })
   } else if (__FEATURE_SUSPENSE__) {
      // async setup returned Promise.
      // bail here and wait for re-entry.
      instance.asyncDep = setupResult
   } else if (__DEV__) {
      warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
      )
    }
  } else {
     handleSetupResult(instance, setupResult, isSSR)
  }
} else {
    finishComponentSetup(instance, isSSR) 
}
2.4 统一执行 callWithErrorHandling 函数
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
}

这个函数其实本质就是自己封装了一个 try catch ,在其内部来来调 fn

三、结论

  • 结合源码一分析,就很确定 执行 setup 时,组件实例 已经创建,官方描述是有问题的~