vue3的setup的使用和原理解析
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
1.前言
最近在做vue3相关的项目用到了组合式api对于vue3的语法的改进也是大为赞赏用起来十分方便。对于已经熟悉vue2写法的同学也说上手还是需要一定的学习成本有可能目前停留在会写会用的阶段但是setup带来哪些改变以及ref,reactive这两api内部实现原理到底是什么下面先来总结
setup带来的改变
1.解决了vue2的data和methods方法相距太远无法组件之间复用
2.提供了script标签引入共同业务逻辑的代码块顺序执行
3.script变成setup函数默认暴露给模版
4.组件直接挂载无需注册
5.自定义的指令也可以在模版中自动获得
6.this不再是这个活跃实例的引用
7.带来的大量全新api比如definePropsdefineEmitswithDefaulttoReftoRefs
ref带来的改变
Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式数据
Ref作TS的类型标注
reactive带来的改变
可以使用 reactive() 函数创建一个响应式对象或数组
reactive可以隐式地从它的参数中推导类型
使用interface进行类型标注
需要了解vue2和vue3区别的可以查看我的这篇文章
2.setup
在 setup()
函数中手动暴露大量的状态和方法非常繁琐。幸运的是我们可以通过使用构建工具来简化该操作。当使用单文件组件SFC时我们可以使用 <script setup>
来大幅度地简化代码。
<script setup>
中的顶层的导入和变量声明可在同一组件的模板中直接使用。你可以理解为模板中的表达式和 <script setup>
中的代码处在同一个作用域中。
里面的代码会被编译成组件 setup() 函数的内容
。这意味着与普通的 <script>
只在组件被首次引入
的时候执行一次
不同<script setup>
中的代码会在每次
组件实例被创建
的时候执行。
官方解答
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的<script>
语法它具有更多优势
- 更少的样板内容更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
setup执行是在创建实例之前就是beforeCreate执行所以setup函数中的this还不是组件的实例而是undefinedsetup是同步的。
setup?: (this: void, props: Readonly<LooseRequired<Props & UnionToIntersection<ExtractOptionProp<Mixin>> & UnionToIntersection<ExtractOptionProp<Extends>>>>, ctx: SetupContext<E>) => Promise<RawBindings> | RawBindings | RenderFunction | void;)
在上面的代码中我们了解到了第一个参数props还有第二个参数context。
props是接受父组件传递过来的所有的属性和方法context是一个对象这个对象不是响应式的可以进行解构赋值。存在属性为attrs:instance.slots,slots: instance.slots,emit: instance.emit。
setup(props, { attrs, slots, emit, expose }) {
...
}
或
setup(props, content) {
const { attrs, slots, emit, expose } = content
}
这里要注意一下attrs 和 slots 是有状态的对象它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构并始终以 attrs.x 或 slots.x 的方式引用 property。请注意与 props 不同attrs 和 slots 的 property 是非响应式的。如果你打算根据 attrs 或 slots 的更改应用副作用那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。
3.源码分析
在vue的3.2.3x版本中处理setup函数源码文件位于node_moudles/@vue/runtime-core/dist/runtime-core.cjs.js文件中。
setupStatefulComponent
下面开始解析一下setupStatefulComponent的执行过程
function setupStatefulComponent(instance, isSSR) {
var _a;
const Component = instance.type;
{
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config);
}
if (Component.components) {
const names = Object.keys(Component.components);
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config);
}
}
if (Component.directives) {
const names = Object.keys(Component.directives);
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i]);
}
}
if (Component.compilerOptions && isRuntimeOnly()) {
warn(`"compilerOptions" is only supported when using a build of Vue that ` +
`includes the runtime compiler. Since you are using a runtime-only ` +
`build, the options should be passed via your build tool config instead.`);
}
}
// 0. create render proxy property access cache
instance.accessCache = Object.create(null);
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = reactivity.markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
{
exposePropsOnRenderContext(instance);
}
// 2. call setup()
const { setup } = Component;
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
setCurrentInstance(instance);
reactivity.pauseTracking();
const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [reactivity.shallowReadonly(instance.props) , setupContext]);
reactivity.resetTracking();
unsetCurrentInstance();
if (shared.isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance);
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult) => {
handleSetupResult(instance, resolvedResult, isSSR);
})
.catch(e => {
handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */);
});
}
else {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult;
if (!instance.suspense) {
const name = (_a = Component.name) !== null && _a !== void 0 ? _a : 'Anonymous';
warn(`Component <${name}>: setup function returned a promise, but no ` +
`<Suspense> boundary was found in the parent component tree. ` +
`A component with async setup() must be nested in a <Suspense> ` +
`in order to be rendered.`);
}
}
}
else {
handleSetupResult(instance, setupResult, isSSR);
}
}
else {
finishComponentSetup(instance, isSSR);
}
}
函数接受两个参数一个是组建实例另一个是是否ssr渲染接下来是验证过程这里的文件是开发环境文件 DEV 环境则会开始检测组件中的各种选项的命名比如 name、components、directives 等如果检测有问题就会在开发环境报出警告。
检测完成之后进行初始化生成一个accessCached的属性对象该属性用以缓存渲染器代理属性以减少读取次数。然后在初始化一个代理的属性instance.proxy = reactivity.markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));这个代理属性代理了组件的上下文并且将它设置为观察原始值这样这个代理对象将不会被追踪。
接下来便是setup的核心逻辑了如果组件上有setup 函数继续执行如果不存在跳到尾部执行finishComponentSetup(instance, isSSR)完成组件的初始化否则就会进入 if (setup)
之后的分支条件中。是否执行setup生成上下文取决于setup.length > 1 createSetupContext(instance) : null。
来看一下setup执行上下文究竟有哪些东西
function createSetupContext(instance) {
const expose = exposed => {
if (instance.exposed) {
warn(`expose() should be called only once per setup().`);
}
instance.exposed = exposed || {};
};
let attrs;
{
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
return Object.freeze({
get attrs() {
return attrs || (attrs = createAttrsProxy(instance));
},
get slots() {
return reactivity.shallowReadonly(instance.slots);
},
get emit() {
return (event, ...args) => instance.emit(event, ...args);
},
expose
});
}
}
expose解析
可以在 setup() 中使用该 API 来清除地控制哪些内容会明确地公开暴露给组件使用者。
当你在封装组件时如果嫌 ref 中暴露的内容过多不妨用 expose 来约束一下输出。
import { ref } from 'vue'
export default {
setup(_, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
// 仅仅暴露 increment 给父组件
expose({
increment
})
return { increment, count }
}
}
例如当你像上方代码一样使用 expose 时父组件获取的 ref 对象里只会有 increment 属性而 count 属性将不会暴露出去。
执行setup函数
在处理完 createSetupContext 的上下文后组件会停止依赖收集并且开始执行 setup 函数。
const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [reactivity.shallowReadonly(instance.props) , setupContext]);
Vue 会通过 callWithErrorHandling 调用 setup 函数组件实例instance传入这里我们可以看最后一行是作为 args 参数传入的与上文描述一样props 会始终传入若是 setup.length <= 1 , setupContext 则为 null。
调用玩setup之后会重置收集的状态reactivity.resetTracking()接下来是判断setupResult的类型。
if (shared.isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance);
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult) => {
handleSetupResult(instance, resolvedResult, isSSR);
})
.catch(e => {
handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */);
});
}
else {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult;
if (!instance.suspense) {
const name = (_a = Component.name) !== null && _a !== void 0 ? _a : 'Anonymous';
warn(`Component <${name}>: setup function returned a promise, but no ` +
`<Suspense> boundary was found in the parent component tree. ` +
`A component with async setup() must be nested in a <Suspense> ` +
`in order to be rendered.`);
}
}
}
如果 setup 函数的返回值是 promise 类型并且是服务端渲染的则会等待继续执行。否则就会报错说当前版本的 Vue 并不支持 setup 返回 promise 对象。
如果不是 promise 类型返回值则会通过 handleSetupResult 函数来处理返回结果。
else {
handleSetupResult(instance, setupResult, isSSR);
}
function handleSetupResult(instance, setupResult, isSSR) {
if (shared.isFunction(setupResult)) {
// setup returned an inline render function
if (instance.type.__ssrInlineRender) {
// when the function's name is `ssrRender` (compiled by SFC inline mode),
// set it as ssrRender instead.
instance.ssrRender = setupResult;
}
else {
instance.render = setupResult;
}
}
else if (shared.isObject(setupResult)) {
if (isVNode(setupResult)) {
warn(`setup() should not return VNodes directly - ` +
`return a render function instead.`);
}
// setup returned bindings.
// assuming a render function compiled from template is present.
{
instance.devtoolsRawSetupState = setupResult;
}
instance.setupState = reactivity.proxyRefs(setupResult);
{
exposeSetupStateOnRenderContext(instance);
}
}
else if (setupResult !== undefined) {
warn(`setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult}`);
}
finishComponentSetup(instance, isSSR);
}
在 handleSetupResult 这个结果捕获函数中首先判断 setup 返回结果的类型如果是一个函数并且又是服务端的行内模式渲染函数则将该结果作为 ssrRender 属性而在非服务端渲染的情况下会直接当做 render 函数来处理。
接着会判断 setup 返回结果如果是对象就会将这个对象转换成一个代理对象并设置为组件实例的 setupState 属性。
最终还是会跟其他没有 setup 函数的组件一样调用 finishComponentSetup 完成组件的创建。
finishComponentSetup
function finishComponentSetup(instance, isSSR, skipOptions) {
const Component = instance.type;
// template / render function normalization
// could be already set when returned from setup()
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
// is done by server-renderer
if (!isSSR && compile && !Component.render) {
const template = Component.template;
if (template) {
{
startMeasure(instance, `compile`);
}
const { isCustomElement, compilerOptions } = instance.appContext.config;
const { delimiters, compilerOptions: componentCompilerOptions } = Component;
const finalCompilerOptions = shared.extend(shared.extend({
isCustomElement,
delimiters
}, compilerOptions), componentCompilerOptions);
Component.render = compile(template, finalCompilerOptions);
{
endMeasure(instance, `compile`);
}
}
}
instance.render = (Component.render || shared.NOOP);
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (installWithProxy) {
installWithProxy(instance);
}
}
// support for 2.x options
{
setCurrentInstance(instance);
reactivity.pauseTracking();
applyOptions(instance);
reactivity.resetTracking();
unsetCurrentInstance();
}
// warn missing template/render
// the runtime compilation of template in SSR is done by server-render
if (!Component.render && instance.render === shared.NOOP && !isSSR) {
/* istanbul ignore if */
if (!compile && Component.template) {
warn(`Component provided template option but ` +
`runtime compilation is not supported in this build of Vue.` +
(``) /* should not happen */);
}
else {
warn(`Component is missing template or render function.`);
}
}
}
这个函数的主要作用是获取并为组件设置渲染函数对于模板template以及渲染函数的获取方式有以下三种规范行为
1、渲染函数可能已经存在通过 setup 返回了结果。例如我们在上一节讲的 setup 的返回值为函数的情况。
2、如果 setup 没有返回则尝试获取组件模板并编译从 Component.render
中获取渲染函数
3、如果这个函数还是没有渲染函数则将 instance.render
设置为空以便它能从 mixins/extend 等方式中获取渲染函数。
这个在这种规范行为的指导下首先判断了服务端渲染的情况接着判断没有 instance.render 存在的情况当进行这种判断时已经说明组件并没有从 setup 中获得渲染函数在进行第二种行为的尝试。从组件中获取模板设置好编译选项后调用Component.render = compile(template, finalCompilerOptions);进行编译编译过程不再赘述。
最后将编译后的渲染函数赋值给组件实例的 render 属性如果没有则赋值为 NOOP 空函数。
接着判断渲染函数是否是使用了 with 块包裹的运行时编译的渲染函数如果是这种情况则会将渲染代理设置为一个不同的 has
handler 代理陷阱它的性能更强并且能够去避免检测一些全局变量。
至此组件的初始化完毕渲染函数也设置结束了。
4.总结
在vue3中新的setup函数属性给我们提供了书写的便利其背后的工作量无疑是巨大的有状态的组件的初始化的过程在 setup 函数初始化部分我们讨论的源码的执行过程我们不仅学习了 setup 上下文初始化的条件也明确的知晓了 setup 上下文究竟给我们暴露了哪些属性并且从中学到了一个新的 RFC 提案属性 expose 属性
我们学习了 setup 函数执行的过程以及 Vue 是如何处理捕获 setup 的返回结果的。
然后我们讲解了组件初始化时不论是否使用 setup 都会执行的 finishComponentSetup 函数通过这个函数内部的逻辑我们了解了一个组件在初始化完毕时渲染函数设置的规则。
最后如果本文对你了解setup过程有所帮助希望三连支持一波哈~~~❤️
你也可以关注我的vue其他文章