[vue3] vue3 setup函数

从语法上看,Composition API 提供了一个 setup 启动函数作为逻辑组织的入口,提供了响应式 API,提供了生命周期函数以及依赖注入的接口,通过调用函数来声明一个组件。

Options API

  • 选项式 API 在 props、data、methods、computed 等选项中定义变量;
  • 在组件初始化阶段,Vue.js 内部处理这些 options,把定义的变量添加到组件实例上;
  • 等模板编译成 render 函数的时候,内部通过 with(this){} 的语法去访问在组件实例中的变量。

在 Vue3 中,这两种 API 能够同时使用,但执行的优先级不同,建议只使用其中一种。

  • Options API 适合小型简单的组件;
  • Composition API 适合大型复杂、需要拆分逻辑的组件。

组件初始化

在 Vue3 中,render函数可以访问到 setup 函数返回的数据,这是怎么实现的呢?

组件的渲染流程是:创建vnode、渲染vnode、生成DOM。

其中渲染vnode就是在挂载(或更新)组件,通过 patch 函数对不同类型的 vnode 进行挂载或更新。

setup 函数只在首次挂载的流程中调用,因此这里主要研究挂载的流程。

通过 patch 函数内部的调用链通过 mountComponent 函数进行组件挂载。

mountComponent

const mountComponent = (
	initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    optimized,
) => {
    // 创建组件实例
    const instance: ComponentInternalInstance = 
          (initialVNode.component = createComponentInstance(
              initialVNode,
              parentComponent,
              parentSuspense,
          ))
    // 设置组件实例:props, slots ...
    setupComponent(instance)
    
    // 设置并运行带副作用的渲染函数
    setupRenderEffect(
        instance,
        initialVNode,
        container,
        anchor,
        parentSuspense,
        namespace,
        optimized,
    )
}

mountComponent 函数内部主要执行了三个函数:

  • createComponentInstance:【工厂模式】内部通过对象字面量创建一个组件实例对象并返回,对象包含许多属性,例如:
    • effect,update,job:与副作用和更新逻辑相关;
    • components、directives:局部组件与局部指令;
    • ctx、data、props:state相关;
    • bc、c、bm、m、bu:声明周期相关,源代码中用首字母命名,bc 是 beforeCreate;
  • setupComponent:
    1. 判断组件是否有状态;
    2. 初始化props
    3. 初始化slots
    4. 如果有状态,则调用 setupStatefulComponent 进行组件实例设置,内部调用了 setup 函数
  • setupRenderEffect:创建一个与更新相关的副作用,再包装成 Job 对象。这个副作用内部会调用生命周期 hook 。

这篇文章主要介绍setup,下面主要内容是执行了setup函数的 setupStatefulComponent 函数。

setupStatefulComponent

创建代理

这个函数创建了渲染上下文代理,创建代理的目的是让访问数据更加简便。

例如在 Vue2 中 props 的数据实际存储在 this._props 上,而 data 的数据则存储在 this._data 上,在组件方法中可以通过 this.msg 访问到 this._data.msg ,就是因为使用了代理。

而在 Vue3 中,不同的状态被存储在了组件实例的 setupState、ctx、data、props中。通过创建一个代理,渲染函数可以在一个对象上进行读写操作,再由代理将读写操作分发给不同的状态对象。

// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

函数内部还创建了一个 accessCache 对象,它是一个 key 到 state类型 的映射,避免每次读取一个 key 都要去判断这个 key 属于哪一种状态。

PublicInstanceProxyHandlers 内部关于 accessCache 的快速命中代码:

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]
            // default: just fallthrough
    }
}

如果没能命中缓存,内部只能通过 hasOwn 方法一个一个去判断状态对象上是否存在查询的 key。hasOwn 是这一过程中的开销大头。

判断的顺序很重要,当不同的状态对象有着相同名称的属性,那么优先应用那个先判断的类型。

get拦截(在没有命中缓存的情况下)的判断顺序是 setup、data、props、context。

这意味着当我们混用 setup函数 和 选项式 API 时,同名响应式变量会命中 setup 中声明的变量。

下面的代码会在界面上显示“from setup”。

<template>
  <p>{{ msg }}</p>
</template>

<script>
import { ref } from 'vue';
export default{
  data() {
    return {
      msg: 'from data'
    }
  },
  setup(){
    const msg = ref('from setup');
    return {
      msg
    }
  }
}
</script>

上面说的都是对get的拦截,对于set的拦截简要介绍如下:

  • key 的判断顺序和 get 一样;
  • 在开发环境中对props的修改操作进行警告。
调用setup

setupStatefulComponent 函数在创建了这个上下文代理之后,就调用了 setup 函数

大致流程如下:

  1. 判断是否有 setup 函数;

  2. 如果setup函数参数列表长度大于1,则调用 createSetupContext 函数创建 setup 上下文对象;

    setup函数的参数如下,第二个参数是一个上下文对象。如果用户(前端程序员)编写setup的时候使用了第二个参数,那么Vue在执行setup函数之前就要把这个上下文对象准备好。

    setup(props, { attrs, slots, emit, expose }) {
        ...
    }
    
  3. 执行 setup 函数获取返回的结果;

    执行 setup 函数是通过 callWithErrorHandling 间接调用的。其实内部不复杂,有参数则携带参数执行,用 try/catch 捕获错误。

    export function callWithErrorHandling(
      fn: Function,
      instance: ComponentInternalInstance | null | undefined,
      type: ErrorTypes,
      args?: unknown[],
    ): any {
      try {
        return args ? fn(...args) : fn()
      } catch (err) {
        handleError(err, instance, type)
      }
    }
    
  4. 处理 setup 返回的结果。

    setup 函数的返回值有两种类型:数据对象 和 渲染函数。

    • 如果是渲染函数,则绑定到 instance.render 上;

      instance.render = setupResult as InternalRenderFunction
      
    • 如果是数据对象,则先进行响应式包装,再绑定到 instance.setupState 上。

      instance.setupState = proxyRefs(setupResult)
      

至此,setup函数执行完成了,相关的数据也都绑定到了组件实例上。