面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

这篇具有很好参考价值的文章主要介绍了面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

我们每天都在用v-model,并且大家都知道在vue3中v-model:modelValue@update:modelValue的语法糖。那你知道v-model指令是如何变成组件上的modelValue属性和@update:modelValue事件呢?将v-model指令转换为modelValue属性和@update:modelValue事件这一过程是在编译时还是运行时进行的呢?

先说结论

下面这个是我画的处理v-model指令的完整流程图:
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

首先会调用parse函数将template模块中的代码转换为AST抽象语法树,此时使用v-model的node节点的props属性中还是v-model。接着会调用transform函数,经过transform函数处理后在node节点中多了一个codegenNode属性。在codegenNode属性中我们看到没有v-model指令,取而代之的是modelValueonUpdate:modelValue属性。经过transform函数处理后已经将v-model指令编译为modelValueonUpdate:modelValue属性,此时还是AST抽象语法树。所以接下来就是调用generate函数将AST抽象语法树转换为render函数,到此为止编译时做的事情已经做完了,经过编译时的处理v-model指令已经变成了modelValueonUpdate:modelValue属性。

接着就是运行时阶段,在浏览器中执行render函数生成虚拟DOM。在生成虚拟DOM的过程中由于props属性中有modelValueonUpdate:modelValue属性,所以就会给组件对象加上modelValue属性和@update:modelValue事件。最后就是调用mount方法将虚拟DOM转换为真实DOM。所以v-model指令转换为modelValue属性和@update:modelValue事件这一过程是在编译时进行的。

什么是编译时?什么是运行时?

vue是一个编译时+运行时一起工作的框架,之前有小伙伴私信我说自己傻傻分不清楚在vue中什么时候是编译时,什么时候是运行时。要回答小伙伴的这个问题我们要从一个vue文件是如何渲染到浏览器窗口中说起。

我们的vue代码一般都是写在后缀名为vue的文件上,显然浏览器是不认识vue文件的,浏览器只认识html、css、jss等文件类型。所以第一步就是通过webpack或者vite将一个vue文件编译为一个包含render函数的js文件,在这一步中代码的执行环境是在nodejs中进行,也就是我们所说的编译时。相比浏览器端来说能够拿到的权限更多,也能做更多的事情。后面就是执行render函数生成虚拟DOM,再调用浏览器的DOM API根据虚拟DOM生成真实DOM挂载到浏览器上。在第一步后面的这些过程中代码执行环境都是在浏览器中,也就是我们所说的运行时。在客户端渲染的场景下,一句话总结就是:代码跑在nodejs端的时候就是编译时,代码跑在浏览器端的时候就是运行时。
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

举个例子

我们来看一个v-model的例子,父组件index.vue的代码如下:

<template>
  <CommonChild v-model="inputValue" />
  <p>input value is: {{ inputValue }}</p>
</template>

<script setup lang="ts">
import { ref } from "vue";
import CommonChild from "./child.vue";

const inputValue = ref();
</script>

我们上面是一个很简单的v-model的例子,在CommonChild子组件上使用v-model绑定一个叫inputValue的ref变量,然后将这个inputValue变量渲染到p标签上面。

前面我们已经讲过了客户端渲染的场景下,在nodejs端工作的时候是编译时,在浏览器端工作的时候是运行时。那我们现在先来看看经过编译时阶段处理后,刚刚进入到浏览器端运行时阶段的js代码是什么样的。我们要如何在浏览器中找到这个js文件呢?其实很简单直接在network上面找到你的那个vue文件就行了,比如我这里的文件是index.vue,那我只需要在network上面找叫index.vue的文件就行了。但是需要注意一下network上面有两个index.vue的js请求,分别是template模块+script模块编译后的js文件,和style模块编译后的js文件。

那怎么区分这两个index.vue文件呢?很简单,通过query就可以区分。由style模块编译后的js文件的URL中有type=style的query,如下图所示:
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

这时有的小伙伴就开始疑惑了不是说好的浏览器不认识vue文件吗?怎么这里的文件名称是index.vue而不是index.js呢?其实很简单,在开发环境时index.vue文件是在App.vue文件中import导入的,而App.vue文件是在main.js文件中import导入的。所以当浏览器中执行main.js的代码时发现import导入了App.vue文件,那浏览器就会去加载App.vue文件。当浏览器加载完App.vue文件后执行时发现import导入了index.vue文件,所以浏览器就会去加载index.vue文件,而不是index.js文件。

至于什么时候将index.vue文件中的template模块、script模块、style模块编译成js代码,我们在 通过debug搞清楚.vue文件怎么变成.js文件文章中已经讲过了当import加载一个文件时会触发@vitejs/plugin-vue包中的transform钩子函数,在这个transform钩子函数中会将template模块、script模块、style模块编译成js代码。所以在浏览器中拿到的index.vue文件就是经过编译后的js代码了。

现在我们在浏览器的network中来看刚刚进入编译时index.vue文件代码,简化后的代码如下:

import {
  Fragment as _Fragment,
  createElementBlock as _createElementBlock,
  createElementVNode as _createElementVNode,
  createVNode as _createVNode,
  defineComponent as _defineComponent,
  openBlock as _openBlock,
  toDisplayString as _toDisplayString,
  ref,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";
import CommonChild from "/src/components/vModel/child.vue?t=1710943659056";
import "/src/components/vModel/index.vue?vue&type=style&index=0&scoped=0ebe7d62&lang.css";

const _sfc_main = _defineComponent({
  __name: "index",
  setup(__props, { expose: __expose }) {
    __expose();
    const inputValue = ref();
    const __returned__ = { inputValue, CommonChild };
    return __returned__;
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [
        _createVNode(
          $setup["CommonChild"],
          {
            modelValue: $setup.inputValue,
            "onUpdate:modelValue":
              _cache[0] ||
              (_cache[0] = ($event) => ($setup.inputValue = $event)),
          },
          null,
          8,
          ["modelValue"]
        ),
        _createElementVNode(
          "p",
          null,
          "input value is: " + _toDisplayString($setup.inputValue),
          1
          /* TEXT */
        ),
      ],
      64
      /* STABLE_FRAGMENT */
    )
  );
}

_sfc_main.render = _sfc_render;
export default _sfc_main;

从上面的代码中我们可以看到编译后的js代码主要分为两块,第一块是_sfc_main组件对象,里面有name属性和setup方法。一个vue组件在运行时实际就是一个对象,这里的_sfc_main就是一个vue组件对象。至于defineComponent函数的作用是在定义 Vue 组件时提供类型推导的辅助函数,所以在我们这个场景没什么用。我们接着来看第二块_sfc_render,从名字我想你应该已经猜到了他是一个render函数。执行这个_sfc_render函数就会生成虚拟DOM,然后再由虚拟DOM生成浏览器上面的真实DOM。

我们再来看这个render函数,在这个render函数前面会调用openBlock函数和createElementBlock函数。他的作用是在编译时尽可能的提取多的关键信息,可以减少运行时比较新旧虚拟DOM带来的性能开销,我们这篇文章不关注这点,所以我们接下来会直接看下面的_createVNode函数和_createElementVNode函数。

v-model语法糖怎么工作的

我们接着来看render函数中的_createVNode函数和_createElementVNode函数,代码如下:

import {
  createElementVNode as _createElementVNode,
  createVNode as _createVNode,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";

_createVNode(
  $setup["CommonChild"],
  {
    modelValue: $setup.inputValue,
    "onUpdate:modelValue":
      _cache[0] ||
      (_cache[0] = ($event) => ($setup.inputValue = $event)),
  },
  null,
  8,
  ["modelValue"]
),
_createElementVNode(
  "p",
  null,
  "input value is: " + _toDisplayString($setup.inputValue),
  1
  /* TEXT */
),

从这两个函数的名字我想你也能猜出来他们的作用是创建虚拟DOM,再仔细一看这两个函数不就是对应的我们template模块中的这两行代码吗。

<CommonChild v-model="inputValue" />
<p>input value is: {{ inputValue }}</p>

第一个_createVNode函数对应的是CommonChild,第二个_createElementVNode对应的是p标签。我们将重点放在_createVNode函数上,从import导入来看_createVNode函数是从vue中导出的createVNode函数。你是不是觉得createVNode这个名字比较熟悉呢,其实在 vue官网中有提到。

h() 是 hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是 createVnode(),但当你需要多次使用渲染函数时,一个简短的名字会更省力。

vue官网中h() 函数用于生成虚拟DOM,其实h()函数底层就是调用的createVnode函数。同样的createVnode函数和h() 函数接收的参数也差不多,第一个参数可以是一个组件对象也可以是像p这样的html标签,也可以是一个虚拟DOM。第二个参数为给组件或者html标签传递的props属性或者attribute。第三个参数是该节点的children子节点。现在我们再来仔细看这个_createVNode函数你应该已经明白了:

_createVNode(
  $setup["CommonChild"],
  {
    modelValue: $setup.inputValue,
    "onUpdate:modelValue":
      _cache[0] ||
      (_cache[0] = ($event) => ($setup.inputValue = $event)),
  },
  null,
  8,
  ["modelValue"]
),

我们在 Vue 3 的 setup语法糖到底是什么东西?文章中已经讲过了render函数中的$setup变量就是setup函数的返回值经过Proxy处理后的对象,由于Proxy的拦截处理让我们在template中使用ref变量时无需再写.value。在上面的setup函数中我们看到CommonChild组件对象也在返回值对象中,所以这里传入给createVNode函数的第一个参数为CommonChild组件对象。

我们再来看第二个参数对象,对象中有两个key,分别是modelValueonUpdate:modelValue。这两个key就是传递给CommonChild组件的两个props,等等这里有两个问题。第一个问题是这里怎么是onUpdate:modelValue,我们知道的v-model:modelValue@update:modelValue的语法糖,不是说好的@update怎么变成了onUpdate了呢?第二个问题是onUpdate:modelValue明显是事件监听而不是props属性,怎么是“通过props属性”而不是“通过事件”传递给了CommonChild子组件呢?

因为在编译时处理v-on事件监听会将监听的事件首字母变成大写然后在前面加一个on,塞到props属性对象中,所以这里才是onUpdate:modelValue。所以在组件上不管是v-bind的attribute和prop,还是v-on事件监听,经过编译后都会被塞到一个大的props对象中。以on开头的属性我们都视作事件监听,用于和普通的attribute和prop区分。所以你在组件上绑定一个onConfirm属性,属性值为一个handleClick的函数。在子组件中使用emit('confirm')是可以触发handleClick函数的执行的,但是一般情况下还是不要这样写,维护代码的人会看着一脸蒙蔽的。

我们接着来看传递给CommonChild组件的这两个属性值。

{
  modelValue: $setup.inputValue,
  "onUpdate:modelValue":
    _cache[0] ||
    (_cache[0] = ($event) => ($setup.inputValue = $event)),
}

第一个modelValue的属性值是$setup.inputValue。前面我们已经讲过了$setup.inputValue就是指向setup中定义的名为inputValue的ref变量,所以第一个属性的作用就是给CommonChild组件添加:modelValue="inputValue"的属性。

我们再来看第二个属性onUpdate:modelValue,属性值为_cache[0] ||(_cache[0] = ($event) => ($setup.inputValue = $event))。这里为什么要加一个_cache缓存呢?原因是每次页面刷新都会重新触发render函数的执行,如果不加缓存那不就变成了每次执行render函数都会生成一个事件处理函数。这里的事件处理函数也很简单,接收一个$event变量然后赋值给setup中的inputValue变量。接收的$event变量就是我们在子组件中调用emit触发事件传过来的第二个变量,比如:emit('update:modelValue', 'helllo word')。为什么是第二个变量呢?是因为emit函数接收的第一个变量为要触发的事件名称。所以第二个属性的作用就是给CommonChild组件添加@update:modelValue的事件绑定。

编译时如何处理v-model

前面我们已经讲过了在运行时已经拿到了key为modelValueonUpdate:modelValue的props属性对象了,我们知道这个props属性对象是在编译时由v-model指令编译而来的,那在这个编译过程中是如何处理v-model指令的呢?请看下面编译时的流程图:

面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

首先会调用parse函数将template模块中的代码转换为AST抽象语法树,此时使用v-model的node节点的props属性中还是v-model。接着会调用transform函数,经过transform函数处理后在node节点中多了一个codegenNode属性。在codegenNode属性中我们看到没有v-model指令,取而代之的是modelValueonUpdate:modelValue属性。经过transform函数处理后已经将v-model指令编译为modelValueonUpdate:modelValue属性,此时还是AST抽象语法树。所以接下来就是调用generate函数将AST抽象语法树转换为render函数,到此为止编译时做的事情已经做完了。

parse函数

首先是使用parse函数将template模块中的代码编译成AST抽象语法树,在这个过程中会使用到大量的正则表达式对字符串进行解析。我们直接来看编译后的AST抽象语法树是什么样子:
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

从上图中我们可以看到使用v-model指令的node节点中有了namemodelrawNamev-model的props了,明显可以看出将template中code代码字符串转换为AST抽象语法树时没有处理v-model指令。那么什么时候处理的v-model指令呢?

transform函数

其实是在后面的一个transform函数中处理的,在这个函数中主要调用的是traverseNode函数处理AST抽象语法树。在traverseNode函数中会去递归的去处理AST抽象语法树中的所有node节点,这也解释了为什么还要在transform函数中再抽取出来一个traverseNode函数。

我们再来思考一个问题,由于traverseNode函数会处理node节点的所有情况,比如v-model指令、v-for指令、v-onv-bind。如果将这些的逻辑全部都放到traverseNode函数中,那traverseNode函数的体量将会是非常大的。所以抽取出来一个nodeTransforms的概念,这个nodeTransforms是一个数组。里面存了一组transform函数,用于处理node节点。每个transform函数都有自己独有的作用,比如transformModel函数用于处理v-model指令,transformIf函数用于处理v-if指令。我们来看看经过transform函数处理后的AST抽象语法树是什么样的:
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

从上图中我们可以看到同一个使用v-model指令的node节点,经过transform函数处理后的和第一步经过parse函数处理后比起来node节点最外层多了一个codegenNode属性。

我们接下来看看codegenNode属性里面是什么样的:
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

从上图中我们可以看到在codegenNode中还有一个props属性,在props属性下面还有一个properties属性。这个properties属性是一个数组,里面就是存的是node节点经过transform函数处理后的props属性的内容。我们看到properties数组中的每一个item都有keyvalue属性,我想你应该已经反应过来了,这个keyvalue分别对应的是props属性中的属性名和属性值。从上图中我们看到第一个属性的属性名key的值为modelValue,属性值value$setup.inputValue。这个刚好就对应上v-model指令编译后的:modelValue="$setup.inputValue"

我们再来接着看第二个属性:
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

从上图中我们同样也可以看到第二个属性的属性名key的值为onUpdate:modelValue,属性值value的值拼起来就是为一串箭头函数,和我们前面编译后的代码一模一样。第二个属性刚好就对应上v-model指令编译后的@update:modelValue="($event) => ($setup.inputValue = $event)"

从上面的分析我们看到经过transform函数的处理后已经将v-model指令处理为对应的代码了,接下来我们要做的事情就是调用generate函数将AST抽象语法树转换成render函数

generate函数

generate函数中会递归遍历AST抽象语法树,然后生成对应的浏览器可执行的js代码。如下图:
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了

从上图中我们可以看到经过generate函数处理后生成的render函数和我们之前在浏览器的network中看到的经过编译后的index.vue文件中的render函数一模一样。这也证明了modelValue属性和@update:modelValue事件塞到组件上是在编译时进行的。

总结

现在我们可以回答前面提的两个问题了:

  • v-model指令是如何变成组件上的modelValue属性和@update:modelValue事件呢?

    首先会调用parse函数将template模块中的代码转换为AST抽象语法树,此时使用v-model的node节点的props属性中还是v-model。接着会调用transform函数,经过transform函数处理后在node节点中多了一个codegenNode属性。在codegenNode属性中我们看到没有v-model指令,取而代之的是modelValueonUpdate:modelValue属性。经过transform函数处理后已经将v-model指令编译为modelValueonUpdate:modelValue属性。其实在运行时onUpdate:modelValue属性就是等同于@update:modelValue事件。接着就是调用generate函数,将AST抽象语法树生成render函数。然后在浏览器中执行render函数时,将拿到的modelValueonUpdate:modelValue属性塞到组件对象上,所以在组件上就多了两个modelValue属性和@update:modelValue事件。

  • v-model指令转换为modelValue属性和@update:modelValue事件这一过程是在编译时还是运行时进行的呢?

    从上面的问题答案中我们可以知道将v-model指令转换为modelValue属性和@update:modelValue事件这一过程是在编译时进行的。

transform函数中是调用transformModel函数处理v-model指令,这篇文章没有深入到transformModel函数源码内去讲解。如果大家对transformModel函数的源码感兴趣请在评论区留言或者给我发信息,我会在后面的文章安排上。

关注公众号:前端欧阳,解锁我更多vue干货文章。还可以加我微信,私信我想看哪些vue原理文章,我会根据大家的反馈进行创作。
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了
面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了文章来源地址https://www.toymoban.com/news/detail-843406.html

到了这里,关于面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?

    最近有粉丝找到我,说被面试官给问懵了。 粉丝:面试官上来就问“ 一个vue文件是如何渲染成浏览器上面的真实DOM? ”,当时还挺窃喜这题真简单。就简单说了一下先是编译成render函数、然后根据render函数生成虚拟DOM,最后就是根据虚拟DOM生成真实DOM。按照正常套路面试官

    2024年04月22日
    浏览(45)
  • 程序员接私活还不知道这几个平台?那你真的亏了!

    程序员接私活现在已经是一个老生常谈的话题了,现在市面上各种程序员接单平台层出不穷,也参差不齐,有比较老牌的知名平台,也有比较好的新兴平台,如此多的平台就容易让人眼花缭乱,不知道该如何选择。 这期文章就带大家来了解几个比较热门的平台,涉及 国内外

    2024年01月16日
    浏览(68)
  • 每天使用Spring 框架,那你知道 lazy-init 懒加载原理吗?

    懒加载是Spring框架中的一个重要特性,它允许我们将bean的实例化推迟到第一次使用时。懒加载的主要用途是提高应用程序的启动性能,减少不必要的资源消耗。 在大型的应用程序中,有些bean可能只在特定的条件下才会被使用到。如果在应用程序启动时就实例化所有的bean,会

    2024年02月05日
    浏览(47)
  • 你不知道监控系统中的解码器、数字矩阵、监控平台,那你还未入门

    一、视频解码器 1、什么是视频解码器? 从定义上来说,视频编解码器,是指一个能够对数字视频进行压缩或者解压缩的程序或者设备。 这么说你可能有些模糊,总的来说,监控系统视频解码器的作用就是把偌大的视频信息进行压缩,然后在有线或无线的网络通讯中进行传输

    2024年02月02日
    浏览(43)
  • v-model

    v-model  是 Vue.js 中一个非常重要的指令,它用于在表单控件元素上创建双向数据绑定。这意味着表单控件和 Vue 实例的数据之间会保持同步:当表单控件的值改变时,Vue 实例的数据也会相应地更新;反之亦然,当 Vue 实例的数据改变时,表单控件的值也会更新。 v-model  本质上

    2024年04月10日
    浏览(35)
  • 组件v-model

    最近看到vue版本更新到3.4.x了,其中有个defineModel API,defineModel在3.3的时候是作为实验特性发布的,在3.4中逐渐稳定。这个API就是Vue3简化组件v-model的写法的,所以这篇就一块儿来总结一下vue中的组件v-model 官网的示例  先说下vue2中的使用,再说下vue3中的使用 下面这个单个v

    2024年02月01日
    浏览(76)
  • v-model原理

    v-model既可以作用于表单元素,也可以作用于自定义组件。无论哪种情况,它 都是一个语法糖,最终会生成一个属性和事件。 当作用于表单元素视时,vue会根据作用于表单元素类型而生成合适的属性和事件。例如: 1、作用于普通文本框时,会生成value属性和input事件; 2、当

    2024年01月23日
    浏览(36)
  • v-model的原理

    v-model 是 Vue.js 框架中的一个重要指令,用于实现双向数据绑定。它是一个语法糖,等效于使用 :value 和 @input 事件的组合。v-model 的工作原理可以分为以下步骤:首先,Vue 创建一个代理对象,其中包含了数据属性,例如 message。初始时,表单元素的值会被设置为数据属性的当前

    2024年02月08日
    浏览(40)
  • v-model.trim

    v-model.trim  是 Vue.js 中的指令之一,用于对双向绑定的数据进行自动去除首尾空格的处理。 当你在使用  v-model  指令时,可以通过添加  .trim  修饰符来启用自动去除首尾空格的功能。这对于输入框等表单元素非常有用,可以确保用户输入的值不包含不必要的空格。 以下是一

    2024年02月08日
    浏览(35)
  • vue v-model例子

    code 11

    2024年02月10日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包