Vue 3 的 setup语法糖到底是什么东西?
前言
我们每天写vue3项目的时候都会使用setup语法糖,但是你有没有思考过下面几个问题。setup语法糖经过编译后是什么样子的?为什么在setup顶层定义的变量可以在template中可以直接使用?为什么import一个组件后就可以直接使用,无需使用components 选项来显式注册组件?
vue 文件如何渲染到浏览器上
要回答上面的问题,我们先来了解一下从一个vue文件到渲染到浏览器这一过程经历了什么?
我们的vue代码一般都是写在后缀名为vue的文件上,显然浏览器是不认识vue文件的,浏览器只认识html、css、jss等文件。所以第一步就是通过webpack或者vite将一个vue文件编译为一个包含render函数的js文件。然后执行render函数生成虚拟DOM,再调用浏览器的DOM API根据虚拟DOM生成真实DOM挂载到浏览器上。

setup编译后的样子
在javascript标准中script标签是不支持setup属性的,浏览器根本就不认识setup属性。所以很明显setup是作用于编译时阶段,也就是从vue文件编译为js文件这一过程。
我们来看一个简单的demo,这个是index.vue源代码:
<template>
<h1>{{ title }}</h1>
<h1>{{ msg }}</h1>
<Child />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Child from "./child.vue";
const msg = ref("Hello World!");
const title = "title";
if (msg.value) {
const content = "content";
console.log(content);
}
</script>
这里我们定义了一个名为msg的ref响应式变量和非响应式的title变量,还有import了child.vue组件。
这个是child.vue的源代码
<template>
<div>i am child</div>
</template>
我们接下来看index.vue编译后的样子,代码我已经做过了简化:
import { ref } from "vue";
import Child from "./Child.vue";
const title = "title";
const __sfc__ = {
__name: "index",
setup() {
const msg = ref("Hello World!");
if (msg.value) {
const content = "content";
console.log(content);
}
const __returned__ = { title, msg, Child };
return __returned__;
},
};
import {
toDisplayString as _toDisplayString,
createElementVNode as _createElementVNode,
createVNode as _createVNode,
Fragment as _Fragment,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
_Fragment,
null,
[
_createElementVNode("h1", null, _toDisplayString($setup.title)),
_createElementVNode(
"h1",
null,
_toDisplayString($setup.msg),
1 /* TEXT */
),
_createVNode($setup["Child"]),
],
64 /* STABLE_FRAGMENT */
)
);
}
__sfc__.render = render;
export default __sfc__;
我们可以看到index.vue编译后的代码中已经没有了template标签和script标签,取而代之是render函数和__sfc__对象。并且使用__sfc__.render = render将render函数挂到__sfc__对象上,然后将__sfc__对象export default出去。
看到这里你应该知道了其实一个vue组件就是一个普通的js对象,import一个vue组件,实际就是import这个js对象。这个js对象中包含render方法和setup方法。
编译后的setup方法
我们先来看看这个setup方法,是不是觉得和我们源代码中的setup语法糖中的代码很相似?没错,这个setup方法内的代码就是由setup语法糖中的代码编译后来的。
setup语法糖原始代码
<script lang="ts" setup>
import { ref } from "vue";
import Child from "./child.vue";
const msg = ref("Hello World!");
const title = "title";
if (msg.value) {
const content = "content";
console.log(content);
}
</script>
setup编译后的代码
import { ref } from "vue";
import Child from "./Child.vue";
const title = "title";
const __sfc__ = {
__name: "index",
setup() {
const msg = ref("Hello World!");
if (msg.value) {
const content = "content";
console.log(content);
}
const __returned__ = { title, msg, Child };
return __returned__;
},
};
经过分析我们发现title变量由于不是响应式变量,所以编译后title变量被提到了js文件的全局变量上面去了。而msg变量是响应式变量,所以依然还是在setup方法中。我们再来看看setup的返回值,返回值是一个对象,对象中包含title、msg、Child属性,非setup顶层中定义的content变量就不在返回值对象中。
看到这里,可以回答我们前面提的第一个问题。
setup语法糖经过编译后是什么样子的?
setup语法糖编译后会变成一个setup方法,编译后setup方法中的代码和script标签中的源代码很相似。方法会返回一个对象,对象由setup中定义的顶层变量和import导入的内容组成。
由template编译后的render函数
我们先来看看原本template中的代码:
<template>
<h1>{{ title }}</h1>
<h1>{{ msg }}</h1>
<Child />
</template>
我们再来看看由template编译成的render函数:
import {
toDisplayString as _toDisplayString,
createElementVNode as _createElementVNode,
createVNode as _createVNode,
Fragment as _Fragment,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
_Fragment,
null,
[
_createElementVNode("h1", null, _toDisplayString($setup.title)),
_createElementVNode(
"h1",
null,
_toDisplayString($setup.msg),
1 /* TEXT */
),
_createVNode($setup["Child"]),
],
64 /* STABLE_FRAGMENT */
)
);
}
我们这次主要看在render函数中如何访问setup中定义的顶层变量title、msg,createElementBlock和createElementVNode等创建虚拟DOM的函数不在这篇文章的讨论范围内。你只需要知道createElementVNode("h1", null, _toDisplayString($setup.title))为创建一个h1标签的虚拟DOM就行了。
在render函数中我们发现读取title变量的值是通过$setup.title读取到的,读取msg变量的值是通过$setup.msg读取到的。这个$setup对象就是调用render函数时传入的第四个变量,我想你应该猜出来了,这个$setup对象就是我们前面的setup方法返回的对象。
那么问题来了,在执行render函数的时候是如何将setup方法的返回值作为第四个变量传递给render函数的呢?我在下一节会一步一步的带你通过debug源码的方式去搞清楚这个问题,我们带着问题去debug源码其实非常简单。
debug源码搞清楚是如何调用render函数
有的小伙伴看到这里需要看源码就觉得头大了,别着急,其实很简单,我会一步一步的带着你去debug源码。
首先我们将Enable JavaScript source maps给取消勾选了,不然在debug源码的时候断点就会走到vue文件中,而不是走到编译会的js文件中。

然后我们需要在设置里面的Ignore List看看node_modules文件夹是否被忽略。新版谷歌浏览器中会默认排除掉node_modules文件夹,所以我们需要将这个取消勾选。如果忽略了node_modules文件夹,那么debug的时候断点就不会走到node_modules中vue的源码中去了。

接下来我们需要在浏览器中找到vue文件编译后的js代码,我们只需要在network面板中找到这个vue文件的http请求,然后在Response下右键选择Open in Sources panel,就会自动在sources面板自动打开对应编译后的js文件代码。

找到编译后的js文件,我们想debug看看是如何调用render函数的,所以我们给render函数加一个断点。然后刷新页面,发现代码已经走到了断点的地方。我们再来看看右边的Call Stack调用栈,发现render函数是由一个vue源码中的renderComponentRoot函数调用的。

点击Call Stack中的renderComponentRoot函数就可以跳转到renderComponentRoot函数的源码,我们发现renderComponentRoot函数中调用render函数的代码主要是下面这样的:
function renderComponentRoot(instance) {
const {
props,
data,
setupState,
// 省略...
} = instance;
render2.call(
thisProxy,
proxyToUse,
renderCache,
props,
setupState,
data,
ctx
)
}
这里我们可以看到前面的$setup实际就是由setupState赋值的,而setupState是当前vue实例上面的一个属性。那么setupState属性是如何被赋值到vue实例上面的呢?
我们需要给setup函数加一个断点,然后刷新页面进入断点。通过分析Call Stack调用栈,我们发现setup函数是由vue中的一个setupStatefulComponent函数调用执行的。

点击Call Stack调用栈中的setupStatefulComponent,进入到setupStatefulComponent的源码。我们看到setupStatefulComponent中的代码主要是这样的:
function setupStatefulComponent(instance) {
const { setup } = Component;
// 省略
const setupResult = callWithErrorHandling(
setup,
instance
);
handleSetupResult(instance, setupResult);
}
setup函数是Component上面的一个属性,我们将鼠标放到Component上面,看看这个Component是什么东西?

看到这个Component对象中既有render方法也有setup方法是不是感觉很熟悉,没错这个Component对象实际就是我们的vue文件编译后的js对象。
const __sfc__ = {
__name: "index",
setup() {
const msg = ref("Hello World!");
if (msg.value) {
const content = "content";
console.log(content);
}
const __returned__ = { title, msg, Child };
return __returned__;
},
};
__sfc__.render = render;
从Component对象中拿到setup函数,然后执行setup函数得到setupResult对象。然后再调用handleSetupResult(instance, setupResult);
我们再来看看handleSetupResult函数是什么样的,下面是我简化后的代码:
function handleSetupResult(instance, setupResult) {
if (isFunction(setupResult)) {
// 省略
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult);
}
}
我们的setup的返回值是一个对象,所以这里会执行instance.setupState = proxyRefs(setupResult),将setup执行会的返回值赋值到vue实例的setupState属性上。
看到这里我们整个流程已经可以串起来了,首先会执行由setup语法糖编译后的setup函数。然后将setup函数中由顶层变量和import导入组成的返回值对象赋值给vue实例的setupState属性,然后执行render函数的时候从vue实例中取出setupState属性也就是setup的返回值。这样在render函数也就是template模版就可以访问到setup中的顶层变量和import导入。

现在我们可以回答前面提的另外两个问题了:
为什么在setup顶层定义的变量可以在template中可以直接使用?
因为在setup语法糖顶层定义的变量经过编译后会被加入到setup函数返回值对象__returned__中,而非setup顶层定义的变量不会加入到__returned__对象中。setup函数返回值会被塞到vue实例的setupState属性上,执行render函数的时候会将vue实例上的setupState属性传递给render函数,所以在render函数中就可以访问到setup顶层定义的变量和import导入。而render函数实际就是由template编译得来的,所以说在template中可以访问到setup顶层定义的变量和import导入。。
为什么import一个组件后就可以直接使用,无需使用components 选项来显式注册组件?
因为在setup语法糖中import导入的组件对象经过编译后同样也会被加入到setup函数返回值对象__returned__中,同理在template中也可以访问到setup的返回值对象,也就可以直接使用这个导入的组件了。
总结
setup语法糖经过编译后就变成了setup函数,而setup函数的返回值是一个对象,这个对象就是由在setup顶层定义的变量和import导入组成的。vue在初始化的时候会执行setup函数,然后将setup函数返回值塞到vue实例的setupState属性上。执行render函数的时候会将vue实例上的setupState属性(也就是setup函数的返回值)传递给render函数,所以在render函数中就可以访问到setup顶层定义的变量和import导入。而render函数实际就是由template编译得来的,所以说在template中就可以访问到setup顶层定义的变量和import导入。
如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!
Vue 3 的 setup语法糖到底是什么东西?的更多相关文章
- vue3 学习笔记(九)——script setup 语法糖用了才知道有多爽
刚开始使用 script setup 语法糖的时候,编辑器会提示这是一个实验属性,要使用的话,需要固定 vue 版本. 在 6 月底,该提案被正式定稿,在 v3.1.3 的版本上,继续使用但仍会有实验 ...
- 基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结
随着Vue3和TypeScript的大浪潮不断袭来,越来越多的Vue项目采用了TypeScript的语法来编写代码,而Vue3的JS中的Setup语法糖也越来越广泛的使用,给我们这些以前用弱类型的JS ...
- 【Vue3.0】关于 script setup 语法糖的用法
script setup - 简介 先来看一看官网关于 <script setup> 的介绍: 要彻底的了解 setup 语法糖,你必须先明确 setup() 这个 组合式API 官网中对 ...
- 传说中 VUE 的“语法糖”到底是啥?
一.什么是语法糖? 语法糖也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语.指的是计算机语言中添加的一种语法,在不影响功能的情况下,添加某种简单的语 ...
- Vue3中setup语法糖学习
目录 1,前言 2,基本语法 2,响应式 3,组件使用 3.1,动态组件 3.2,递归组件 4,自定义指令 5,props 5.1,TypeScript支持 6,emit 6.1,TypeScript ...
- vue 中的.sync语法糖
提到父子组件相互通信,可能大家的第一反应是$emit,最近在学着封装组件,以前都是用的别人封装好的UI组件,对vue中的.sync这个修饰符有很大的忽略,后来发现这个修饰符很nice,官方对她的描述是 ...
- vue3 setup语法糖下,vue自定义指令的实现,以及指令全局挂载,自定义v-loading的实现
最近一段时间,在做h5的移动端项目,UI组件库使用的vant,vant组件中的loading实在难用,无法包裹某个块进行loading,也无法对非组件的标签进行loading,所以想着自定义写个指令, ...
- Vue3.2中的setup语法糖,保证你看的明明白白!
vue3.2 到底更新了什么? 根据原文内容的更新的内容主要有以下 5 块: 1.SSR:服务端渲染优化.@vue/server-renderer包加了一个ES模块创建, 与Node.js解耦,使在非 ...
- vue3的setup语法糖
https://blog.csdn.net/weixin_44922480/article/details/127337914 https://blog.csdn.net/m0_63108819/ar ...
- vue项目中快捷语法糖
1.Vue.js是渐进式框架,采用自底向上增量开发的设计基于MVVM思想. 2.Vue 完全有能力驱动采用单文件组件和Vue生态系统支持的库开发的复杂单页应用. 3.Vue.js 的目标是通过尽可能简 ...
随机推荐
- [转帖]浅析TiDB二阶段提交
https://cloud.tencent.com/developer/article/1608073 关键内容说明: TiDB 对于每个事务,会涉及改动的所有key中,选择出一个作为当前事务的Pri ...
- [转帖]shell脚本实现文本内容比较交互程序
背景介绍 脚本基于Comm命令进行功能封装,考虑到命令执行前需要对文本进行排序,并且在多文件需要比较内容时可能会导致多个文本混乱,因此使用Shell封装成了一个交互式程序,快速对文件内容进行判断和输出 ...
- [转帖]Redis各版本特性汇总
redis4 redis5 redis6 redis6.2 重大特性 1.模块系统 2.PSYNC2 3.LFU淘汰策略 4.混合RDB-AOF持久化 5.LAZY FREE延迟释放 6.MEMORY ...
- node中的优先从缓存中加载模块与模块的加载规则
执行 node main.js 请问 b模块会被加载几次 //main.js require('./a.js') var fn = require('./b.js') console.log(fn.s ...
- 2021美亚杯团队赛write up
个人赛与团队赛下载文件解压密码:MeiyaCup2021 加密容器解密密码: uR%{)Y'Qz-n3oGU`ZJo@(1ntxp8U1+bW;JlZH^I4%0rxf;[N+eQ)Lolrw& ...
- 消息队列RabbitMQ教程
RabbitMQ教程 翻译自RabbitMQ Tutorials. 0. 准备 前期准备 1. Hello World 最简入门教程 2. 工作队列 竞争消费者模式 3. 发布/订阅 同时发送消息给多 ...
- @RequestBody中使用@DateTimeFormat报错:JSON parse error: Expected array or string.; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException
原因分析 根据异常提示:不匹配输入异常,指输入的参数错误,说是只支持String类型和Array数组类型的. @PostMapping("/test") public Dto ge ...
- 物联网浏览器(IoTBrowser)-Modbus协议集成和测试
Modbus协议在应用中一般用来与PLC或者其他硬件设备通讯,Modbus集成到IoTBrowser使用串口插件模式开发,不同的是采用命令函数,具体可以参考前面几篇文章.目前示例实现了Modbus-R ...
- idea右键没有run
最近用idea打开一个用Eclipse创建的项目,发现右键没有运行,这是一个很常见的问题,网上一大堆解决方案,试了一通都不行,最后在Event Log里的提示解决了问题. 首先我去配置jdk,发现已经 ...
- delphi 官方例子 simples 路径
公用 文件件 可能是隐藏的 若是的话 则显示 隐藏