翼度科技»论坛 编程开发 JavaScript 查看内容

vue3中template使用ref无需.value原因解析

4

主题

4

帖子

12

积分

新手上路

Rank: 1

积分
12
前言

众所周知,vue3的template中使用ref变量无需使用
  1. .value
复制代码
。还可以在事件处理器中进行赋值操作时,无需使用
  1. .value
复制代码
就可以直接修改ref变量的值,比如:
  1. <button @click="msg = 'Hello Vue3'">change msg</button>
复制代码
。你猜vue是在编译时就已经在代码中生成了
  1. .value
复制代码
,还是运行时使用Proxy拦截的方式去实现的呢?注:本文中使用的vue版本为
  1. 3.4.19
复制代码


看个demo

看个简单的demo,代码如下:
  1. <template>
  2.   <p>{{ msg }}</p>
  3.   <button @click="msg = 'Hello Vue3'">change msg</button>
  4. </template>
  5. <script setup lang="ts">
  6. import { ref } from "vue";
  7. const msg = ref("Hello World");
  8. console.log(msg.value);
  9. </script>
复制代码
上面的代码很简单,在script中想要访问
  1. msg
复制代码
变量的值需要使用
  1. msg.value
复制代码
。但是在template中将
  1. msg
复制代码
变量渲染到p标签上面时就是直接使用
  1. {{ msg }}
复制代码
,在
  1. click
复制代码
的事件处理器中给
  1. msg
复制代码
变量赋新的值时也没有使用到
  1. .value
复制代码

然后在浏览器中找到上面这个vue文件编译后的样子,在之前的文章中已经讲过很多次如何在浏览器中查看编译后的vue文件,这篇文章就不赘述了。编译后的代码如下:
  1. import {
  2.   Fragment as _Fragment,
  3.   createElementBlock as _createElementBlock,
  4.   createElementVNode as _createElementVNode,
  5.   defineComponent as _defineComponent,
  6.   openBlock as _openBlock,
  7.   toDisplayString as _toDisplayString,
  8.   ref,
  9. } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
  10. const _sfc_main = _defineComponent({
  11.   __name: "index",
  12.   setup() {
  13.     const msg = ref("Hello World");
  14.     console.log(msg.value);
  15.     const __returned__ = { msg };
  16.     return __returned__;
  17.   },
  18. });
  19. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  20.   return (
  21.     _openBlock(),
  22.     _createElementBlock(
  23.       _Fragment,
  24.       null,
  25.       [
  26.         _createElementVNode("p", null, _toDisplayString($setup.msg), 1),
  27.         _createElementVNode(
  28.           "button",
  29.           {
  30.             onClick:
  31.               _cache[0] ||
  32.               (_cache[0] = ($event) => ($setup.msg = "Hello Vue3")),
  33.           },
  34.           "change msg"
  35.         ),
  36.       ],
  37.       64
  38.     )
  39.   );
  40. }
  41. _sfc_main.render = _sfc_render;
  42. export default _sfc_main;
复制代码
vue文件编译后的代码主要分为两块:
  1. _sfc_main
复制代码
  1. _sfc_render
复制代码


    1. _sfc_main
    复制代码
    中主要是setup方法,这个是vue的
    1. <script setup lang="ts">
    复制代码
    部分编译后的样子。从上面可以看到在编译后的setup方法中,访问
    1. msg
    复制代码
    变量时依然使用了
    1. msg.value
    复制代码
    ,并且在setup方法中return了
    1. { msg }
    复制代码
    对象。
    1. _sfc_render
    复制代码
    就是我们熟悉的render函数,在render函数中渲染p标签部分的内容是:
    1. _toDisplayString($setup.msg)
    复制代码
    。很明显这个
    1. toDisplayString
    复制代码
    就是一个将输入值转换为字符串的函数,并没有处理
    1. .value
    复制代码

  1. $setup.msg
复制代码
中的
  1. $setup.
复制代码
,我想你猜到了应该和前面这个
  1. setup
复制代码
方法中return的
  1. { msg }
复制代码
对象有关,但是又不是直接使用
  1. setup
复制代码
方法中return的
  1. { msg }
复制代码
对象,因为使用setup中的
  1. msg
复制代码
变量需要使用
  1. .value
复制代码
,在编译后的render函数中并没有帮我们自动生成一个
  1. .value
复制代码
,比如这样的代码:
  1. $setup.msg.value
复制代码

同样的在render函数中,button的click事件给
  1. msg
复制代码
变量赋值时也没有帮我们生成一个类似于这样的代码:
  1. $setup.msg.value = "Hello Vue3"
复制代码
,而是
  1. $setup.msg = "Hello Vue3"
复制代码

从render函数中可以看出在template中使用ref变量无需使用
  1. .value
复制代码
,并不是编译时就已经在代码中生成了
  1. .value
复制代码
,比如
  1. $setup.msg.value
复制代码
,而是通过Proxy的方式去实现的。

render函数

在render函数中读和写
  1. msg
复制代码
变量都变成了
  1. $setup.msg
复制代码
,而这个
  1. $setup
复制代码
对象又是调用render函数时传入的第四个参数。现在我们需要搞清楚调用render函数时传入的第四个参数到底是什么?给render函数打一个断点,刷新页面,此时代码走到了断点里面,如下图:

右边的Call Stack表示当前函数的调用链,从调用链中可以看到render函数是由一个名为
  1. renderComponentRoot
复制代码
的函数调用的。
点击Call Stack中的
  1. renderComponentRoot
复制代码
,代码会跳转到
  1. renderComponentRoot
复制代码
函数中,在我们这个场景中简化后的
  1. renderComponentRoot
复制代码
函数代码如下:
  1. function renderComponentRoot(instance) {
  2.   const {
  3.     props,
  4.     data,
  5.     setupState,
  6.     render: render2,
  7.     // 省略...
  8.   } = instance;
  9.   render2.call(
  10.     thisProxy,
  11.     proxyToUse,
  12.     renderCache,
  13.     props,
  14.     setupState,
  15.     data,
  16.     ctx
  17.   );
  18. }
复制代码
这里的
  1. render2
复制代码
也就是我们的render函数,由于使用了
  1. .call
复制代码
,所以调用render函数时传入的第四个参数为
  1. setupState
复制代码
对象。而
  1. setupState
复制代码
对象的值又是从
  1. instance.setupState
复制代码
而来的。
通过debug调试render函数我们发现,在render函数中渲染
  1. msg
复制代码
变量是使用
  1. $setup.msg
复制代码
,而
  1. $setup
复制代码
对象的值是从
  1. instance.setupState
复制代码
对象上面来的。

前面讲过了编译后的
  1. setup
复制代码
方法会返回一个包含
  1. msg
复制代码
属性的对象,而这个
  1. $setup
复制代码
对象也就是
  1. instance.setupState
复制代码
肯定是和
  1. setup
复制代码
方法返回的对象有关系的。所以接下来我们需要去debug调试setup方法搞清楚他们到底是什么关系。

setup方法

将render函数中的断点去掉,然后给setup方法打一个断点。刷新页面,此时代码会走到断点中,如下图:

同理在Call Stack中可以看到调用setup方法的是
  1. callWithErrorHandling
复制代码
函数,点击Call Stack中的
  1. callWithErrorHandling
复制代码
,代码会跳转到
  1. callWithErrorHandling
复制代码
函数中。代码如下:
  1. function callWithErrorHandling(fn, instance, type, args) {
  2.   try {
  3.     return args ? fn(...args) : fn();
  4.   } catch (err) {
  5.     handleError(err, instance, type);
  6.   }
  7. }
复制代码
从上面可以看到在
  1. callWithErrorHandling
复制代码
函数中只是进行了错误处理,并不是我们想要找的。
  1. setupStatefulComponent
复制代码
函数
从Call Stack中可以看到调用
  1. callWithErrorHandling
复制代码
函数的是
  1. setupStatefulComponent
复制代码
函数,点击Call Stack中的
  1. setupStatefulComponent
复制代码
,代码会跳转到
  1. setupStatefulComponent
复制代码
函数中。在我们这个场景中简化后的
  1. setupStatefulComponent
复制代码
函数代码如下:
  1. function setupStatefulComponent(instance) {
  2.   const Component = instance.type;
  3.   const { setup } = Component;
  4.   const setupResult = callWithErrorHandling(setup, instance);
  5.   handleSetupResult(instance, setupResult);
  6. }
复制代码
从上面的代码可以看到确实是使用
  1. callWithErrorHandling
复制代码
函数执行了setup方法,并且还将setup方法的返回值对象赋值给了
  1. setupResult
复制代码
变量。然后以
  1. instance
复制代码
(vue实例)和
  1. setupResult
复制代码
(setup方法的返回值)为参数,调用了
  1. handleSetupResult
复制代码
函数。
  1. handleSetupResult
复制代码
函数
将断点走进
  1. handleSetupResult
复制代码
函数,在我们这个场景中简化后的
  1. handleSetupResult
复制代码
函数代码如下:
  1. function handleSetupResult(instance, setupResult) {
  2.   instance.setupState = proxyRefs(setupResult);
  3. }
复制代码
我们在render函数中渲染
  1. msg
复制代码
变量是使用
  1. $setup.msg
复制代码
,而
  1. $setup
复制代码
对象的值是从
  1. instance.setupState
复制代码
对象上面来的。
现在我们已经找到了
  1. instance.setupState
复制代码
是在这里赋值的,它的值是
  1. proxyRefs
复制代码
函数的返回结果。
  1. proxyRefs
复制代码
函数
将断点走进
  1. proxyRefs
复制代码
函数,代码如下:
  1. function proxyRefs(objectWithRefs) {
  2.   return isReactive(objectWithRefs)
  3.     ? objectWithRefs
  4.     : new Proxy(objectWithRefs, shallowUnwrapHandlers);
  5. }
复制代码
这个
  1. isReactive
复制代码
函数是vue暴露出来的一个API,它的作用是检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。
这里的
  1. objectWithRefs
复制代码
对象就是
  1. setup
复制代码
方法的返回值对象,通过前面我们知道
  1. setup
复制代码
方法的返回值对象就是一个普通的js对象,并不是reactive的。所以
  1. proxyRefs
复制代码
函数会返回三目运算符冒号(
  1. :
复制代码
)后面的表达式,也就是使用
  1. Proxy
复制代码
创建的
  1. setup
复制代码
方法返回值对象代理。
我们接着来看
  1. shallowUnwrapHandlers
复制代码
里面做了哪些事情,代码如下:
  1. const shallowUnwrapHandlers = {
  2.   get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
  3.   set: (target, key, value, receiver) => {
  4.     const oldValue = target[key];
  5.     if (isRef(oldValue) && !isRef(value)) {
  6.       oldValue.value = value;
  7.       return true;
  8.     } else {
  9.       return Reflect.set(target, key, value, receiver);
  10.     }
  11.   },
  12. };
复制代码
这个handler包含get和set方法,会对setup的返回值对象进行拦截。
当在render函数中渲染p标签时会去读
  1. $setup.msg
复制代码
,就会走到get的拦截中。在get方法中使用到了
  1. Reflect.get
复制代码
方法和
  1. unref
复制代码
函数。

    1. Reflect.get(target, key, receiver)
    复制代码
    的作用是获取
    1. target
    复制代码
    对象的
    1. key
    复制代码
    属性,在我们这里就是获取setup返回值对象的
    1. msg
    复制代码
    属性,也就是我们定义的
    1. msg
    复制代码
    变量。并且这个
    1. msg
    复制代码
    变量是一个ref。
    1. Reflect.get
    复制代码
    方法拿到的
    1. msg
    复制代码
    变量传给
    1. unref
    复制代码
    函数,这个
    1. unref
    复制代码
    函数同样是暴露出来的一个API。如果参数是 ref,则返回内部值,否则返回参数本身。这是
    1. val = isRef(val) ? val.value : val
    复制代码
    计算的一个语法糖。
经过
  1. unref
复制代码
函数的处理后,在get拦截中return的就是
  1. .value
复制代码
后的内容,也就是
  1. msg.value
复制代码

所以在template中使用ref变量无需使用
  1. .value
复制代码
,是因为在Proxy的get拦截中已经帮我们自动处理了.value。

当在render函数中去对ref变量进行赋值,比如:
  1. <button @click="msg = 'Hello Vue3'">change msg</button>
复制代码

就会走到set拦截中,首先会执行
  1. const oldValue = target[key]
复制代码
。这里的key就是"msg",target就是setup函数返回值对象。使用
  1. oldValue
复制代码
就是
  1. msg
复制代码
变量,是一个ref。
由于我们在click事件中要将
  1. msg
复制代码
赋值成'Hello Vue3'字符串,所以在set拦截中拿到的新value为'Hello Vue3'字符串。
接着执行
  1. if (isRef(oldValue) && !isRef(value))
复制代码
判断,这里的
  1. oldValue
复制代码
前面已经讲过了是一个名为
  1. msg
复制代码
的ref变量,所以
  1. isRef(oldValue)
复制代码
为true。
  1. value
复制代码
为'Hello Vue3'字符串,所以!isRef(value)也是为true。
代码就会走进if判断中执行
  1. oldValue.value = value
复制代码
,也就是在执行
  1. msg.value = 'Hello Vue3'
复制代码

所以在template中给ref变量赋值无需使用
  1. .value
复制代码
,是因为在Proxy的set拦截中也帮我们自动处理了
  1. .value
复制代码


总结

整个流程图如下:

在vue3的template中使用ref变量无需使用
  1. .value
复制代码
,是因为有个Proxy的get拦截,在get拦截中会自动帮我们去取ref变量的
  1. .value
复制代码
属性。
同样的在template中对ref变量进行赋值也无需使用
  1. .value
复制代码
,也是有个Proxy的set拦截,在set拦截中会自动帮我们去给ref变量的
  1. .value
复制代码
属性进行赋值。
到此这篇关于原来vue3中template使用ref无需.value是因为这个的文章就介绍到这了,更多相关原来vue3中template使用ref无需.value是因为这个内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/javascript/322392h4x.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具