V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ouyangcoder
V2EX  ›  推广

[vue3 编译原理揭秘] vue3 的宏到底是什么东西?

  •  
  •   ouyangcoder · 203 天前 · 1255 次点击
    这是一个创建于 203 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    vue3开始vue引入了宏,比如definePropsdefineEmits等。我们每天写vue代码时都会使用到这些宏,但是你有没有思考过vue中的宏到底是什么?为什么这些宏不需要手动从vueimport?为什么只能在setup顶层中使用这些宏?

    vue 文件如何渲染到浏览器上

    要回答上面的问题,我们先来了解一下从一个vue文件到渲染到浏览器这一过程经历了什么?

    我们的vue代码一般都是写在后缀名为 vue 的文件上,显然浏览器是不认识 vue 文件的,浏览器只认识 html 、css 、jss 等文件。所以第一步就是通过webpack或者vite将一个 vue 文件编译为一个包含render函数的js文件。然后执行render函数生成虚拟 DOM ,再调用浏览器的DOM API根据虚拟 DOM 生成真实 DOM 挂载到浏览器上。

    vue3 的宏是什么?

    我们先来看看vue官方的解释:

    宏是一种特殊的代码,由编译器处理并转换为其他东西。它们实际上是一种更巧妙的字符串替换形式。

    宏是在哪个阶段运行?

    通过前面我们知道了vue 文件渲染到浏览器上主要经历了两个阶段。

    第一阶段是编译时,也就是从一个vue文件经过webpack或者vite编译变成包含 render 函数的 js 文件。此时的运行环境是nodejs环境,所以这个阶段可以调用nodejs相关的api,但是没有在浏览器环境内执行,所以不能调用浏览器的API

    第二阶段是运行时,此时浏览器会执行js文件中的render函数,然后依次生成虚拟DOM和真实DOM。此时的运行环境是浏览器环境内,所以可以调用浏览器的 API ,但是在这一阶段中是不能调用nodejs相关的api

    而宏就是作用于编译时,也就是从 vue 文件编译为 js 文件这一过程。

    举个defineProps的例子:在编译时defineProps宏就会被转换为定义props相关的代码,当在浏览器运行时自然也就没有了defineProps宏相关的代码了。所以才说宏是在编译时执行的代码,而不是运行时执行的代码。

    一个defineProps宏的例子

    我们来看一个实际的例子,下面这个是我们的源代码:

        <template>
          <div>content is {{ content }}</div>
          <div>title is {{ title }}</div>
        </template>
    
        <script setup lang="ts">
        import {ref} from "vue"
        const props = defineProps({
          content: String,
        });
        const title = ref("title")
        </script>
    

    在这个例子中我们使用defineProps宏定义了一个类型为String,属性名为contentprops,并且在template中渲染content的内容。

    我们接下来再看看编译成js文件后的代码,代码我已经进行过简化:

        import { defineComponent as _defineComponent } from "vue";
        import { ref } from "vue";
    
        const __sfc__ = _defineComponent({
          props: {
            content: String,
          },
          setup(__props) {
            const props = __props;
            const title = ref("title");
            const __returned__ = { props, title };
            return __returned__;
          },
        });
    
        import {
          toDisplayString as _toDisplayString,
          createElementVNode as _createElementVNode,
          Fragment as _Fragment,
          openBlock as _openBlock,
          createElementBlock as _createElementBlock,
        } from "vue";
    
        function render(_ctx, _cache, $props, $setup) {
          return (
            _openBlock(),
            _createElementBlock(
              _Fragment,
              null,
              [
                _createElementVNode(
                  "div",
                  null,
                  "content is " + _toDisplayString($props.content),
                  1 /* TEXT */
                ),
                _createElementVNode(
                  "div",
                  null,
                  "title is " + _toDisplayString($setup.title),
                  1 /* TEXT */
                ),
              ],
              64 /* STABLE_FRAGMENT */
            )
          );
        }
        __sfc__.render = render;
        export default __sfc__;
    

    我们可以看到编译后的js文件主要由两部分组成,第一部分为执行defineComponent函数生成一个 __sfc__ 对象,第二部分为一个render函数。render函数不是我们这篇文章要讲的,我们主要来看看这个__sfc__对象。

    看到defineComponent是不是觉得很眼熟,没错这个就是vue提供的 API 中的 definecomponent函数。这个函数在运行时没有任何操作,仅用于提供类型推导。这个函数接收的第一个参数就是组件选项对象,返回值就是该组件本身。所以这个__sfc__对象就是我们的vue文件中的script代码经过编译后生成的对象,后面再通过__sfc__.render = renderrender函数赋值到组件对象的render方法上面。

    我们这里的组件选项对象经过编译后只有两个了,分别是props属性和setup方法。明显可以发现我们原本在setup里面使用的defineProps宏相关的代码不在了,并且多了一个props属性。没错这个props属性就是我们的defineProps宏生成的。

    我们再来看一个不在setup顶层调用defineProps的例子:

        <script setup lang="ts">
        import {ref} from "vue"
        const title = ref("title")
    
        if (title.value) {
          const props = defineProps({
            content: String,
          });
        }
        </script>
    

    运行这个例子会报错:defineProps is not defined

    我们来看看编译后的 js 代码:

        import { defineComponent as _defineComponent } from "vue";
        import { ref } from "vue";
    
        const __sfc__ = _defineComponent({
          setup(__props) {
            const title = ref("title");
            if (title.value) {
              const props = defineProps({
                content: String,
              });
            }
            const __returned__ = { title };
            return __returned__;
          },
        });
    

    明显可以看到由于我们没有在setup的顶层调用defineProps宏,在编译时就不会将defineProps宏替换为定义props相关的代码,而是原封不动的输出回来。在运行时执行到这行代码后,由于我们没有任何地方定义了defineProps函数,所以就会报错defineProps is not defined

    总结

    现在我们能够回答前面提的三个问题了。

    • vue中的宏到底是什么?

      vue3的宏是一种特殊的代码,在编译时会将这些特殊的代码转换为浏览器能够直接运行的指定代码,根据宏的功能不同,转换后的代码也不同。

    • 为什么这些宏不需要手动从vueimport

      因为在编译时已经将这些宏替换为指定的浏览器能够直接运行的代码,在运行时已经不存在这些宏相关的代码,自然不需要从vueimport

    • 为什么只能在setup顶层中使用这些宏?

      因为在编译时只会去处理setup顶层的宏,其他地方的宏会原封不动的输出回来。在运行时由于我们没有在任何地方定义这些宏,当代码执行到宏的时候当然就会报错。

    如果想要在vue中使用更多的宏,可以使用 vue macros。这个库是用于在 vue 中探索更多的宏和语法糖,作者是 vue 的团队成员 三咲智子

    关注(图 1 )公众号: [前端欧阳] ,解锁我更多 vue 原理文章。

    加我(图 2 )微信回复「资料」,免费领取欧阳研究 vue 源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对 vue 有深入理解的人。

    公众号微信

    2 条回复
    wusheng0
        1
    wusheng0  
       203 天前 via Android
    👍🏻
    TaiShang
        2
    TaiShang  
       203 天前
    支持
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   6128 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 06:15 · PVG 14:15 · LAX 22:15 · JFK 01:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.