Vue3中setup的运作方式
前言
在 Vue3 中,提供了一个新的选项setup
,我们可以在 setup
中使用 composition api,那么,这个setup
是如何工作的?
工作流程
首先,setup
函数初始化是在组件 mount 的时候。
// runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
));
// ...
// 在这里开始安装组件
setupComponent(instance);
if (__DEV__) {
endMeasure(instance, `init`);
}
// ...
};
它接收一个实例,执行组件的安装
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR;
const { props, children, shapeFlag } = instance.vnode;
// 判断是否是一个有状态的组件
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT;
// 初始化 props
initProps(instance, props, isStateful, isSSR);
// 初始化 插槽
initSlots(instance, children);
// 安装有状态的组件实例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
return setupResult;
}
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions;
// ...
// 0. create render proxy property access cache
// 创建渲染代理的属性访问缓存
instance.accessCache = {};
// 1. create public instance / render proxy
// also mark it raw so it's never observed
// 创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
if (__DEV__) {
exposePropsOnRenderContext(instance);
}
// 2. call setup()
// 调用setup
const { setup } = Component;
if (setup) {
// 判断setup是否有参数,如果有,则创建一个setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
currentInstance = instance;
pauseTracking();
// 执行 setup 函数,获取结果
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) => {
// 处理 setup 执行结果
handleSetupResult(instance, resolvedResult, isSSR);
});
} 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 {
// 处理 setup 执行结果
handleSetupResult(instance, setupResult, isSSR);
}
} else {
// 完成组件实例设置
finishComponentSetup(instance, isSSR);
}
}
如上述代码所示,组件安装的过程很简单,首先初始化props
、初始化slots
,然后判断组件是否为有状态组件,如果是有状态组件,则安装有状态组件的实例,最后将setup
的结果返回。
我们知道,在setup
中我们可以定义很多变量,在return
后可以在template
中被访问到,vue 是如何做到这一点的?这里比较核心的就是安装有状态组件实例的过程了 setupStatefulComponent
。
它的工作流程主要分为 3 个步骤:
1、创建可访问代理缓存
2、创建渲染上下文代理
3、执行setup
函数
export default {
data() {
return {
name: "leevare"
};
},
getName() {
return this.name;
}
};
通过instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
创建了渲染上下文代理,这有什么用?
在 vue2 中,我们给data
定义数据name
(如上述代码所示),其实在 vue 内部,实际数据是存储在this._data
对象上,当你访问this.name
的时候,其实真正访问的是this._data.name
。在 vue3 中,也需要这个代理,这里使用了instance.proxy
。那么此时对于数据的访问,都会被 proxy 所拦截,那么我们就看看PublicInstanceProxyHandlers
是如何定义的。
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const {
ctx,
setupState,
data,
props,
accessCache,
type,
appContext
} = instance;
if (key === ReactiveFlags.SKIP) {
return true;
}
let normalizedProps;
// 从缓存中读取
if (key[0] !== "$") {
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
}
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache![key] = AccessTypes.SETUP;
// 从 setupState 中取数据
return setupState[key];
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache![key] = AccessTypes.DATA;
// 从 data 中取数据
return data[key];
} else if (
// 从 props 中取数据
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
accessCache![key] = AccessTypes.PROPS;
return props![key];
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache![key] = AccessTypes.CONTEXT;
// 从 ctx 中取数据
return ctx[key];
} else if (!__FEATURE_OPTIONS_API__ || !isInBeforeCreate) {
// 全部都取不到
accessCache![key] = AccessTypes.OTHER;
}
}
const publicGetter = publicPropertiesMap[key];
let cssModule, globalProperties;
// 公开的 $xxx 属性或方法
if (publicGetter) {
if (key === "$attrs") {
track(instance, TrackOpTypes.GET, key);
__DEV__ && markAttrsAccessed();
}
return publicGetter(instance);
} else if (
// css 模块,通过 vue-loader 编译的时候注入
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])
) {
return cssModule;
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// 用户自定义的属性,也用 `$` 开头
accessCache![key] = AccessTypes.CONTEXT;
return ctx[key];
} else if (
// 全局定义的属性
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))
) {
return globalProperties[key];
} else if (
__DEV__ &&
currentRenderingInstance &&
(!isString(key) ||
// #1091 avoid internal isRef/isVNode checks on component instance leading
// to infinite warning loop
key.indexOf("__v") !== 0)
) {
if (
data !== EMPTY_OBJ &&
(key[0] === "$" || key[0] === "_") &&
hasOwn(data, key)
) {
warn(
`Property ${JSON.stringify(
key
)} must be accessed via $data because it starts with a reserved ` +
`character ("$" or "_") and is not proxied on the render context.`
);
} else {
warn(
`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`
);
}
}
},
// ...
set() {
//...
},
has() {
// ...
}
};
这里定义了三个方法,分别为get
、set
、has
。
当访问数据的时候,会被get
所拦截,在get
内部,控制了属性读取的先后顺序,依次为setupState
、data
、props
、ctx
、公开的内部$
开头的属性、cssModule 属性、用户自定义属性。可以看到,setup
中定义的属性优先级最高,就意味着,我们如果在data
中和setup
同时定义了同名的属性,优先访问的是setup
中的属性。
// 输出:<div>setup msg</div>
<template>
<div>{{ msg }}</div>
</template>
<script lang="ts">
export default {
data() {
return {
msg: "data msg"
};
},
setup() {
return {
msg: "setup msg"
};
}
};
</script>
这里使用了accessCache
,这个值是从外部定义的,通过这个值可以优化属性访问的速度。怎么做到的?
从上面的代码可以看出,如果我访问this.name
,那么 vue 会顺着setupState
、data
、props
等等的顺序依次寻找,通过hasOwn
寻找是否存在某个键。如果多次这样在不同对象上去寻找内容,肯定没有在一个对象中直接访问来的快,所以,这里的accessCache
就缓存了某个值具体存在于哪个属性中,下次访问的时候,直接到对应的属性中直接取值即可。
然后是set
和has
方法,和get
类似,这里就不在赘述,有兴趣可以直接看源码。
安装有状态组件的第一步完成了,然后是执行setup
。
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
);
// ...
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;
}
通过callWithErrorHandling
执行setup
,可以看到,其实在args
就是传给setup
的参数,这里传给了setup
两个参数,分别为props
和setupContext
,这就和官网上描述的一致(https://v3.vuejs.org/api/composition-api.html#setup)。
然后处理setup
执行的结果。
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup returned an inline render function
instance.render = setupResult as InternalRenderFunction;
} else if (isObject(setupResult)) {
// setup returned bindings.
// assuming a render function compiled from template is present.
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
instance.devtoolsRawSetupState = setupResult;
}
// 把 setup 返回结果变成响应式
instance.setupState = proxyRefs(setupResult);
if (__DEV__) {
exposeSetupStateOnRenderContext(instance);
}
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${
setupResult === null ? "null" : typeof setupResult
}`
);
}
finishComponentSetup(instance, isSSR);
}
如果结果是一个函数,直接将结果赋值给render
,就意味着,我们可以在setup
的返回函数中使用render
函数或者 JSX。如果是一个对象,就将它转换为响应式数据。
最后,完成组件的安装 finishComponentSetup
。
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions;
// template / render function normalization
// 对模板或者渲染函数的标准化
if (__NODE_JS__ && isSSR) {
if (Component.render) {
instance.render = Component.render as InternalRenderFunction;
}
} else if (!instance.render) {
// could be set from setup()
if (compile && Component.template && !Component.render) {
// 运行时编译
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
});
}
// 组件对象的 render 函数赋值给 instance
instance.render = (Component.render || NOOP) as InternalRenderFunction;
if (instance.render._rc) {
// 对于使用 with 块的运行时编译的渲染函数,使用新的渲染上下文的代理
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
);
}
}
// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
currentInstance = instance;
applyOptions(instance, Component);
currentInstance = null;
}
}
在最后一步,主要处理了 SSR、运行时编译和兼容 Vue2.x Options api 的一些操作。
结语
setup
给我们带来编码的灵活性是基于它的内部代码的复杂的处理逻辑,学习了解它的工作方式,对于我们后续的开发会有更大的帮助。
- 本博客所有文章除特别声明外,均可转载和分享,转载请注明出处!
- 本文地址:https://www.leevii.com/?p=2666