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导入。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

匝间冲击耐压试验仪产品介绍及工作原理

产品简介 武汉凯迪正大KD2684S匝间冲击耐压试验仪适用于电机、变压器、电器线圈等这些由漆包线绕制的产品。因漆包线的绝缘涂敷层本身存在着质量问题&#xff0c;以及在绕线、嵌线、刮线、接头端部整形、绝缘浸漆、装配等工序工艺中不慎而引起绝缘层的损伤等&#xff0c;都会造…

零基础代码随想录【Day42】|| 1049. 最后一块石头的重量 II,494. 目标和,474.一和零

目录 DAY42 1049.最后一块石头的重量II 解题思路&代码 494.目标和 解题思路&代码 474.一和零 解题思路&代码 DAY42 1049.最后一块石头的重量II 力扣题目链接(opens new window) 题目难度&#xff1a;中等 有一堆石头&#xff0c;每块石头的重量都是正整…

Axure软件安装教程

链接&#xff1a;https://pan.baidu.com/s/1fHrSrZ7PIeDZZpn6QyJ6jQ?pwdb4mv 提取码&#xff1a;b4mv 安装完后点击Finish 名字随便起 关闭Axure 复制到安装目录下 最后成果

ASP+ACCESS基于WEB社区论坛设计与实现

摘要&#xff1a;系统主要实现BBS网站全部功能。采用目前应用最为广泛的ASP作为开发工具来开发此系统、以保证系统的稳定性。采用目前最为流行的网页制作工具Dreamweaver和目前最为流行的动画制作工具Flash MX。整个系统从符合操作简便、界面友好、灵活、实用、安全的要求出发&…

第七步 实现打印函数

文章目录 前言一、如何设计我们的打印函数&#xff1f;二、实践检验&#xff01; 查看系列文章点这里&#xff1a; 操作系统真象还原 前言 现在接力棒意见交到内核手中啦&#xff0c;只不过我们的内核现在可谓是一穷二白啥都没有&#xff0c;为了让我们设计的内核能被看见被使用…

uniapp微信小程序在ios端返回不显示弹窗的bug解决

这个问题其实是因为返回页面的时候弹的太快了导致的解决办法&#xff1a; 其实就是返回页面的弹窗加个延迟就好啦

电脑同时配置两个版本mysql数据库常见问题

1.配置时&#xff0c;要把bin中的mysql.exe和mysqld.exe 改个名字&#xff0c;不然两个版本会重复&#xff0c;当然&#xff0c;在初始化数据库的时候&#xff0c;如果时57版本的&#xff0c;就用mysql57(已经改名的)和mysqld57 代替 mysql 和 mysqld 例如 mysql -u root -p …

golang通过go-aci适配神通数据库

1. go-aci简介 go-aci是神通数据库基于ACI(兼容Oracle的OCI)开发的go语言开发接口&#xff0c;因此运行时需要依赖ACI驱动和ACI库的头文件。支持各种数据类型的读写、支持参数绑定、支持游标范围等操作。 2. Linux部署步骤 2.1. Go安装&#xff1a; 版本&#xff1a;1.9以上…

[数据集][目标检测]吸烟检测数据集VOC+YOLO格式1449张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1449 标注数量(xml文件个数)&#xff1a;1449 标注数量(txt文件个数)&#xff1a;1449 标注…

深度学习之基于Pytorch框架手写数字识别

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 手写数字识别是数字图像处理领域的一个经典问题&#xff0c;也是深度学习技术的一个常用应用场…

初识C语言——第二十八天

代码练习1&#xff1a; 用函数的方式实现9*9乘法表 void print_table(int n) {int i 0;int j 0;for (i 1; i< n; i){for (j 1; j< i; j){printf("%d*%d%-3d ", i, j, i * j);}printf("\n");}}int main() {int n 0;scanf("%d", &a…

【加密与解密(第四版)】第十六章笔记

第十六章 脱壳技术 16.1 基础知识 壳的加载过程&#xff1a;保存入口参数、获取壳本身需要使用的API地址、解密原程序各个区块的数据、IAT的初始化、重定位项的处理、HOOK API、跳转到程序原入口点 手动脱壳步骤&#xff1a;查找真正的入口点、抓取内存映像文件、重建PE文件&…

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…

Android studio关闭自动更新

Windows下&#xff1a; 左上角file - setting - Appearance & Behavier - system setting - update - 取消勾选

汉明码(海明码)的计算的规则

一.汉明码的由来 1.汉明码&#xff08;Hamming Code&#xff09;&#xff0c;是在电信领域的一种线性调试码&#xff0c;以发明者理查德卫斯里汉明的名字命名。汉明码在传输的消息流中插入验证码&#xff0c;当计算机存储或移动数据时&#xff0c;可能会产生数据位错误&#x…

vivado2020.2创建hls仿真工程实现led闪烁

下载vivado2020.2后会有这个出现在桌面 点击进入创建工程&#xff0c;这里注意不要有前面的\我再复制的时候复制错了导致创建失败 按f光标就会跳转到下一个f开头的函数处&#xff0c;要查找其他函数也同理 生成了一个synthesis summary文件 找到目录下生成的.v文件 an 点…

U-Mail邮件系统为用户提供更加安全的数据保护机制

据外媒报道&#xff0c;近日美国国家安全委员会泄露了其成员的近1万封电子邮件和密码&#xff0c;暴露了政府组织和大公司在内的2000家公司。其中包括美国国家航空航天局和特斯拉等。报道称该漏洞于3月7日被研究人员发现&#xff0c;通过该漏洞攻击者能够访问对web服务器操作至…

等保三级-MySQL 加固

1、身份鉴别 要求&#xff1a;建议身份密码登录&#xff0c;身份标识具有唯一性&#xff0c;身份鉴别信息具有复杂度要求&#xff0c;密码长度最少为8位&#xff0c;密码由数字、字母大小写、特殊符号组成、并设置定期更换&#xff0c;更换时间最长位90天 &#xff08;1&#…

asp.net core接入prometheus

安装prometheus和Grafana 参考之前的文章->安装prometheus和Grafana教程 源代码 dotnet源代码 新建.net core7 web项目 修改Program.cs using Prometheus;namespace PrometheusStu01;public class Program {public static void Main(string[] args){var builder We…

airflow2.7.3 + celery + redis + mysql 安装部署测试

集群环境&#xff1a; ​ 3台 centos 7.9 (dp95、dp96、dp97) python3.8 ​ dp96&#xff1a;mysql8.0.36(mysql8.0离线安装) ​ dp95\dp96\dp97&#xff1a;celery 集群(Celery安装测试) 安装目标&#xff1a; airflow2.7.3 mysql celery redis WebserverSchedulerwo…