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>

这里我们定义了一个名为msgref响应式变量和非响应式的title变量,还有importchild.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 = renderrender函数挂到__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的返回值,返回值是一个对象,对象中包含titlemsgChild属性,非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中定义的顶层变量titlemsgcreateElementBlockcreateElementVNode等创建虚拟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_modulesvue的源码中去了。

图片

接下来我们需要在浏览器中找到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导入。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/840081.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

KDE-Ambari-Metrics-Collector问题排查解决手册

文档说明 本文档是为了解决KDE平台的Ambari-Metrics-Collector服务在运行时遇到的问题而提供的问题排查和解决方法的参考文档 说明: 当前的Ambari-Metrics-Collector服务包括了ams-collector和ams-hbase两个程序,在Ambari-Metrics-Collector安装的节点执行ps -elf|grep am…

远动通讯屏具体干啥作用

远动通讯屏具体干啥作用 远动通讯屏主要用于电力系统中的各类发电厂、变电站、光伏电站、开闭所、配电房等&#xff0c;具有实时传输数据和远程控制功能。它的主要作用包括&#xff1a; 数据采集&#xff1a;远动通讯屏能够采集各种模拟量、开关量和数字量等信息&#xff0c…

Java设计模式-备忘录模式(23)

备忘录模式(Memento Pattern)是一种行为设计模式,它提供了一种在不破坏对象封装性的前提下,捕获并存储对象的内部状态,并且可以在将来需要的时候恢复对象状态的方式。这一模式非常适合用于需要撤销操作或者实现状态回滚的场景。以下是Java中备忘录模式的详细解释: 核心角…

云计算的主要服务模式有哪几种?分别是什么特点?

云计算主要有以下几种服务模式&#xff1a; Infrastructure as a Service (IaaS&#xff0c;基础设施即服务)&#xff1a;提供虚拟化的计算资源&#xff0c;如服务器、存储和网络等基础设施。用户可以根据需要自由配置和管理这些资源&#xff0c;具有灵活性和可扩展性。 Platf…

pod介绍之 容器分类与重启策略

目录 一 pod 基础概念介绍 1&#xff0c;pod 是什么 2&#xff0c;Pod使用方式 3&#xff0c;如何解决一个pod 多容器通信 4&#xff0c;pod 组成 5&#xff0c; k8s 中的 pod 二 pause容器 1&#xff0c;pause容器 是什么 2&#xff0c;pause容器作用 3&#xff…

GitLab的原理及应用详解(二)

本系列文章简介: 随着软件开发的不断进步和发展,版本控制系统成为了现代软件开发过程中不可或缺的一部分。而GitLab作为其中一种流行的版本控制工具,在软件开发领域享有广泛的应用。GitLab不仅提供了强大的版本控制功能,还集成了项目管理、持续集成和部署、代码审查等多个功…

LeetCode-105-岛屿的最大面积

题目描述&#xff1a;给定一个由 0 和 1 组成的非空二维数组 grid &#xff0c;用来表示海洋岛屿地图。 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合&#xff0c;这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0&#xff08…

docker 笔记汇总

ubuntu 20.04 安装docker https://zhuanlan.zhihu.com/p/143156163 sudo apt update sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -s…

MybatisPlus优雅实现加密?

前言 最近在搞个安全需求&#xff0c;需要对敏感字段做加密存储。于是&#xff0c;&#xff0c;于是我就躺了个坑。 方案梳理 方案一&#xff1a;基于Mybatis的拦截器Interceptor 我的第一个反应其实是基于Mybatis的拦截器Interceptor机制实现&#xff0c;在设置参数的时候…

Vue开发实例(十三)用户登录功能

使用Vue实现登录具有以下几个好处&#xff1a; 响应式界面&#xff1a;Vue框架的响应式特性可以帮助开发者轻松地实现用户登录界面的交互效果&#xff0c;包括表单验证、实时错误提示等&#xff0c;从而提升用户体验。组件化开发&#xff1a;Vue框架支持组件化开发&#xff0c;…

【AI学习】卷积神经网络的由来

乱读书&#xff0c;看见这么这段话&#xff1a; “生物的眼睛以精巧的方式与大脑相连。视网膜上的感光细胞&#xff08;人眼的视杆或视锥&#xff09;并不直接连接到单个神经元上&#xff0c;而是会有一整片区域的神经元与每一个感光细胞相连接。相邻的神经元会连接到视网膜上相…

最新版npm详解

如&#xff1a;npm中搜索 jQuery image.png image.png 接地气的描述&#xff1a;npm 类似于如下各大手机应用市场 image.png image.png 查看本地 node 和 npm 是否安装成功 image.png image.png 或 npm install -g npm image.png image.png image.png image.png image.…

【数据库】MySQL

文章目录 概述DDL数据库操作查询使用创建删除 表操作创建约束MySqL数据类型数值类型字符串类型日期类型 查询修改删除 DMLinsertupdatedelete DQL基本查询条件查询分组查询分组查询排序查询分页查询 多表设计一对多一对一多对多设计步骤 多表查询概述内连接外连接 子查询标量子…

这所211专硕22408复试线310分,学硕收调剂!辽宁大学计算机考研考情分析!

辽宁大学信息学院下设计算机科学与技术、电子信息科学与技术、通信工程、信息管理与信息系统、软件工程5个本科专业&#xff0c;有计算机软件与理论、计算机应用技术2个硕士学位授权点&#xff0c;软件工程和计算机技术两个专业硕士学位点&#xff0c;1个计算机应用研究所、1个…

wordpress woocommer 添加代码实现,点击按钮,将产品添加到购物车并且跳转到结账页面

wordpress woocommer 添加代码实现&#xff0c;点击按钮&#xff0c;将产品添加到购物车并且跳转到结账页面 案列代码1&#xff0c;解决的是普通产品的 //短代码生成按钮&#xff0c;传入短代码&#xff0c;点击直接到达结账页面 function add_product_to_cart_button($atts)…

案例题(第一版)

案例题目 软件架构设计考点&#xff08;历年必考&#xff09; 软件架构设计通常在每年的第一题&#xff0c;该题必考 必备概念 必备概念即考试必须要默写出来的概念 概念描述软件架构风格是指描述特定软件系统组织方式和惯用模式。组织方式描述了系统的组成构件和这些构件的组…

在Spring Boot项目中通过自定义注解实现多数据源以及主备数据库切换

在现代的企业应用开发中&#xff0c;使用多数据源是一个常见的需求。尤其在关键应用中&#xff0c;设置主备数据库可以提高系统的可靠性和可用性。在这篇博客中&#xff0c;我将展示如何在Spring Boot项目中通过自定义注解实现多数据源以及主备数据库切换。 在此说明&#xff…

【HTML】制作一个跟随鼠标的流畅线条引导页界面(可直接复制源码)

目录 前言 HTML部分 CSS部分 JS部分 效果图 总结 前言 无需多言&#xff0c;本文将详细介绍一段HTML代码&#xff0c;图中线条可跟随鼠标移动&#xff0c;具体内容如下&#xff1a; 开始 首先新建一个HTML的文本&#xff0c;文本名改为[index.html]&#xff0c;创建好后右…

【逻辑漏洞】验证码绕过

【逻辑漏洞】验证码绕过 参数操纵值提取和重用自动化和识别其他测试 参数操纵 随机应变的请求方法&#xff1a;清空发送验证码参数。尝试更改HTTP方法从POST到GET或其他动词&#xff0c;并更改数据格式&#xff0c;例如在表单数据和JSON之间切换。发送空验证码&#xff1a;提交…

第十一节 SpringBoot Starter 面试题

一、面试题 很多同学的简历都写着熟悉 SpringBoot&#xff0c; 而 Starter 的实现原理被当作的考题的的情况越来越多。 来源牛客网关于 starter 的一些面试题 情景一、路虎一面 情景二、蔚来 情景三、同花顺 Starter 频频出现&#xff0c;因此在面试准备时&#xff0c;这道题…