Skip to content

响应式基础

使用 ref

可以使用 ref 创建一个响应式的数据:

vue
<template>
  <div>{{ name }}</div>
</template>

<script setup>
import { ref } from "vue";
// 现在的 name 就是一个响应式数据
let name = ref("Bill");
console.log(name);
console.log(name.value);
setTimeout(() => {
  name.value = "Tom";
}, 2000);
</script>

<style lang="scss" scoped></style>

ref 返回的响应式数据是一个对象,我们需要通过 .value 访问到内部具体的值。模板中之所以不需要 .value,是因为在模板会对 ref 类型的响应式数据自动解包。

ref 可以持有任意的类型,可以是对象、数组、普通类型的值、Map、Set...

对象的例子:

vue
<template>
  <div>{{ Bill.name }}</div>
  <div>{{ Bill.age }}</div>
</template>

<script setup>
import { ref } from "vue";
// 现在的 name 就是一个响应式数据
let Bill = ref({
  name: "Biil",
  age: 18,
});
setTimeout(() => {
  Bill.value.name = "Biil2";
  Bill.value.age = 20;
}, 2000);
</script>

<style lang="scss" scoped></style>

数组的例子:

vue
<template>
  <div>{{ arr }}</div>
</template>

<script setup>
import { ref } from "vue";
// 现在的 name 就是一个响应式数据
let arr = ref([1, 2, 3]);
setTimeout(() => {
  arr.value.push(4, 5, 6);
}, 2000);
</script>

<style lang="scss" scoped></style>

第二个点,ref 所创建的响应式数据是具备深层响应式,这一点主要体现在值是对象,对象里面又有嵌套的对象:

vue
<template>
  <div>{{ Bill.name }}</div>
  <div>{{ Bill.age }}</div>
  <div>{{ Bill.nested.count }}</div>
</template>

<script setup>
import { ref } from "vue";
// 现在的 name 就是一个响应式数据
let Bill = ref({
  name: "Biil",
  age: 18,
  nested: {
    count: 1,
  },
});
setTimeout(() => {
  Bill.value.name = "Biil2";
  Bill.value.age = 20;
  Bill.value.nested.count += 2;
}, 2000);
</script>

<style lang="scss" scoped></style>

如果想要放弃深层次的响应式,可以使用 shallowRef,通过 shallowRef 所创建的响应式,不会深层地递归将对象每一层转为响应式,而只会将 .value 的访问转为响应式:

js
const state = shallowRef({ count: 1 });
// 这个操作不会触发响应式更新
state.value.count += 2;
// 只针对 .value 值的更改会触发响应式更新
state.value = { count: 2 };

具体示例:

vue
<template>
  <div>{{ Bill.name }}</div>
  <div>{{ Bill.age }}</div>
  <div>{{ Bill.nested.count }}</div>
</template>

<script setup>
import { shallowRef } from "vue";
let Bill = shallowRef({
  name: "Biil",
  age: 18,
  nested: {
    count: 1,
  },
});
// 下面的更新不会触发视图更新
setTimeout(() => {
  Bill.value.name = "Biil2";
  Bill.value.age = 20;
  Bill.value.nested.count += 2;
}, 2000);
// 下面的更新会触发视图更新
setTimeout(() => {
  Bill.value = {
    name: "Biil3",
    age: 30,
    nested: {
      count: 3,
    },
  };
}, 4000);
</script>

<style lang="scss" scoped></style>

使用 nextTick

响应式数据的更新,带来了 DOM 的自动更新,但是这个 DOM 的更新并非是同步的,这意味着当响应式数据发生修改后,我们去获取 DOM 值,拿到的是之前的 DOM 数据:

vue
<template>
  <div id="container">{{ count }}</div>
</template>

<script setup>
import { ref, onMounted } from "vue";
let count = ref(1);
let container = null;
setTimeout(() => {
  count.value = 2; // 修改响应式状态
  console.log("第二次打印:", container.innerText);
}, 2000);
// 这是一个生命周期钩子方法
// 会在组件完成初始渲染并创建 DOM 节点后自动调用
onMounted(() => {
  container = document.getElementById("container");
  console.log("第一次打印:", container.innerText);
});
</script>

<style lang="scss" scoped></style>

如果想要获取最新的 DOM 数据,可以使用 nextTick,这是 Vue 提供的一个工具方法,会等待下一次的 DOM 更新,从而方便后面能够拿到最新的 DOM 数据。

vue
<template>
  <div id="container">{{ count }}</div>
</template>

<script setup>
import { ref, onMounted, nextTick } from "vue";
let count = ref(1);
let container = null;
setTimeout(async () => {
  count.value = 2; // 修改响应式状态
  // 等待下一个 DOM 更新周期
  await nextTick();
  // 这个时候再打印就是最新的值了
  console.log("第二次打印:", container.innerText);
}, 2000);
// 这是一个生命周期钩子方法
// 会在组件完成初始渲染并创建 DOM 节点后自动调用
onMounted(() => {
  container = document.getElementById("container");
  console.log("第一次打印:", container.innerText);
});
</script>

<style lang="scss" scoped></style>

如果不用 async await,那么就是通过回调的形式:

js
setTimeout(() => {
  count.value = 2; // 修改响应式状态
  // 等待下一个 DOM 更新周期
  nextTick(() => {
    // 这个时候再打印就是最新的值了
    console.log("第二次打印:", container.innerText);
  });
}, 2000);

当然还是推荐使用 async await,看上去代码的逻辑更加清晰一些。

使用 reactive

reactive 通常将一个对象转为响应式对象

vue
<template>
  <div>{{ state.count1 }}</div>
  <div>{{ state.nested.count2 }}</div>
</template>

<script setup>
import { reactive } from "vue";
const state = reactive({
  count1: 0,
  nested: {
    count2: 0,
  },
});
setTimeout(() => {
  state.count1++;
  state.nested.count2 += 2;
}, 2000);
</script>

<style lang="scss" scoped></style>

Vue 中的响应式底层是通过 ProxyAPI 来实现的,但是这个 ProxyAPI 只能对对象进行拦截,无法对原始值进行拦截。

这里就会产生一个问题:如果用户想要把一个原始值转为响应式,该怎么办?

两种方案:

  1. 让用户自己处理,用户需要将自己想要转换的原始值包装为对象,然后再使用 reactive API 🙅
  2. 框架层面来处理,多提供一个 API,这个 API 可以帮助用户简化操作,将原始值也能转为响应式数据 🙆

ref 的背后其实也调用了 reactive API

  • 原始值:Object.defineProperty
  • 复杂值:reactive API

reactive 还有一个相关的 API shallowReactiveAPI,是浅层次的,不会深层次去转换成响应式

vue
<template>
  <div>{{ state.count1 }}</div>
  <div>{{ state.nested.count2 }}</div>
</template>

<script setup>
import { shallowReactive } from "vue";
const state = shallowReactive({
  count1: 0,
  nested: {
    count2: 0,
  },
});
setTimeout(() => {
  state.count1++;
}, 2000);
setTimeout(() => {
  state.nested.count2++;
}, 4000);
</script>

<style lang="scss" scoped></style>

使用细节

先说最佳实践:尽量使用 ref 来作为声明响应式数据的主要 API.

reactive 局限性

  1. 使用 reactvie 创建响应式数据的时候,值的类型是有限的
    • 只能是对象类型(object、array、map、set)
    • 不能够是简单值(string、number、boolean)
  2. 第二条算是一个注意点,不能够去替换响应式对象,否则会丢失响应式的追踪
js
let state = reactive({ count: 0 });
// 下面的这个操作会让上面的对象引用不再被追踪,从而导致上面对象的响应式丢失
state = reactive({ count: 1 });
  1. 对解构操作不友好,当对一个 reactvie 响应式对象进行解构的时候,也会丢失响应式
js
let state = reactive({ count: 0 });
// 当进行解构的时候,解构出来的是一个普通的值
let { count } = state;
count++; // 这里也就是单纯的值的改变,不会触发和响应式数据关联的操作

// 另外还有函数传参的时候
// 这里传递过去的也就是一个普通的值,没有响应式
func(state.count);

ref 解包细节

所谓 ref 的解包,指的是自动访问 value,不需要再通过 .value 去获取值。例如模板中使用 ref 类型的数据,就会自动解包。

1. ref 作为 reactvie 对象属性

这种情况下也会自动解包

vue
<template>
  <div></div>
</template>

<script setup>
import { ref, reactive } from "vue";
const name = ref("Bill");
const state = reactive({
  name,
});
console.log(state.name); // 这里会自动解包
console.log(name.value);
</script>

<style lang="scss" scoped></style>

如果 ref 作为 shallowReactive 对象的属性,那么不会自动解包

vue
<template>
  <div></div>
</template>

<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill");
const state = shallowReactive({
  name,
});
console.log(state.name.value); // 不会自动解包
console.log(name.value);
</script>

<style lang="scss" scoped></style>

因为对象的属性是一个 ref 值,这也是一个响应式数据,因此 ref 的变化会引起响应式对象的更新

vue
<template>
  <div>
    <div>{{ state.name.value }}</div>
  </div>
</template>

<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill");
const state = shallowReactive({
  name,
});
setTimeout(() => {
  name.value = "Tom";
}, 2000);
</script>

<style lang="scss" scoped></style>

【课堂练习】下面的代码:

  1. 为什么 Bill 渲染出来有双引号?
  2. 为什么 2 秒后界面没有渲染 Smith ?
vue
<template>
  <div>{{ obj.name }}</div>
</template>

<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill"); // name 是一个 ref 值
const obj = shallowReactive({
  name,
});
setTimeout(() => {
  obj.name = "John";
}, 1000);
setTimeout(() => {
  name.value = "Smith";
}, 2000);
</script>

<style lang="scss" scoped></style>

答案:

  1. 因为使用的是 shallowReactive,shallowReactive 内部的 ref 是不会自动解包的
  2. 1 秒后,obj.name 被赋予了 John 这个字符串值,这就使得和原来的 ref 数据失去了联系

如果想要渲染出 Smith,修改如下:

js
import { ref, shallowReactive } from "vue";
const name = ref("Bill"); // name 是一个 ref 值
const obj = shallowReactive({
  name,
});
setTimeout(() => {
  obj.name.value = "John";
}, 1000);
setTimeout(() => {
  name.value = "Smith";
}, 2000);

下面再来看一个例子:

vue
<template>
  <div>{{ obj.name.value }}</div>
</template>

<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill");
const stuName = ref("John");

const obj = shallowReactive({ name });

// 注意这句代码,意味着和原来的 name 这个 Ref 失去关联
obj.name = stuName;

setTimeout(() => {
  name.value = "Tom"; //这个地方视图不会修改
}, 2000);

setTimeout(() => {
  stuName.value = "Smith"; //4s后,视图修改为smith
}, 4000);
</script>

<style lang="scss" scoped></style>

2. 数组和集合里面使用 ref

如果将 ref 数据作为 reactvie 数组或者集合的一个元素,此时是不会自动解包的

js
// 下面这些是官方所给的例子
const books = reactive([ref("Vue 3 Guide")]);
// 这里需要 .value
console.log(books[0].value);

const map = reactive(new Map([["count", ref(0)]]));
// 这里需要 .value
console.log(map.get("count").value);
vue
<template>
  <div></div>
</template>

<script setup>
import { ref, reactive } from "vue";
const name = ref("Bill");
const score = ref(100);
const state = reactive({
  name,
  scores: [score],
});
console.log(state.name); // 会自动解包
console.log(state.scores[0]); // 不会自动解包 RefImpl{}
console.log(state.scores[0].value); // 100
</script>

<style lang="scss" scoped></style>

3. 在模板中的自动解包

在模板里面,只有顶级的 ref 才会自动解包。

vue
<template>
  <div>
    <div>{{ count }}</div>
    <div>{{ object.id }}(会渲染,但没有解包)</div>
  </div>
</template>

<script setup>
import { ref } from "vue";
const count = ref(0); // 顶级的 Ref 自动解包
const object = {
  id: ref(1), // 这就是一个非顶级 Ref 不会自动解包
};
</script>

<style lang="scss" scoped></style>

上面的例子,感觉非顶级的 Ref 好像也能够正常渲染出来,仿佛也是自动解包了的。

但是实际情况并非如此。

vue
<template>
  <div>
    <div>{{ count + 1 }}</div>
    <div>{{ object.id + 1 }}</div>
  </div>
</template>

<script setup>
import { ref } from "vue";
const count = ref(0); // 顶级的 Ref 自动解包
const object = {
  id: ref(1), // 这就是一个非顶级 Ref 不会自动解包
};
</script>

<style lang="scss" scoped></style>

例如我们在模板中各自加 1 就会发现上面因为已经解包出来了,所以能够正常的进行表达式的计算。

但是下面因为没有解包,意味着 object.id 仍然是一个对象,因此最终计算的结果为 [object Object]1

因此要访问 object.id 的值,没有自动解包我们就手动访问一下 value

vue
<template>
  <div>
    <div>{{ count + 1 }}</div>
    <div>{{ object.id.value + 1 }}</div>
  </div>
</template>

-EOF-

MIT License