Vue.js 3.0 允许我们在编写组件的时候添加一个 setup 启动函数,它是 Composition API 逻辑组织的入口,本节课我们就来分析一下这个函数。

我们先通过一段代码认识它,在这里编写一个 button 组件:

  1. <template>
  2. <button @click="increment">
  3. Count is: {{ state.count }}, double is: {{ state.double }}
  4. </button>
  5. </template>
  6. <script>
  7. import { reactive, computed } from 'vue'
  8. export default {
  9. setup() {
  10. const state = reactive({
  11. count: 0,
  12. double: computed(() => state.count * 2)
  13. })
  14. function increment() {
  15. state.count++
  16. }
  17. return {
  18. state,
  19. increment
  20. }
  21. }
  22. }
  23. </script>

可以看到,这段代码和 Vue.js 2.x 组件的写法相比,多了一个 setup 启动函数,另外组件中也没有定义 props、data、computed 这些 options。

在 setup 函数内部,定义了一个响应式对象 state,它是通过 reactive API 创建的。state 对象有 count 和 double 两个属性,其中 count 对应一个数字属性的值;而 double 通过 computed API 创建,对应一个计算属性的值。reactive API 和 computed API 不是我们关注的重点,在后续响应式章节我会详细介绍。

这里需要注意的是,模板中引用到的变量 state 和 increment 包含在 setup 函数的返回对象中,那么它们是如何建立联系的呢?

我们先来回想一下 Vue.js 2.x 编写组件的时候,会在 props、data、methods、computed 等 options 中定义一些变量。在组件初始化阶段,Vue.js 内部会处理这些 options,即把定义的变量添加到了组件实例上。等模板编译成 render 函数的时候,内部通过 with(this){} 的语法去访问在组件实例中的变量。

那么到了 Vue.js 3.0,既支持组件定义 setup 函数,而且在模板 render 的时候,又可以访问到 setup 函数返回的值,这是如何实现的?我们来一探究竟。

创建和设置组件实例

首先,我们来回顾一下组件的渲染流程:创建 vnode 、渲染 vnode 和生成 DOM。

04 | Setup:组件渲染前的初始化过程是怎样的? - 图1

其中渲染 vnode 的过程主要就是在挂载组件:

  1. const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
  3. setupComponent(instance)
  4. setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
  5. }

可以看到,这段挂载组件的代码主要做了三件事情:创建组件实例、设置组件实例和设置并运行带副作用的渲染函数。前两个流程就跟我们今天提到的问题息息相关,所以这一节课我们将重点分析它们。

先看创建组件实例的流程,我们要关注 createComponentInstance 方法的实现:

  1. function createComponentInstance (vnode, parent, suspense) {
  2. const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
  3. const instance = {
  4. uid: uid++,
  5. vnode,
  6. parent,
  7. appContext,
  8. type: vnode.type,
  9. root: null,
  10. next: null,
  11. subTree: null,
  12. update: null,
  13. render: null,
  14. proxy: null,
  15. withProxy: null,
  16. effects: null,
  17. provides: parent ? parent.provides : Object.create(appContext.provides),
  18. accessCache: null,
  19. renderCache: [],
  20. ctx: EMPTY_OBJ,
  21. data: EMPTY_OBJ,
  22. props: EMPTY_OBJ,
  23. attrs: EMPTY_OBJ,
  24. slots: EMPTY_OBJ,
  25. refs: EMPTY_OBJ,
  26. setupState: EMPTY_OBJ,
  27. setupContext: null,
  28. components: Object.create(appContext.components),
  29. directives: Object.create(appContext.directives),
  30. suspense,
  31. asyncDep: null,
  32. asyncResolved: false,
  33. isMounted: false,
  34. isUnmounted: false,
  35. isDeactivated: false,
  36. bc: null,
  37. c: null,
  38. bm: null,
  39. m: null,
  40. bu: null,
  41. u: null,
  42. um: null,
  43. bum: null,
  44. da: null,
  45. a: null,
  46. rtg: null,
  47. rtc: null,
  48. ec: null,
  49. emit: null
  50. }
  51. instance.ctx = { _: instance }
  52. instance.root = parent ? parent.root : instance
  53. instance.emit = emit.bind(null, instance)
  54. return instance
  55. }

从上述代码中可以看到,组件实例 instance 上定义了很多属性,你千万不要被这茫茫多的属性吓到,因为其中一些属性是为了实现某个场景或者某个功能所定义的,你只需要通过我在代码中的注释大概知道它们是做什么的即可。

Vue.js 2.x 使用 new Vue 来初始化一个组件的实例,到了 Vue.js 3.0,我们直接通过创建对象去创建组件的实例。这两种方式并无本质的区别,都是引用一个对象,在整个组件的生命周期中去维护组件的状态数据和上下文环境。

创建好 instance 实例后,接下来就是设置它的一些属性。目前已完成了组件的上下文、根组件指针以及派发事件方法的设置。我们在后面会继续分析更多 instance 实例属性的设置逻辑。

接着是组件实例的设置流程,对 setup 函数的处理就在这里完成,我们来看一下 setupComponent 方法的实现:

  1. function setupComponent (instance, isSSR = false) {
  2. const { props, children, shapeFlag } = instance.vnode
  3. const isStateful = shapeFlag & 4
  4. initProps(instance, props, isStateful, isSSR)
  5. initSlots(instance, children)
  6. const setupResult = isStateful
  7. ? setupStatefulComponent(instance, isSSR)
  8. : undefined
  9. return setupResult
  10. }

可以看到,我们从组件 vnode 中获取了 props、children、shapeFlag 等属性,然后分别对 props 和插槽进行初始化,这两部分逻辑在后续的章节再详细分析。根据 shapeFlag 的值,我们可以判断这是不是一个有状态组件,如果是则要进一步去设置有状态组件的实例。

接下来我们要关注到 setupStatefulComponent 函数,它主要做了三件事:创建渲染上下文代理、判断处理 setup 函数和完成组件实例设置。它代码如下所示:

  1. function setupStatefulComponent (instance, isSSR) {
  2. const Component = instance.type
  3. instance.accessCache = {}
  4. instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  5. const { setup } = Component
  6. if (setup) {
  7. const setupContext = (instance.setupContext =
  8. setup.length > 1 ? createSetupContext(instance) : null)
  9. const setupResult = callWithErrorHandling(setup, instance, 0 , [instance.props, setupContext])
  10. handleSetupResult(instance, setupResult)
  11. }
  12. else {
  13. finishComponentSetup(instance)
  14. }
  15. }

创建渲染上下文代理

首先是创建渲染上下文代理的流程,它主要对 instance.ctx 做了代理。在分析实现前,我们需要思考一个问题,这里为什么需要代理呢?

其实在 Vue.js 2.x 中,也有类似的数据代理逻辑,比如 props 求值后的数据,实际上存储在 this._props 上,而 data 中定义的数据存储在 this._data 上。举个例子:

  1. <template>
  2. <p>{{ msg }}</p>
  3. </template>
  4. <script>
  5. export default {
  6. data() {
  7. msg: 1
  8. }
  9. }
  10. </script>

在初始化组件的时候,data 中定义的 msg 在组件内部是存储在 this._data 上的,而模板渲染的时候访问 this.msg,实际上访问的是 this._data.msg,这是因为 Vue.js 2.x 在初始化 data 的时候,做了一层 proxy 代理。

到了 Vue.js 3.0,为了方便维护,我们把组件中不同状态的数据存储到不同的属性中,比如存储到 setupState、ctx、data、props 中。我们在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以我们也要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupState、ctx、data、props 中的数据的访问和修改。

明确了代理的需求后,我们接下来就要分析 proxy 的几个方法: get、set 和 has。

当我们访问 instance.ctx 渲染上下文中的属性时,就会进入 get 函数。我们来看一下它的实现:

  1. const PublicInstanceProxyHandlers = {
  2. get ({ _: instance }, key) {
  3. const { ctx, setupState, data, props, accessCache, type, appContext } = instance
  4. if (key[0] !== '$') {
  5. const n = accessCache[key]
  6. if (n !== undefined) {
  7. switch (n) {
  8. case 0:
  9. return setupState[key]
  10. case 1 :
  11. return data[key]
  12. case 3 :
  13. return ctx[key]
  14. case 2:
  15. return props[key]
  16. }
  17. }
  18. else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
  19. accessCache[key] = 0
  20. return setupState[key]
  21. }
  22. else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
  23. accessCache[key] = 1
  24. return data[key]
  25. }
  26. else if (
  27. type.props &&
  28. hasOwn(normalizePropsOptions(type.props)[0], key)) {
  29. accessCache[key] = 2
  30. return props[key]
  31. }
  32. else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
  33. accessCache[key] = 3
  34. return ctx[key]
  35. }
  36. else {
  37. accessCache[key] = 4
  38. }
  39. }
  40. const publicGetter = publicPropertiesMap[key]
  41. let cssModule, globalProperties
  42. if (publicGetter) {
  43. return publicGetter(instance)
  44. }
  45. else if (
  46. (cssModule = type.__cssModules) &&
  47. (cssModule = cssModule[key])) {
  48. return cssModule
  49. }
  50. else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
  51. accessCache[key] = 3
  52. return ctx[key]
  53. }
  54. else if (
  55. ((globalProperties = appContext.config.globalProperties),
  56. hasOwn(globalProperties, key))) {
  57. return globalProperties[key]
  58. }
  59. else if ((process.env.NODE_ENV !== 'production') &&
  60. currentRenderingInstance && key.indexOf('__v') !== 0) {
  61. if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
  62. warn(`Property ${JSON.stringify(key)} must be accessed via $data because it starts with a reserved ` +
  63. `character and is not proxied on the render context.`)
  64. }
  65. else {
  66. warn(`Property ${JSON.stringify(key)} was accessed during render ` +
  67. `but is not defined on instance.`)
  68. }
  69. }
  70. }
  71. }

可以看到,函数首先判断 key 不以 $ 开头的情况,这部分数据可能是 setupState、data、props、ctx 中的一种,其中 data、props 我们已经很熟悉了;setupState 就是 setup 函数返回的数据,稍后我们会详细说;ctx 包括了计算属性、组件方法和用户自定义的一些数据。

如果 key 不以 $ 开头,那么就依次判断 setupState、data、props、ctx 中是否包含这个 key,如果包含就返回对应值。注意这个判断顺序很重要在 key 相同时它会决定数据获取的优先级,举个例子:

  1. <template>
  2. <p>{{msg}}</p>
  3. </template>
  4. <script>
  5. import { ref } from 'vue'
  6. export default {
  7. data() {
  8. return {
  9. msg: 'msg from data'
  10. }
  11. },
  12. setup() {
  13. const msg = ref('msg from setup')
  14. return {
  15. msg
  16. }
  17. }
  18. }
  19. </script>

我们在 data 和 setup 中都定义了 msg 变量,但最终输出到界面上的是 “msg from setup”,这是因为 setupState 的判断优先级要高于 data。

再回到 get 函数中,我们可以看到这里定义了 accessCache 作为渲染代理的属性访问缓存,它具体是干什么的呢?组件在渲染时会经常访问数据进而触发 get 函数,这其中最昂贵的部分就是多次调用 hasOwn 去判断 key 在不在某个类型的数据中,但是在普通对象上执行简单的属性访问相对要快得多。所以在第一次获取 key 对应的数据后,我们利用 accessCache[key] 去缓存数据,下一次再次根据 key 查找数据,我们就可以直接通过 accessCache[key] 获取对应的值,就不需要依次调用 hasOwn 去判断了。这也是一个性能优化的小技巧。

如果 key 以 $ 开头,那么接下来又会有一系列的判断,首先判断是不是 Vue.js 内部公开的 $xxx 属性或方法(比如 $parent);然后判断是不是 vue-loader 编译注入的 css 模块内部的 key;接着判断是不是用户自定义以 $ 开头的 key;最后判断是不是全局属性。如果都不满足,就剩两种情况了,即在非生产环境下就会报两种类型的警告,第一种是在 data 中定义的数据以 $ 开头的警告,因为 $ 是保留字符,不会做代理;第二种是在模板中使用的变量没有定义的警告。

接下来是 set 代理过程,当我们修改 instance.ctx 渲染上下文中的属性的时候,就会进入 set 函数。我们来看一下 set 函数的实现:

  1. const PublicInstanceProxyHandlers = {
  2. set ({ _: instance }, key, value) {
  3. const { data, setupState, ctx } = instance
  4. if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
  5. setupState[key] = value
  6. }
  7. else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
  8. data[key] = value
  9. }
  10. else if (key in instance.props) {
  11. (process.env.NODE_ENV !== 'production') &&
  12. warn(`Attempting to mutate prop "${key}". Props are readonly.`, instance)
  13. return false
  14. }
  15. if (key[0] === '$' && key.slice(1) in instance) {
  16. (process.env.NODE_ENV !== 'production') &&
  17. warn(`Attempting to mutate public property "${key}". ` +
  18. `Properties starting with $ are reserved and readonly.`, instance)
  19. return false
  20. }
  21. else {
  22. ctx[key] = value
  23. }
  24. return true
  25. }
  26. }

结合代码来看,函数主要做的事情就是对渲染上下文 instance.ctx 中的属性赋值,它实际上是代理到对应的数据类型中去完成赋值操作的。这里仍然要注意顺序问题,和 get 一样,优先判断 setupState,然后是 data,接着是 props。

我们对之前的例子做点修改,添加一个方法:

  1. <template>
  2. <p>{{ msg }}</p>
  3. <button @click="random">Random msg</button>
  4. </template>
  5. <script>
  6. import { ref } from 'vue'
  7. export default {
  8. data() {
  9. return {
  10. msg: 'msg from data'
  11. }
  12. },
  13. setup() {
  14. const msg = ref('msg from setup')
  15. return {
  16. msg
  17. }
  18. },
  19. methods: {
  20. random() {
  21. this.msg = Math.random()
  22. }
  23. }
  24. }
  25. </script>

我们点击按钮会执行 random 函数,这里的 this 指向的就是 instance.ctx,我们修改 this.msg 会触发 set 函数,所以最终修改的是 setupState 中的 msg 对应的值。

注意,如果我们直接对 props 中的数据赋值,在非生产环境中会收到一条警告,这是因为直接修改 props 不符合数据单向流动的设计思想;如果对 Vue.js 内部以 $ 开头的保留属性赋值,同样也会收到一条警告。

如果是用户自定义的数据,比如在 created 生命周期内定义的数据,它仅用于组件上下文的共享,如下所示:

  1. export default {
  2. created() {
  3. this.userMsg = 'msg from user'
  4. }
  5. }

当执行 this.userMsg 赋值的时候,会触发 set 函数,最终 userMsg 会被保留到 ctx 中。

最后是 has 代理过程,当我们判断属性是否存在于 instance.ctx 渲染上下文中时,就会进入 has 函数,这个在平时项目中用的比较少,同样来举个例子,当执行 created 钩子函数中的’msg’ in this 时,就会触发 has 函数。

  1. export default {
  2. created () {
  3. console.log('msg' in this)
  4. }
  5. }

下面我们来看一下 has 函数的实现:

  1. const PublicInstanceProxyHandlers = {
  2. has
  3. ({ _: { data, setupState, accessCache, ctx, type, appContext } }, key) {
  4. return (accessCache[key] !== undefined ||
  5. (data !== EMPTY_OBJ && hasOwn(data, key)) ||
  6. (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
  7. (type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) ||
  8. hasOwn(ctx, key) ||
  9. hasOwn(publicPropertiesMap, key) ||
  10. hasOwn(appContext.config.globalProperties, key))
  11. }
  12. }

这个函数的实现很简单,依次判断 key 是否存在于 accessCache、data、setupState、props 、用户数据、公开属性以及全局属性中,然后返回结果。

至此,我们就搞清楚了创建上下文代理的过程,让我们回到 setupStatefulComponent 函数中,接下来分析第二个流程——判断处理 setup 函数。

判断处理 setup 函数

我们看一下整个逻辑涉及的代码:

  1. const { setup } = Component
  2. if (setup) {
  3. const setupContext = (instance.setupContext =
  4. setup.length > 1 ? createSetupContext(instance) : null)
  5. const setupResult = callWithErrorHandling(setup, instance, 0 , [instance.props, setupContext])
  6. handleSetupResult(instance, setupResult)
  7. }

如果我们在组件中定义了 setup 函数,接下来就是处理 setup 函数的流程,主要是三个步骤:创建 setup 函数上下文、执行 setup 函数并获取结果和处理 setup 函数的执行结果。接下来我们就逐个来分析。

首先判断 setup 函数的参数长度如果大于 1则创建 setupContext 上下文

  1. const setupContext = (instance.setupContext =
  2. setup.length > 1 ? createSetupContext(instance) : null)

举个例子,我们有个 HelloWorld 子组件,如下:

  1. <template>
  2. <p>{{ msg }}</p>
  3. <button @click="onClick">Toggle</button>
  4. </template>
  5. <script>
  6. export default {
  7. props: {
  8. msg: String
  9. },
  10. setup (props, { emit }) {
  11. function onClick () {
  12. emit('toggle')
  13. }
  14. return {
  15. onClick
  16. }
  17. }
  18. }
  19. </script>

我们在父组件引用这个组件:

  1. <template>
  2. <HelloWorld @toggle="toggle" :msg="msg"></HelloWorld>
  3. </template>
  4. <script>
  5. import { ref } from 'vue'
  6. import HelloWorld from "./components/HelloWorld";
  7. export default {
  8. components: { HelloWorld },
  9. setup () {
  10. const msg = ref('Hello World')
  11. function toggle () {
  12. msg.value = msg.value === 'Hello World' ? 'Hello Vue' : 'Hello World'
  13. }
  14. return {
  15. toggle,
  16. msg
  17. }
  18. }
  19. }
  20. </script>

可以看到,HelloWorld 子组件的 setup 函数接收两个参数,第一个参数 props 对应父组件传入的 props 数据,第二个参数 emit 是一个对象,实际上就是 setupContext。

下面我们来看一下用 createSetupContext 函数来创建 setupContext:

  1. function createSetupContext (instance) {
  2. return {
  3. attrs: instance.attrs,
  4. slots: instance.slots,
  5. emit: instance.emit
  6. }
  7. }

这里返回了一个对象,包括 attrs、slots 和 emit 三个属性。setupContext 让我们在 setup 函数内部可以获取到组件的属性、插槽以及派发事件的方法 emit。

可以预见的是,这个 setupContext 对应的就是 setup 函数第二个参数,我们接下来看一下 setup 函数具体是如何执行的。

我们通过下面这行代码来执行 setup 函数并获取结果

  1. const setupResult = callWithErrorHandling(setup, instance, 0 , [instance.props, setupContext])

我们具体来看一下 callWithErrorHandling 函数的实现:

  1. function callWithErrorHandling (fn, instance, type, args) {
  2. let res
  3. try {
  4. res = args ? fn(...args) : fn()
  5. }
  6. catch (err) {
  7. handleError(err, instance, type)
  8. }
  9. return res
  10. }

可以看到,它其实就是对 fn 做的一层包装,内部还是执行了 fn,并在有参数的时候传入参数,所以 setup 的第一个参数是 instance.props,第二个参数是 setupContext。函数执行过程中如果有 JavaScript 执行错误就会捕获错误,并执行 handleError 函数来处理。

执行 setup 函数并拿到了返回的结果,那么接下来就要用 handleSetupResult 函数来处理结果

  1. handleSetupResult(instance, setupResult)

我们详细看一下 handleSetupResult 函数的实现:

  1. function handleSetupResult(instance, setupResult) {
  2. if (isFunction(setupResult)) {
  3. instance.render = setupResult
  4. }
  5. else if (isObject(setupResult)) {
  6. instance.setupState = reactive(setupResult)
  7. }
  8. finishComponentSetup(instance)
  9. }

可以看到,当 setupResult 是一个对象的时候,我们把它变成了响应式并赋值给 instance.setupState,这样在模板渲染的时候,依据前面的代理规则,instance.ctx 就可以从 instance.setupState 上获取到对应的数据,这就在 setup 函数与模板渲染间建立了联系。

另外 setup 不仅仅支持返回一个对象,也可以返回一个函数作为组件的渲染函数。我们可以改写前面的示例,来看一下这时的情况:

  1. <script>
  2. import { h } from 'vue'
  3. export default {
  4. props: {
  5. msg: String
  6. },
  7. setup (props, { emit }) {
  8. function onClick () {
  9. emit('toggle')
  10. }
  11. return (ctx) => {
  12. return [
  13. h('p', null, ctx.msg),
  14. h('button', { onClick: onClick }, 'Toggle')
  15. ]
  16. }
  17. }
  18. }
  19. </script>

这里,我们删除了 HelloWorld 子组件的 template 部分,并把 setup 函数的返回结果改成了函数,也就是说它会作为组件的渲染函数,一切运行正常。

在 handleSetupResult 的最后,会执行 finishComponentSetup 函数完成组件实例的设置,其实这个函数和 setup 函数的执行结果已经没什么关系了,提取到外面放在 handleSetupResult 函数后面执行更合理一些。

另外当组件没有定义的 setup 的时候,也会执行 finishComponentSetup 函数去完成组件实例的设置。

完成组件实例设置

接下来我们来看一下 finishComponentSetup 函数的实现:

  1. function finishComponentSetup (instance) {
  2. const Component = instance.type
  3. if (!instance.render) {
  4. if (compile && Component.template && !Component.render) {
  5. Component.render = compile(Component.template, {
  6. isCustomElement: instance.appContext.config.isCustomElement || NO
  7. })
  8. Component.render._rc = true
  9. }
  10. if ((process.env.NODE_ENV !== 'production') && !Component.render) {
  11. if (!compile && Component.template) {
  12. warn(`Component provided template option but ` +
  13. `runtime compilation is not supported in this build of Vue.` +
  14. (` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
  15. ) )
  16. }
  17. else {
  18. warn(`Component is missing template or render function.`)
  19. }
  20. }
  21. instance.render = (Component.render || NOOP)
  22. if (instance.render._rc) {
  23. instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
  24. }
  25. }
  26. {
  27. currentInstance = instance
  28. applyOptions(instance, Component)
  29. currentInstance = null
  30. }
  31. }

函数主要做了两件事情:标准化模板或者渲染函数和兼容 Options API。接下来我们详细分析这两个流程。

标准化模板或者渲染函数

在分析这个过程之前,我们需要了解一些背景知识。组件最终通过运行 render 函数生成子树 vnode,但是我们很少直接去编写 render 函数,通常会使用两种方式开发组件。

第一种是使用 SFC(Single File Components)单文件的开发方式来开发组件,即通过编写组件的 template 模板去描述一个组件的 DOM 结构。我们知道 .vue 类型的文件无法在 Web 端直接加载,因此在 webpack 的编译阶段,它会通过 vue-loader 编译生成组件相关的 JavaScript 和 CSS,并把 template 部分转换成 render 函数添加到组件对象的属性中。

另外一种开发方式是不借助 webpack 编译,直接引入 Vue.js,开箱即用,我们直接在组件对象 template 属性中编写组件的模板,然后在运行阶段编译生成 render 函数,这种方式通常用于有一定历史包袱的古老项目。

因此 Vue.js 在 Web 端有两个版本:runtime-only 和 runtime-compiled。我们更推荐用 runtime-only 版本的 Vue.js,因为相对而言它体积更小,而且在运行时不用编译,不仅耗时更少而且性能更优秀。遇到一些不得已的情况比如上述提到的古老项目,我们也可以选择 runtime-compiled 版本。

runtime-only 和 runtime-compiled 的主要区别在于是否注册了这个 compile 方法。

在 Vue.js 3.0 中,compile 方法是通过外部注册的:

  1. let compile;
  2. function registerRuntimeCompiler(_compile) {
  3. compile = _compile;
  4. }

回到标准化模板或者渲染函数逻辑,我们先看 instance.render 是否存在,如果不存在则开始标准化流程,这里主要需要处理以下三种情况。

  1. compile 和组件 template 属性存在render 方法不存在的情况。此时, runtime-compiled 版本会在 JavaScript 运行时进行模板编译,生成 render 函数。
  2. compile 和 render 方法不存在,组件 template 属性存在的情况。此时由于没有 compile,这里用的是 runtime-only 的版本,因此要报一个警告来告诉用户,想要运行时编译得使用 runtime-compiled 版本的 Vue.js。
  3. 组件既没有写 render 函数,也没有写 template 模板,此时要报一个警告,告诉用户组件缺少了 render 函数或者 template 模板。

处理完以上情况后,就要把组件的 render 函数赋值给 instance.render。到了组件渲染的时候,就可以运行 instance.render 函数生成组件的子树 vnode 了。

另外对于使用 with 块运行时编译的渲染函数,渲染上下文的代理是 RuntimeCompiledPublicInstanceProxyHandlers,它是在之前渲染上下文代理 PublicInstanceProxyHandlers 的基础上进行的扩展,主要对 has 函数的实现做了优化:

  1. const RuntimeCompiledPublicInstanceProxyHandlers = {
  2. ...PublicInstanceProxyHandlers,
  3. get(target, key) {
  4. if (key === Symbol.unscopables) {
  5. return
  6. }
  7. return PublicInstanceProxyHandlers.get(target, key, target)
  8. },
  9. has(_, key) {
  10. const has = key[0] !== '_' && !isGloballyWhitelisted(key)
  11. if ((process.env.NODE_ENV !== 'production') && !has && PublicInstanceProxyHandlers.has(_, key)) {
  12. warn(`Property ${JSON.stringify(key)} should not start with _ which is a reserved prefix for Vue internals.`)
  13. }
  14. return has
  15. }
  16. }

这里如果 key 以 _ 开头,或者 key 在全局变量的白名单内,则 has 为 false,此时则直接命中警告,不用再进行之前那一系列的判断了。

了解完标准化模板或者渲染函数流程,我们来看完成组件实例设置的最后一个流程——兼容 Vue.js 2.x 的 Options API。

Options API:兼容 Vue.js 2.x

我们知道 Vue.js 2.x 是通过组件对象的方式去描述一个组件,之前我们也说过,Vue.js 3.0 仍然支持 Vue.js 2.x Options API 的写法,这主要就是通过 applyOptions 方法实现的。

  1. function applyOptions(instance, options, deferredData = [], deferredWatch = [], asMixin = false) {
  2. const {
  3. mixins, extends: extendsOptions,
  4. props: propsOptions, data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions,
  5. components, directives,
  6. beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeUnmount, unmounted, renderTracked, renderTriggered, errorCaptured } = options;
  7. const publicThis = instance.proxy;
  8. const ctx = instance.ctx;
  9. }

由于 applyOptions 的代码特别长,所以这里我用注释列出了它主要做的事情,感兴趣的同学可以去翻阅它的源码。

总结

这节课我们主要分析了组件的初始化流程,主要包括创建组件实例和设置组件实例。通过进一步细节的深入,我们也了解了渲染上下文的代理过程;了解了 Composition API 中的 setup 启动函数执行的时机,以及如何建立 setup 返回结果和模板渲染之间的联系;了解了组件定义的模板或者渲染函数的标准化过程;了解了如何兼容 Vue.js 2.x 的 Options API。

我们通过一张图再直观感受一下 Vue.js 3.0 组件的初始化流程:

04 | Setup:组件渲染前的初始化过程是怎样的? - 图2

最后,给你留一道思考题目,在执行 setup 函数并获取结果的时候,我们使用 callWithErrorHandling 把 setup 包装了一层,它有哪些好处?欢迎你在留言区与我分享。

本节课的相关代码在源代码中的位置如下:
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/errorHandling.ts