Vue 3 的 setup语法糖工作原理

news2024/11/16 17:30:43

前言

我们每天写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.coloradmin.cn/o/1688301.html

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

相关文章

【如何让论文中摘要后面的内容不出现在目录中】

首先选择摘要二字&#xff0c;设置为一级标题&#xff0c;然后选择摘要后面的内容设置为正文样式&#xff0c;再选择这一部分看一下是不是都是正文大纲级别&#xff0c;如果是那就可以了。 具体流程如下 1、选择摘要二字&#xff0c;设置为一级标题样式 2、选择摘要后面的文…

FreeRTOS学习——FreeRTOS队列(下)之队列创建

本篇文章记录我学习FreeRTOS队列创建的知识。主要分享队列创建需要使用的初始化函数、队列复位函数。 需要进一步了解FreeRTOS队列的相关知识&#xff0c;读者可以参考以下文章&#xff1a; FreeRTOS学习——FreeRTOS队列&#xff08;上&#xff09;_freertos 单元素队列-CSDN博…

scikit-learn机器学习要点总结

目录 一、机器学习总体流程二、引入数据集三、将数据集转换为DataFrame四、可视化数据五、数据预处理&#xff08;一&#xff09;数据标准化&#xff08;二&#xff09;独热编码 六、数据集划分为训练集和测试集七、创建模型估计器(estimator)&#xff08;一&#xff09;用于回…

人力资源(HR)OKR 案例

HR人员 #OKR# 是一个很好的方法来建立一致性&#xff0c;吸引团队成员&#xff0c;并实现高绩效。 在本文中&#xff0c;我们将回答以下问题&#xff1a; 如何写好HR OKR &#xff1f; 什么是好的HR OKR 的例子 &#xff1f; 我应该在我的HR OKR 中填写什么 &#xff1f; 如何…

stream( ).collect ( Collectors.groupingBy ( ) ) 的用法

文章目录 第一种解释1、基本用法2、指定值收集器3、多级分组4、常见应用场景和用处 第二种解释1、基本语法2、示例3、更复杂的用法 第一种解释 Collectors.groupingBy 是 Java 8 引入的 Stream API 中的一个收集器&#xff08;Collector&#xff09;&#xff0c;它用于将流&am…

【NumPy】关于numpy.transpose()函数,看这一篇文章就够了

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

16.js数学方法和进制转换

数学方法 &#xff08;1&#xff09;Math.random() 默认生成0-1的随机数 var resMath.random() console.log(res) &#xff08;2&#xff09;Math.round(数字) 取整&#xff1a;正数-四舍五入 负数-5舍6入 var resMath.round(11)console.log(res) //11var res1Math.round(1…

2024-5-6-从0到1手写配置中心Config之实现配置中心客户端

配置加载原理 在Spring中PropertySource类实现了所有属性的实例化。 启动赋值&#xff1a; 定义自定义属性配置源&#xff0c;从config-server获取全局属性&#xff1b;Spring启动时&#xff0c;插入自定义属性配置源&#xff1b;绑定属性会优先使用&#xff0c;给自定义属性…

下拉框操作/键鼠操作/文件上传

在我们做UI自动化测试的时候&#xff0c;会有一些元素需要特殊操作&#xff0c;比如下拉框操作/键鼠操作/文件上传。 下拉框操作 在我们很多页面里有下拉框的选择&#xff0c;这种元素怎么定位呢&#xff1f;下拉框分为两种类型&#xff1a;我们分别针对这两种元素进行定位和…

移动硬盘容量消失无法读取的解决方案

在数字化时代&#xff0c;数据的存储和备份变得尤为重要。移动硬盘作为一种便捷、大容量的存储设备&#xff0c;受到许多人的青睐。然而&#xff0c;有时我们可能会遭遇这样的问题&#xff1a;移动硬盘不显示容量且无法访问。这种情况无疑给我们的数据存储和管理带来了巨大的困…

sequence cache太小导致enq: SQ – contention

当业务卡的时候&#xff0c;发现大量等待事件为enq: SQ – contention&#xff0c;检查awr的top 5事件&#xff1a; sql语句对sequence的调用非常频繁&#xff1a; 对这些语句排查发现sequence cache值均为默认20&#xff0c;调大cache到1000值&#xff1a; SQL> select SE…

基于YOLOV8/YOLOV5的PCB板缺陷检测识别系统

摘要&#xff1a; 本文详细阐述了一个利用深度学习进行PCB板缺陷检测的系统&#xff0c;该系统集成了最新的YOLOv8算法&#xff0c;并与YOLOv7、YOLOv6、YOLOv5等先前版本进行了性能比较。该系统能够在图像、视频、实时视频流和批量文件中精确地识别和分类PCB板缺陷。文中不仅深…

WiFi蓝牙模块厂家带你了解蓝牙模块功率的等级

目前蓝牙技术的突破已经让许多蓝牙模块厂家从业者忘记了很多专业术语&#xff0c;比如Class1&#xff0c;Class2等&#xff0c;那么我们就蓝牙模块发射功率来做个详细了解。   针对功率来说&#xff0c;低功耗蓝牙和经典蓝牙又有区别。   低功耗蓝牙没有功率的级别&#xf…

Milvus:揭秘未来数据探索之钥

Milvus是什么&#xff1f; 想象一下&#xff0c;你正坐在一个巨大的图书馆中&#xff0c;成千上万本书摆放在眼前&#xff0c;但图书馆却没有目录和顺序&#xff0c;这听起来像一项艰巨的任务&#xff0c;不是吗&#xff1f;好消息是&#xff0c;在数字世界中&#xff0c;我们…

经纬恒润第三代重载自动驾驶平板车

随着无人驾驶在封闭场地和干线道路场景的加速落地&#xff0c;港口作为无人化运营的先行者&#xff0c;其场景的复杂度、特殊性对无人化运营的技术提出了各种挑战。经纬恒润作为无人驾驶解决方案提供商&#xff0c;见证了港口在无人化运营方面的尝试及发展&#xff0c;并深度参…

Python——基于共享单车使用量数据的可视化分析(1)

目录 &#x1f9fe; 1、数据集&#xff08;部分数据&#xff09; ✏️ 2、导入数据集与必要模块 1️⃣ 2.1 导入库以及字体包 2️⃣ 2.2 读取数据集 3️⃣ 2.3 查看数据集基本信息 ⌨️ 3、数据预处理 1️⃣ 3.1删除无关字段 2️⃣ 3.2对各字段进行中文标识 3️⃣ 3.3…

Timeline

SignalTrack信号轨道和自定义带参数的Marker信号和轨道 MySignalReceiver using System; using System.ComponentModel; using UnityEngine.Playables; using UnityEngine.Events;namespace UnityEngine.Timeline { public class BaseSignalReceiver<T, Q> : MonoBeha…

3100点失守,小丑竟是我自己

敏感时刻&#xff0c;亮剑的我们股市买单&#xff0c;海的那边反倒还涨了&#xff0c;好生气啊&#xff01;就连我们胡主编昨晚都气得睡不着&#xff0c;一点多了还在那抒发情感&#xff0c; 确实&#xff0c;有句话说到心窝里了&#xff1a;小散们是拿真金白银撑场子的&#x…

微星打造全球首款DDR5 CAMM2内存主板:这下清爽了

微星预告了新款主板“ Z790 Project Zero Plus”&#xff0c;这也是 全球第一款配备DDR5 CAMM2内存的桌面主板&#xff01; CAMM全称为“Compression Attached Memory Module”&#xff0c;已被JEDEC组织接纳为行业标准&#xff0c;相比传统的SO-DIMM、DIMM内存最大特点就是取消…

大模型之Ollama:在本地机器上释放大型语言模型的强大功能

LlaMA 3 系列博客 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;一&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;二&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;三&#xff09; 基于 LlaMA…