[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:
- 判断组件是否有状态;
- 初始化props
- 初始化slots
- 如果有状态,则调用 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 函数。
大致流程如下:
-
判断是否有 setup 函数;
-
如果setup函数参数列表长度大于1,则调用 createSetupContext 函数创建 setup 上下文对象;
setup函数的参数如下,第二个参数是一个上下文对象。如果用户(前端程序员)编写setup的时候使用了第二个参数,那么Vue在执行setup函数之前就要把这个上下文对象准备好。
setup(props, { attrs, slots, emit, expose }) { ... }
-
执行 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) } }
-
处理 setup 返回的结果。
setup 函数的返回值有两种类型:数据对象 和 渲染函数。
-
如果是渲染函数,则绑定到 instance.render 上;
instance.render = setupResult as InternalRenderFunction
-
如果是数据对象,则先进行响应式包装,再绑定到 instance.setupState 上。
instance.setupState = proxyRefs(setupResult)
-
至此,setup函数执行完成了,相关的数据也都绑定到了组件实例上。