05|响应式开发操作:如何理解和使用Vue3的响应式数据?

05|响应式开发操作:如何理解和使用 Vue 3的响应式数据?

你好,我是杨文坚。

你应该经常在一些技术文章或者博客听到Vue.js的“响应式编程”,那究竟什么是“响应式”?

“响应式”这个词放在不同技术场景下有不同含义,Vue.js一开始就是一个“响应式”的前端模板库,简单来讲,这个语境里的“响应式”就是“页面的模板内容能随着数据变化而重新渲染”。Vue.js最开始的响应式实现是基于ES6的一个 Object.defineProperty的特性拦截监听数据变化,一旦监听到数据变化就触发对应依赖数据的模板重新渲染。

到了Vue.js的3.x版本,响应式就换成 基于ES6的Proxy特性 来实现的,Proxy能监听一个对象的“读数据”和“写数据”的操作。最大的问题是因为Vue.js 1.x到2.x版本都是用的 Object.defineProperty 在监听数组变化时候,监听不到Array.push等数组变化操作,需要实现很多代码逻辑才能做好兼容。但用Proxy就能比较完美地直接监听数组的变化。

响应式开发是Vue.js框架的核心内容,开发者可以通过Vue.js的响应式的能力,直接用数据来驱动视图的变化,不需要写繁琐的DOM操作代码来处理视图的变化,可以让开发者能更加关注“如何设计数据来管理视图”,进而可以更加专注如何“根据业务逻辑来设计数据”,提升实现功能的开发效率。

那么既然Vue.js的响应式特性如此重要,学起来难不难呢?只要你跟着我的步骤由浅入深来进行学习,一节课下来就能掌握大致的使用方法啦。我现在就从响应式数据的基本操作来讲起。

响应式数据的基本操作

还记得我上节课一直演示的一个Vue.js的代码例子吗?这里再给你展示一下我们之前实现过的一个“计数器”,就是点击按钮,数字不断加1的计数器功能。

注意了,我这里用的是Vue.js推荐的组合式API的开发方式, <script> 标签需要加上setup属性。具体代码如下:

<template>
  <div class="app">
    <div class="text">Count: {{state.count}}</div>
    <button class="btn" @click="onClick">Add</button>
  </div>
</template>

<script setup>
  import { reactive } from 'vue';
  const state = reactive({
    count: 0
  });
  const onClick = () => {
    state.count ++;
  }
</script>

在这段Vue.js 3的代码中,reactive就是Vue.js 3官方提供的响应式API,state就是通过响应式API声明的响应式数据。state这个数据可以直接在模板里使用,例如上述代码中我们使用了对象state里的count数据进行显示。

那么响应式体现在哪呢?你看,上述代码里其实还实现了一个点击事件的函数onClick,这个函数实现了对state.count数值的自增操作。每当触发这个onClick事件,state.count就会自动加1,对应使用到state.count的模板也会随之重新渲染展示最新的数据内容。

上述功能代码里的这种视图随着数据的变化,就是响应式的特征。 基于Vue.js 3的响应式API reactive生成的数据,就是Vue.js 3的响应式数据。在Vue.js 3运行环境中,如果响应式数据的发生了变化,那么依赖了数据的视图也会跟着变化。

这里的响应式API reactive必须接受一个对象数据,通过内部封装返回一个Proxy类型的数据对象,这个Proxy数据就是响应式的核心。可以监听对象里每个属性的“读写”操作,通过“监听”或“劫持”属性的“读”和“写”的数据变化,触发依赖对应数据的模板视图进行重新渲染展示最新的数据值。

我们可以通过以下代码验证一下reactive 返回的数据类型:

<template></template>

<script setup>
import { reactive } from 'vue';
const state = reactive({
  count: 0
});

// 答应响应式数据内容
console.log(state)
</script>

执行这个Vue.js 3代码,在浏览器控制台打印的结果如下图所示:

在这里插入图片描述

我们可以看到,reactive生成的响应式数据是一个Proxy数据。那么问题来了,什么是Proxy?

Proxy是ES6的一个新语法,功能与其英文字面意思一样,就是提供“代理”的功能,可以让对象数据实现属性的读和写操作的代理拦截处理,具体的使用方法你可以参考 MDN技术文档对Proxy的介绍

通过上述内容,我们可以知道 Vue.js 3的响应式API reactive是把对象数据转成Proxy类型数据进行数据的“代理”和“监听”,那么是不是所有响应式数据都必须转成Proxy数据类型呢?

不一定。其实我们将上述计数器的代码换成另外一个响应式API,也能实现类似响应式功能的效果,代码如下所示:

<template>
  <div class="app">
    <div class="text">Count: {{count}}</div>
    <button class="btn" @click="onClick">Add</button>
  </div>
</template>

<script setup>
  import { ref } from 'vue';
  const count = ref(0);
  const onClick = () => {
    count.value ++;
  }
</script>

你可以看到,在这个修改过的Vue.js 3的代码里,ref就是Vue.js 3官方提供的另一个响应式API,count就是通过响应式API声明的响应式数据。如果我们要修改这个count数据,就需要借助 count.value 属性来“间接”修改这个响应数据。但如果你只是想在模板中使用这个数据,就不需要执行value属性访问,直接使用这个数据名称就行了。

这里的 ref可以接收一个基础数据类型或者对象数据类型,最后根据不同数据类型返回不同的响应式数据。

如果ref 接收的是基础类型,例如Number、String、Boolean、Null、Undefined等,返回的响应式数据类型就是Vue.js 3定义的响应式数据类型 RefImpl。如果想在JavaScript读写基础类型的响应式数据,需要通过 .value这个属性来进行操作,但是在模板template中就不需要这个value属性了。

如果ref 接收的是对象的数据类型,例如JSON、Array数据,那么返回的响应式数据类型也是RefImpl 数据类型。基于Proxy封装的数据是放在RefImpl数据中的value属性里,如果你想在JavaScript操作这个对象的响应式数据,也是需要执行value属性,value属性是用Proxy封装的对象数据,但是在模板template中就不需要这个value属性。

我们来做个代码实验,例如实现以下的代码片段,同样也是之前实现功能,是一个计数器操作。

<template>
  <div>{{count}}</div>
  <div>{{state.count}}</div>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(1);
const state = ref({
  count: 2
});

// ref 传入基本类型Number后 响应式数据内容
console.log(count)

// ref 传入对象数据后 响应式数据内容
console.log(state)
</script>

以上代码通过Vue.js 3编译在浏览器上运行后,在控制台打印结果如下图所示:

在这里插入图片描述

看到这里,你是不是有个疑问,为什么ref需要返回一个RefImpl类型,而不是直接返回Proxy类型呢?

这是因为Proxy只能代理监听对象类型的数据,例如JSON和Array数据,但是监听不了单纯的基础类型数据,例如Number、String之类。这个时候我们就需要设置一个 value属性的对象来处理对基础类型数据的代理监听操作,下面举一个简单的例子:

const data = {
  _value: 1,
  get value() {
    console.log('我代理监听到 “读” Number数据操作,即将返回1')
    return this._value;
  },
  set value(val) {
    console.log('我代理监听到 “写” Number数据操作,即将设置新数据' + val)
    this._value = val;
  }
}

// 读数据时,会打印出监听的日志 '我代理监听到 “读” Number数据操作,即将返回1'
data.value;
// 写数据时,会打印出监听的日志 '我代理监听到 “写” Number数据操作,即将设置新数据2'
data.value = 2;

从上述例子可以看出,因为Proxy监听不了基础类型数据(Number、String、Boolean、Undefined、Null、BigInt等这些基础类型),我们就需要构造出一个对象可以监听基础数据变化,实现响应式操作。

我们可以把基础类型数据(例如Number类型的数据)封装成一个对象,设置将基础类型读写操作转成对象属性value的get和set操作。这个时候,我们读写这个基础数据value,就可以基于对象的get和set的内置方法来拦截监听数据变化,实现基础数据类型的响应式能力。

到了这里,你应该会发现,Vue.js 3提供了两个重要的响应式API,reactive和ref,他们都可以生成响应式数据, 而且ref可以接收基础类型和对象类型数据,但是reactive只能接收对象类型数据

那么,这里就有一个问题了,在项目开发中,我们应该如何选择这两种API呢?

ref和reactive如何选择使用?

我们从上述内容可以知道,ref和reactive最大的区别就是ref可以接收所有数据类型,根据不同数据类型返回对应的RefImpl响应式数据,但是操作数据需要访问value属性。而reactive只能接收对象数据类型,返回一个Proxy的响应式数据,可以访问数据属性来操作数据内容。

这个时候我们可以根据能力差异来做选择。 如果是要定义对象类型的响应式数据,那么我们可以优先选择 reactive 来定义,这样子在JavaScript里可以直接操作对象响应式数据,就不需要多写一个value属性来操作了。而且,我们还可以避免对象本身带有value属性容易导致的理解上的属性混淆问题,例如下面代码所示:

<template>
  <h2>data.value: {{refData.value}}</h2>
</template>

<script setup>
import { ref } from 'vue';
// 如果有对象数据刚好是有value属性
const refData = ref({
  value: 2,
  count: 3
});

// 实际操作 value:2 数据需要这么操作
// 这里就会容易混淆 value 属性含义
refData.value.value += 1;

// 操作 count:3 这个数据
refData.value.count += 1;
</script>

从上述代码可以看出,对象数据还是少用ref来声明响应式数据,value操作容易带来团队合作开发过程中理解和沟通成本。

如果要定义基础类型的响应式数据,就用ref来定义,通过访问基础类型的响应式数据的value属性来进行读写操作。

除了常用的 reactive和ref之外,Vue.js 3还有其他功能定义不同的响应式API,你需要在特定场景下进行选择,相关的响应式API信息你可以查看 Vue.js 3的官方文档

下面,我就根据企业中的开发习惯,结合官方对响应式API的建议,再挑几个响应式的API给你举例说明下它们的适用场景。

如何选择合适使用其它响应式API?

我们先来讲讲 “去除响应式”的响应式API toRaw,也就是把响应式的关系解除。

平时在开发过程中,可能会将有些数据渲染在模板里的表单中,我们可以在表单中更改数据内容,但是又不希望表单里每次数据变化都触发响应式作用,这个时候就需要用到 toRaw 来解除掉数据的响应式。具体案例代码如下述源码所示:

<template>
  <form>
    <input v-model="data.name" placeholder="用户名称" />
    <br/>
    <textarea v-model="data.info" placeholder="用户信息" />
    <div>名称: {{data.name}}</div>
    <div>信息: {{data.info}}</div>
  </form>
</template>

<script setup>
import { reactive, toRaw } from 'vue';
import VUser from './user.vue';
const state = reactive({
  name: '张三',
  info: '某某公司前端开发工程师,有3年前端工作经验',
});
const data = toRaw(state);
</script>

上述代码中,我先基于 reactive创建了一个响应式数据state,里面存放了用户数据,再用toRaw进行解除掉响应式关系,返回一个原始的JSON数据 data,最后将其放在模板里的表单进行渲染。这样,当我们在表单的 <input><textarea> 组件中编辑这个原始数据data时,就不会触发响应式关系,也不会触发内容的重新渲染。

接下来我们要讲的 响应式API watch,这是监听响应式数据源变化的API,也就是监听指定响应式数据变化,触发对应的回调函数,常用于处理数据变化的副作用操作,例如输入框经常需要用到监听数据变化做字数统计的操作、统计一些中文文字个数等,如下述代码所示:

<template>
  <form>
    <textarea v-model="state.text" placeholder="信息" />
    <div>中文字数:{{state.zhCount}}</div>
  </form>
</template>

<script setup>
import { reactive, watch } from 'vue';

// 计算文本中文个数函数
function countZhText(txt) {
  const zhRegExp = /[\u4e00-\u9fa5]{1,}/g;
  const zhList = txt.match(zhRegExp);
  let count = 0;
  zhList.forEach((item) => {
    count += item.length;
  });
  return count;
}
const defaultText = '今天是2022年01月01日'
const state = reactive({
  text: defaultText,
  zhCount: countZhText(defaultText),
});

watch(
  // 监听 state.text 的变化
  [() => state.text ],
  ([ text ], [ prevText ]) => {
    // 当监听到state.text 变化,就会触发这个回调函数
    state.zhCount = countZhText(text);
  }
)
</script>

上述代码运行后,watch会监听 state.text 单独的数据变化,然后在 <textarea> 里编辑数据时,触发 watch 的回调函数,来计算state.text 里的中文个数,计算完之后会修改state.zhCount单独触发视图显示最新的中文个数。更多watch的使用方法,你可以查看 官方文档

通过我们前面对这两个响应式API,以及reactive和ref这两个设置响应式数据的API的讲解,你应该能体会到, 响应式API基本上可以设置响应式数据、解除响应式状态和监听响应式数据这基本三种类型

那么我们在日常开发中,应该如何根据开发场景进行选择合适响应式API呢?

答案就是要先理清楚我们需要什么响应式操作,判断到底是设置、解除,还是监听响应式数据,然后在官方的API文档中 https://cn.vuejs.org/api/ 找到自己合适的响应式处理场景。

不过,当你知道如何在合适场景中选择合适的响应式API进行开发的时候,并不能代表你已经掌握了Vue.js 3的响应式开发,你还需要知道在响应式开发中可能会遇到什么“坑”。那么,为了避免遇到这类“坑”,在做响应式开发时我们需要注意什么呢?

Vue.js 3的响应式开发有什么需要注意的?

这里主要有下面这三个注意点:

  • 响应式数据解构或者属性赋值后,可能会丢失响应式联系;
  • 慎用浅层响应式作用API;
  • 慎用副作用API。

第一个注意点就是“响应式数据解构或者属性赋值后,可能会丢失响应式联系”。怎么讲呢?我先给你展示一个代码例子:

<template>
  <textarea v-model="text" placeholder="文本信息" />
  <div>文本信息:{{text}}</div>
</template>

<script setup>
import { reactive } from 'vue';
const state = reactive({
  text: '今天是2022年01月01日',
});
const { text } = state;
</script>

你可以看到,上述代码中,text的响应式联系并不会生效, <textarea> 修改text内容后,都不会触发页面的展示文本text的视图更新渲染。这是为什么呢?

我来一一分析一下。这里,我们用reactive定义了一个state响应式JSON数据,但是在之后又把其中的 state.text 解构赋值给了变量text,这就会“断掉”了响应式的联系,导致再怎么更新text都不会触发视图重新更新渲染。

所以你在使用响应式数据时,要注意属性解构出来或者赋值出去,可能会带来“响应式联系的断开”,尽量避免相关的操作。

第二个注意点就是“慎用浅层响应式作用API”,例如shallowReactive和shallowReadonly。 为什么呢?

因为shallowReactive这类响应式API生成的响应式对象数据,只作用对象数据的下一级属性,至于对象的下下一级属性就作用不到了。如果没什么特殊的需求,我们就尽量少用这类API,这个Vue.js 3官方在API文档说明也提到过。

第三个注意点就是“慎用副作用API”。 为什么呢?我来把刚刚的一个代码例子改一下:

<template>
  <form>
    <textarea v-model="state.text" placeholder="信息" />
    <div>中文字数:{{state.zhCount}}</div>
  </form>
</template>

<script setup>
import { reactive, watch } from 'vue';

// 计算文本中文个数函数
function countZhText(txt) {
  const zhRegExp = /[\u4e00-\u9fa5]{1,}/g;
  const zhList = txt.match(zhRegExp);
  let count = 0;
  zhList.forEach((item) => {
    count += item.length;
  });
  return count;
}
const defaultText = '今天是2022年01月01日'
const state = reactive({
  text: defaultText,
  zhCount: countZhText(defaultText),
});

watch(
  // 监听 state.text 的变化
  [() => state.text, () => state.zhCount ],
  ([ text ], [ prevText ]) => {
    // 每当 state.text 变化,这个打印会触发两次
    console.log('正在监听变化...')
    // 当监听到state.text 变化,就会触发这个回调函数
    state.zhCount = countZhText(text);
  }
)
</script>

你看,上述代码中,每当 state.text 变化,打印代码 console.log(‘正在监听变化…’) 就会触发两次,这是为什么呢?因为监听修改state.text变化,回调函数里会修改state.zhCount数据,但是state.zhCount也被watch函数监听,这个时候会再次触发回调函数,所以就会触发两次。

现在上述代码在执行过程中,只是修改一个数据,连锁反应触发两次回调。如果监听控制不好,可能会陷入监听回调函数的死循环执行,所以在使用监听副作用的API时候,要注意回调内部的操作和依赖之间的关系,尽可能谨慎使用。

总结

通过今天的讲解,你应该能理解Vue.js 3的响应式开发操作了,正是由于Vue.js的响应式特性,开发者才可以很方便地实现自己想要的页面功能。

我在此总结一下Vue.js 3 的响应式开发操作的注意点,具体如下:

  • 根据不同数据类型选择合适的响应式API,例如基础数据用ref,对象数据用reactive;
  • 如果想消除数据的响应式特性,可以通过toRaw来进行消除,将响应式数据变成普通数据;
  • 不要随便解构响应式数据的属性,把属性赋值给其他变量的时候,赋值出去的数据容易“断开”响应式的联系;
  • 监听响应式数据变化的其它副作用操作,可以通过watch来监听处理事件;
  • 以上几点如果不能满足你的开发需要,再去看官方的其它响应式API,但是要慎用其它响应式API。

为什么要“慎用”其它响应式API呢?这是因为凡事都有两面性的,Vue.js 3提供了这么多的响应式能力,运用不当可能带来的不是方便,而是问题,甚至是故障。我在文稿里也提到一些响应式操作使用不恰当时,会带来的其他意想不到的问题。

所以在使用Vue.js 3的响应式API时候,要注意 响应式的作用范围以及官方对个别API的“慎用提醒”,一般我们开发过程中尽可能做到“ 最小可用”原则就好,没必要用的技术特性或者API就尽量慎用或者少用,避免带来不必要的问题。

思考

我们这节课内容都是基于组合式API(Composition API)的开发方式来进行响应式操作,那么如果换成组合式API(Options API)的开发方式,响应式功能的实现应该怎么操作?

完整的代码在这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源码头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值