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,比如defineProps,defineEmits,withDefault,toRef,toRefs

ref带来的改变:

Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式数据

Ref作TS的类型标注

reactive带来的改变:

可以使用 reactive() 函数创建一个响应式对象或数组

reactive可以隐式地从它的参数中推导类型

使用interface进行类型标注

需要了解vue2和vue3区别的可以查看我的这篇文章:

vue2和vue3的区别(由浅入深)_KinHKin(五年前端)的博客-CSDN博客_vue2开发好还是vue3开发好Vue2使⽤的是选项类型API(Options API),Vue3使⽤的是合成型API(Composition API)Vue3:数据和⽅法都定义在setup中,并统⼀进⾏return{}vue2和vue3比较还是有很多不一样的地方,比如setup语法糖的形式最为便捷而且更符合开发者习惯,未来vue3将会大面积使用这种规则,这样更加符合开发习惯和降低后续维护的成本,还有目前Vue3已经成为了Vue的默认版本,后续维护应该也会以Vue3为主。希望各位同学赶紧学起来吧~......https://blog.csdn.net/weixin_42974827/article/details/126560362?spm=1001.2014.3001.5502

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还不是组件的实例,而是undefined,setup是同步的。

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 cacheinstance.accessCache = Object.create(null);// 1. create public instance / render proxy// also mark it raw so it's never observedinstance.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 itreturn 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 itreturn 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 functionif (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-rendererif (!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-renderif (!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其他文章:

https://blog.csdn.net/weixin_42974827/category_11970323.html?spm=1001.2014.3001.5482https://blog.csdn.net/weixin_42974827/category_11970323.html?spm=1001.2014.3001.5482

vue3的setup的使用和原理解析相关推荐

  1. Vue3的响应式原理解析

    Vue3的响应式原理解析 Vue2响应式原理回顾 // 1.对象响应化:遍历每个key,定义getter.setter // 2.数组响应化:覆盖数组原型方法,额外增加通知逻辑 const origi ...

  2. Vue3源码分析之打包原理

    Vue3源码分析之打包原理 如果之前你已经看过我的<Vue3源码分析之入门>,那么你可以直接阅读此篇文章 Vue3源码分析之入门 一.配置环境 1. 全局安装yarn Monorepo 管 ...

  3. 【Unity】 Spine渲染原理解析与源码解读

    Spine渲染原理解析与源码解读 安装环境 从Spine编辑器导出 将资源导入Unity 基础概念 其他相关概念 Spine架构 Spine运行时的各个模块 有状态(Stateful) 和 无状态(S ...

  4. AsyncTask机制原理解析

    AsyncTask机制原理解析 Android为我们提供了2种方便的异步处理方案,Handler和AsyncTask,两种方式适合的场景网上一搜就知道了,但是为什么呢?这篇分析将为你揭晓答案.前面分析 ...

  5. Spark Shuffle原理解析

    Spark Shuffle原理解析 一:到底什么是Shuffle? Shuffle中文翻译为"洗牌",需要Shuffle的关键性原因是某种具有共同特征的数据需要最终汇聚到一个计算节 ...

  6. 秋色园QBlog技术原理解析:性能优化篇:用户和文章计数器方案(十七)

    2019独角兽企业重金招聘Python工程师标准>>> 上节概要: 上节 秋色园QBlog技术原理解析:性能优化篇:access的并发极限及分库分散并发方案(十六)  中, 介绍了 ...

  7. Tomcat 架构原理解析到架构设计借鉴

    ‍ 点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 Tomcat 架构原理解析到架构设计借鉴 Tomcat 发展这 ...

  8. 秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五)...

    文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色 ...

  9. CSS实现元素居中原理解析

    原文:CSS实现元素居中原理解析 在 CSS 中要设置元素水平垂直居中是一个非常常见的需求了.但就是这样一个从理论上来看似乎实现起来极其简单的,在实践中,它往往难住了很多人. 让元素水平居中相对比较简 ...

最新文章

  1. IAR中断定义#pragma vector = P0INT_VECTOR __interrupt void P0_ISR(void)啥意思?
  2. 数据库密码加密 使用的是 druid加密
  3. OS_FLAG.C(1)
  4. java hashset 源码_Java集合源码分析-HashSet和LinkedHashSet
  5. Implicit conversion from enumeration type 'enum CGImageAlphaInfo' to different enumeration type 'CGB
  6. 数据库MySQL入门第一天
  7. P2P网络借贷系统-核心功能-用户投标-业务解说
  8. 豆瓣评分8.0以上数据分析R、MySQL、Python等书籍,45本包邮送到家
  9. STM32启动代码概述
  10. TreeSet与TreeMap
  11. Python读取nc文件
  12. recyclerview 动态设置高度_RecyclerView 动态调整高度, 调整item高度
  13. 在 UltraEdit /UEStudio 中配置自动更正关键字
  14. 港科招生 | 香港科大DBA(工商管理博士)项目介绍和科研成果分享
  15. 新兴的外贸获客系统:孚盟360度获客矩阵,高效助力外贸客户开发
  16. word2vec和word embedding有什么区别?
  17. fwidth、ddx、ddy解析
  18. 用cdn网址的方式引入echart和echart-gl
  19. python gui模板_Python GUI 编程(Tkinter) | 菜鸟教程
  20. 电信卡流量套餐超40G后,该如何解除网速限制?

热门文章

  1. 从 Secure Element 到 Android KeyStore
  2. 捡了西瓜丢芝麻——注意编程细节
  3. app分发源码完整­­|免签封装
  4. H2O with R 简明使用手记·下篇
  5. babyos2(30) APIC, local APIC, I/O APIC
  6. 火焰课堂java_通过火焰图引入Java剖析
  7. 利率里面的BP是什么意思,基准利率bp是什么意思
  8. 在Sublime Text 2中将默认语法设置为不同的文件类型
  9. python开发小程序拼团_微信小程序新功能,正合适开发拼团类小程序|明智科技...
  10. android显示地图代码,Android Studio之高德地图实现定位和3D地图显示(示例代码)