前言
组合式API是Vue3一个非常亮眼的功能,它的出现是为了解决复杂组件代码的可复用性、可阅读性和可理解性的问题。当说起组合式API,实际上最先想起的就是setup选项,组件中的setup选项就是组合式API的基础,它充当组合API的入口点。区别于Vue2中使用选项组织代码,组合式实际上是对在setup中使用API这种方式的一种描述,如果你愿意实际上你可以在setup选项中完成绝大多数的功能逻辑,例如注册生命周期、定义computed和watch等等。
本文旨在梳理setup具体的执行逻辑,加深对其的理解和使用(Vue 3.1.1版本)。
setup
在之前mount挂载的文章中就提到setup选项的处理是在setupComponent函数,而该函数是在mountComponent中被调用。setupComponent函数的具体处理逻辑如下:
function setupComponent(instance, isSSR = false) {
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance);
initProps(instance, props, isStateful, isSSR);
initSlots(instance, children);
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
...
}
setupComponent函数的的逻辑很清晰,主要有如下几点:
- isStatefulComponent函数实际上就是判断虚拟节点shapeFlag的值是否是4,即普通有状态的组件
- initProps 和 initSlots
- 有状态的组件会调用setupStatefulComponent函数
initProps
initProps的主要逻辑如下:
function initProps(instance, rawProps, isStateful, isSSR = false) {
const props = {};
const attrs = {};
def(attrs, InternalObjectKey, 1);
instance.propsDefaults = Object.create(null);
setFullProps(instance, rawProps, props, attrs);
...
if (isStateful) {
// stateful
instance.props = isSSR ? props : shallowReactive(props);
}
instance.attrs = attrs;
}
实际上就是处理props和attrs,而props会通过shallowReactive进行浅层代理,这决定了使用需要props只代理了自身属性。其中attrs的处理是不同于Vue2,Vue3 attrs是Vue2中$attrs和$listeners的集合。
setupStatefulComponent
该函数的主要关注的逻辑如下:
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type;
...
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
const { setup } = Component;
if (setup) {
instance.setupContext = setup.length > 1
? createSetupContext(instance)
: null
const setupContext = instance.setupContext;
const setupResult = callWithErrorHandling(
setup,
instance,
0,
[shallowReadonly(instance.props) , setupContext]
);
if (isPromise(setupResult)) {
instance.asyncDep = setupResult;
} else {
handleSetupResult(instance, setupResult, isSSR);
}
}
}
setup函数是通过callWithErrorHandling函数来执行的(需要注意setup函数中this值的指向,callWithErrorHandling函数仅仅是调用而已并没有显式绑定this,所以this指向全局对象即window),其中参数是:shallowReadonly(instance.props) 、setupContext,其中props设置为浅层只读,而setupContext提供下面三个属性的获取:
function createSetupContext(instance) {
...
return Object.freeze({
get attrs() {
return new Proxy(instance.attrs, attrHandlers);
},
get slots() {
return shallowReadonly(instance.slots);
},
get emit() {
return (event, ...args) => instance.emit(event, ...args);
},
...
});
}
而针对于setup函数的返回值,实际上存在如下几种类型:
- 返回值是Promise对象,就赋值给instance.asyncDeps
- 返回值是function,就赋值给instance.render
- 返回值是Object,就赋值给instance.setupState,需要注意的是会使用proxyRefs做处理。实际上proxyRefs就是返回一个代理对象,主要是针对ref对象的特殊处理,在模板中可以直接使用而无需调用ref对象的value
setup函数的返回值可以直接在模板中使用,背后的实现逻辑实际上与mount挂载返回值类型以及template构建的render的上下文参数的有关,mount挂载返回的组件实例实际上就是一个Proxy对象,实际上组件中使用this就是组件实例,即instance.proxy:
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
而template构建的render函数中上下文参数最后也是调用PublicInstanceProxyHandlers中定义的拦截方法。
PublicInstanceProxyHandlers
这里值关心其拦截的get操作,对于属性获取的相关主要逻辑如下:
const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
...
if (key[0] !== '$') {
const n = accessCache[key];
if (n !== undefined) {
switch (n) {
case 0 /* SETUP */:
return setupState[key];
case 1 /* DATA */:
return data[key];
case 3 /* CONTEXT */:
return ctx[key];
case 2 /* PROPS */:
return props[key];
// default: just fallthrough
}
}
...
}
}
}
实际上对于组件实例上获取对应属性,其相关get拦截中实际上存在一种缓存机制,对于非$开头的属性第二次时会从指定数据源中获取,分别是:
- setupState:setup函数的返回值
- data:data选项中的数据
- ctx:组件实例都会有一个上下文,方法、$开头的内部属性等都会注册到这里
- props
总结
setup函数的执行逻辑相对来说是非常清晰的,其函数值存在不同类型的返回值,而针对不同类型的返回会有不同的处理,返回值类型支持:promise、function、object。虽然setup比较特殊,但是可以直接访问setup函数返回的值,不需要特殊调用,这一起都是源于组件实例是Proxy对象,其内部get拦截做了特殊的处理。