# Vue 组合式 API - setup、reactive 与 ref,响应式工具

TIP

组合式 API(Composition API)是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

组合式 API 是 Vue 3 及 Vue 2.7 (opens new window) 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api (opens new window)

在 Vue 3 中,组合式 API 基本上都会配合 <script setup> (opens new window) 语法在单文件组件中使用。

下面是一个使用组合式 API 的组件示例:

<script setup>
  import { ref, reactive, computed } from "vue";

  // 创建响应式数据 相当于选项式API中 data中定义的属性
  const msg = ref("Hello Composition API");
  const num = ref(100);

  // 定义一个方法,相当于选项式API中的methods选项中定义的方法
  function update() {
    num.value = 200;
  }

  // 创建一个计算属性,相当于选项式API中的computed选项中创建的计算属性
  const price = computed(() => {
    return "$" + num.value;
  });
</script>
<template>
  <h3>{{ msg }}</h3>
  <p>价格:{{ price }}</p>
  <button @click="update">更新</button>
</template>

注:

  • 从上面代码中可以看到,创建的响应式数据,methods 方法,computed 计算属性都是基于函数来实现的。这些函数组合在一起就能完成一个具体的应用。
  • 所谓组合式 API 就是指这些不同的 API(函数)组合在一起来实现一个完整的应用,和之前学习的选项式 API 风格完全不同。

以上代码最终渲染结果如下:

GIF2023-7-818-26-01

TIP

接下来我们将正式开启组合式 API 的学习之旅,本章节具体内容安排如下:

  • 初识setup()函数
  • 初始reactiveref响应式 API
  • 深入响应式 API- 工具函数(一)
  • 深入响应式 API- 工具函数(二)
  • setup()函数参数

# 一、初识 setup() 函数

TIP

setup() 函数是在组件中使用组合式 API 的入口(表演的舞台),所有组合式 API 代码都写在setup()函数中

<script>
  export default {
    // setup函数是所有组合式API的入口(表演的舞台)
    setup() {
      // .....所有代码在此书写
    },
  };
</script>

# 1、setup() 函数的返回值

TIP

setup()函数的返回值通常是一个对象,这个对象的所有属性会暴露给组件模板和组件实例,所以

  • 在组件的模板中可以直接访问setup()返回对象的属性
  • 在组件的选项式 API中可通过组件实例可以访问setup()返回对象身上的属性。

以下代码展示了,在模板中和组件实例上访问到setup()函数返回的对象身上的属性和方法。

提示: 选项式 API 与组合式 API 是可以共存的。

<script>
  export default {
    setup() {
      // 返回值会暴露给模板和其他的选项式 API 钩子
      return {
        message: "Hello Vue!!",
        sayHello: () => console.log("sayHello"),
      };
    },
    beforeCreate() {
      console.log(this.message); // 组件实例可以直接获取message属性
    },
  };
</script>
<template>
  <button @click="sayHello">sayHello</button>
  <!--模板中可以直接使用message属性-->
  <div>{{ message }}</div>
</template>

最终渲染效果如下:

GIF2023-7-814-43-14

# 2、setup() 函数中 this 指向

TIP

  • setup()函数自身并不含对组件实例的访问权,在setup()中访问this会是undefind
  • 所以setup()函数内是没有办法访问到选项式 API 中的属性、方法、计算属性等。
<!-- 以下代码是错的 -->
<script>
  export default {
    data() {
      return {
        message: "Hello Vue!!",
      };
    },
    setup() {
      console.log(this); // undefined
      console.log(this.message); // 抛出错误,因为this为undefined
    },
  };
</script>

# 3、setup() 函数执行时机

TIP

setup()函数是在 Vue 生命周期函数beforeCreate()之前被自动调用的。

所以在beforeCreate()函数和选项式 API 中可以访问到setup()函数对外暴露的属性

<script>
  export default {
    data() {
      return {
        text: this.message,
      };
    },
    setup() {
      return {
        message: "Hello Vue!!",
      };
    },
    beforeCreate() {
      // 在控制台输出 : beforeCreate: Hello Vue!!
      console.log("beforeCreate:", this.message);
    },
  };
</script>
<template>
  <!--以下代码渲染后效果: <div> Hello Vue!! </div>-->
  <div>{{ text }}</div>
</template>

image-20230522151358423

# 4、setup() 函数暴露非响应式属性

TIP

以下方式setup()函数对外暴露的属性非响应式的,当属性的值发生变化时,页面并不会同步更新。

<script>
  export default {
    setup() {
      let a = 1;
      let b = 2;
      function update() {
        a = 10;
        b = 20;
      }
      return {
        a,
        b,
        update,
      };
    },
  };
</script>
<template>
  <div>a的值:{{ a }}</div>
  <div>b的值:{{ b }}</div>
  <button @click="update">更新a,b的值</button>
</template>

GIF2023-5-2215-22-16

注:

以上的 a,b 属性都是非响应式的,所以当我们点击按扭时,页面中 a,b 的值并没有变化

如果想要对外暴露的属性支持响应式,需要用到响应式 API 中的reactive()ref()方法来实现。

# 5、总结

TIP

  • setup()函数返回值通常是一个对象,这个对象的所有属性会暴露给组件模板和组件实例
  • setup()函数中的this指向undefined
  • setup()函数会在所有生命周期函数beforeCreate之前被执行。
  • setup()函数内定义的变量默认为非响应式的,所以对外暴露该属性为非响应式

# 二、初始 reactive 与 ref 响应式 API

TIP

本小节我们将会初步认识reactiveref 两个响应式 API,掌握他们的基本用法。

# 1、初识 reactive() 方法

TIP

reactive()方法用来返回一个对象的响应式代理。

const objProxy = reactive(obj); // objProxy为obj对象的响应式代理

我们通过响应式代理来操作对象的属性,当属性的值发生变化时也会驱动页面视图的更新。

<script>
  // 导入reactive方法
  import { reactive } from "vue";
  export default {
    setup() {
      const obj = { a: 1, b: 2 };
      // Info为响应式代理对象
      const objProxy = reactive(obj);
      console.log(objProxy); // Proxy(Object) {a: 1, b: 2}

      function update() {
        // 修改对象属性的值
        objProxy.a = 10;
        objProxy.b = 20;
      }
      // 将属性暴露给组件实例
      return {
        objProxy,
        update,
      };
    },
  };
</script>
<template>
  <div>a的值:{{ objProxy.a }}</div>
  <div>b的值:{{ objProxy.b }}</div>
  <button @click="update">更新a,b的值</button>
</template>

以上代码最终渲染效果如下,当点击按扭更新 a、b 值时,页面中 a、b 的值会更新为最新的。

GIF2023-5-2215-51-30

代码分析

  • 以上代码中的objProxyobj对象的响应式代理对象。
  • 我们可以通过objProxy.a=10objProxy.b=20来为obj对象的 a、b 属性赋值。
  • 当点击按扭后,会调用 update 方法,更新对象的属性的值。最终 a、b 属性的值从 1,2 修改成 10,20。因为数据是响应式的,所以数据更新后页面视图也发生了更新

以上过程证明objProxy代理对象具有响应性,所以objProxy为响应式代理对象。

# 1.1、深层响应性

TIP

通过reactive()方法转换的响应式对象是 "深层响应" 的。

即不管对象的属性嵌套有多深,都具有响应性,其值发生变化时,页面视图会同步更新。

<script>
  import { reactive } from "vue";
  export default {
    setup() {
      // info为响应式代理对象
      const info = reactive({
        a: {
          b: {
            c: [1, 2, 3],
          },
        },
      });
      function add() {
        info.a.b.c.push("A");
      }
      return {
        info,
        add,
      };
    },
  };
</script>

<template>
  <ul>
    <li v-for="item in info.a.b.c">{{ item }}</li>
  </ul>
  <button @click="add">添加</button>
</template>

以上代码渲染后效果如下:

GIF2023-5-2217-16-08

当点击按扭后,往数组中添加了“A",页面同步更新渲染出来了。

# 1.2、reactive() 无法转换基本数据类型

TIP

  • reactive()方法只能将一个对象转换为一个响应式对象,而不能将一个基本数据类型转换为响应式对象。
  • 因为reactive()方法的底层采用的是Proxy来实现的,而Proxy只能创建对象的代理。

以下代码为错误示例

import { reactive } from "vue";
export default {
  setup() {
    let msg = reactive("Hello Vue!!");
    console.log(msg);
  },
};

以上代码执行后,在控制台打印如下结果:

image-20230522161131916

注:

警告提示,值不能被设置为响应式,最后 msg 的结果仍为原始值"Hello Vue!!"

# 1.3、reactive() 方法底层实现原理

TIP

reactive()方法返回的响应式代理对象本质是Proxy()的实例。

const objProxy = reactive(obj);

以上代码的内部实现如下:(以下为极简版, 主要帮助大家理解响应式代理)

function reactive(obj) {
  // 判断obj是不是对象,是对象才可以利有Proxy实现代理
  if (obj !== null && typeof obj === "object") {
    // objProxy响应式代理对象
    return new Proxy(obj, {
      // 当访问objProxy对象身上的属性时,get方法会被调用
      get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);
        track(); // 响应式依赖收集 ,收集属性对应的DOM更新代码
        return res;
      },
      // 当给objProxy对象向上的属性赋值时,set方法会被调用
      set(target, key, value, receiver) {
        Reflect.set(target, key, value, receiver);
        trigger(); // 触发响应式依赖 更新DOM
      },
      // ....
    });
  }
}

# 2、初识 ref() 方法

TIP

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

  • 可更改是指你可以为ref对象的value属性赋予新的值
  • 响应式是指所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用(DOM 更新)
<script>
  import { ref } from "vue";
  export default {
    setup() {
      // msg为一个ref对象,并且是响应式的
      const msg = ref("Hello Vue!!"); // {value: "Hello Vue!!"}

      // 修改ref对象value属性的值
      msg.value = "Hello ref!!";

      return {
        msg,
      };
    },
  };
</script>
<template> {{ msg }} </template>

注:

以上代码中的字符串"Hello Vue!!"ref()方法转换成了一个 ref 对象{value: "Hello Vue!!"}

当我们修改这 value 属性的值时,页面视图中用到该数据的地方会同步更新。

以上代码最终渲染结果如下:

image-20230708154959482

# 2.1、访问 ref 对象的 value 属性

TIP

  • setup()中访问ref对象的value值时,需要.value的形式来访问
  • 在组件模板中访问ref对象的value值时,它会自动浅层解包(会自动调用ref.value),因此你无须再在模板中为它写 .value
  • 在选项式 API 中,通过 this 访问 ref 时也会自动浅层解包
使用场景 是否自动浅层解包
setup()函数
组件模板
选项式 API

代码示例

<script>
  // 导入 ref 方法
  import { ref } from "vue";
  export default {
    setup() {
      // 将"Hello Vue!!" 转换为响应式,可更改的ref对象
      const msg = ref("Hello Vue!!");
      console.log(msg);
      console.log("setup--", msg.value);
      // 更新msg方法
      function update() {
        // 修改msg的值
        msg.value = "Hello ref!!!";
      }
      return {
        msg,
        update,
      };
    },
    mounted() {
      // msg是一个ref对象,但会自动解包,所以不需要通过this.msg.value来获取值
      console.log("mounted", this.msg);
    },
  };
</script>
<template>
  <button @click="update">更新</button>
  <!--msg会自动解包,不需要使用 msg.value-->
  <div>{{ msg }}</div>
</template>

以上代码最终渲染效果如下:

GIF2023-5-1722-12-19

# 2.2、ref() 方法转换对象

TIP

  • ref()方法的参数如果为一个对象(引用类型),则最终返回的ref对象的value属性值为该对象的响应式代理对象。
  • 该 ref 对象是一具有深层响应式,可更改的ref对象。
<script>
  import { ref } from "vue";
  export default {
    setup() {
      const obj = { msg: "Hello ref!!" };
      /*
            	ref的参数为一个对象,先用reactive()方法返回obj的响应式代理对象
            	然后将该响应式代理对象赋值给到ref对象的value属性。
            */
      const refProxyObj = ref(obj);

      console.log(refProxyObj);
      console.log(refProxyObj.value);
    },
  };
</script>

以上代码,最终在控制台输出如下结果:

image-20230522195240987

分析输出结果

ref()方法的参数如果为一个对象,其内部相当于经历了以下两步:

  • 先用reactive(obj)方法返回obj的响应式代理对象 proxyObj
  • 然后将该响应式代理对象赋值给到 ref 对象的 value 属性,即{value:proxyObj}

所以最终得到的 ref 对象是一个深层响应式,可更改的ref对象。

代码演示

<script>
  import { ref } from "vue";
  export default {
    setup() {
      const userInfo = ref({
        userName: "清心",
        sex: ref("女"),
        a: {
          b: ref(0),
        },
      });
      function update() {
        userInfo.value.userName = "icoding";
        //sex a.b 都会一个ref对象,但都会自动解包
        userInfo.value.sex = "男";
        userInfo.value.a.b = 100;
        // 下面这种写法,也支持响应式
        // userInfo.value.a={b:100}
      }
      return {
        userInfo,
        update,
      };
    },
  };
</script>
<template>
  <button @click="update">更新</button>
  <!--使用时,全都会自动解包-->
  <div>{{ userInfo.userName }} -- {{ userInfo.sex }}</div>
  <div>{{ userInfo.a.b }}</div>
</template>

GIF2023-5-1814-40-06

注:

当数据的结构比较深时,深层响应式是非常消耗性能的,因为需要对对象进行深度递归,将每一个被嵌套的属性都转换为响应式。

若要避免这种深层次的转换,请使用 shallowRef()来替代。

# 2.3、ref() 方法的底层实现

TIP

以上代码中的ref()方法,返回的ref对象,底层实现如下(极简版,不完整,仅供了解思路)

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, "value");
      return value;
    },
    set value(newValue) {
      value = newValue;
      trigger(refObject, "value");
    },
  };
  return refObject;
}

注:

  • 当我们访问 ref 对象的 value 属性时,本质是触发了 value 属性的 get 方法。
  • 当我们给 ref 对象赋值时,本质是触发了 value 属性的 set 方法。

# 3、总结

TIP

reactive 方法

  • 用来返回一个对象的响应式代理对象,其响应性为深层响应,不管对象的属性嵌套有多深,都具有响应性,其值发生变化时,页面视图会同步更新。
  • reactive 方法无法转换基本数据类型
  • reactive 方法的底层实现是通过proxy代理来实现的

ref 方法

  • 主要用来将一个基本数据类型值转换为一个响应式,可更改的 ref 对象。
  • 访问 ref 对象的属性值时,如果在 setup()函数中,需要打点.value属性才能访问,如果在模板或选项式 API 中,则会自动浅层解包,不需要.value
  • ref 方法的参数如果为对象类型,则返回的 ref 对象的 value 属性值为该对象的 proxy 响应式代理对象。
  • ref 方法的底层实现是通过创建一个具有 get value 与 set value 属性的对象来实现。

# 三、深入响应式 API - 工具函数(一)

TIP

通过前面的学习我们知道reactive()方法的返回值为一个对象的响应式代理。本小节我们针对响应式代理对象做深入的学习,主要内容如下:

  • 响应式对象会自动解包 ref 属性
  • 不能被自动解包的集合
  • 解构响应式对象
  • toRef() 方法
  • toRefs() 方法
  • shallowReactive()方法
  • readonly() 方法

注:我们将响应式代理对象简称为响应式对象

# 1、响应式对象会自动解包 ref 属性

TIP

一个响应式对象的属性及嵌套属性的值如果为ref属性,在模板或setup()中使用时,会自动解包,同时保持响应性

<script>
  import { reactive, ref } from "vue";

  export default {
    setup() {
      const userName = ref("艾编程");
      const age = ref(33);
      const userInfo = reactive({
        userName, // ref对象
        age, // ref对象
        sex: "男",
        test: {
          a: {
            b: ref(3), // ref对象
          },
        },
      });
      function update() {
        // userName会自动解包  所以写成 userInfo.userName.value="清心" 是错误的
        userInfo.userName = "清心"; // 与 userName.value="清心" 一样支持响应式
        userInfo.age = 44; // 与  age.value = "44" 一样支持响应式
        userInfo.sex = "女";
        userInfo.test.a.b = 100; // 深层响应式
      }
      return {
        userInfo,
        update,
      };
    },
  };
</script>

<template>
  <button @click="update">更新数据</button>
  <!--userInfo的属性为ref对象时,会自动解包-->
  <div>{{ userInfo.userName }} -{{ userInfo.age }}--{{ userInfo.sex }}</div>
  <div>{{ userInfo.test.a.b }}</div>
</template>

以上代码渲染后效果如下:

GIF2023-5-1722-57-04

# 2、不能被自动解包的集合

TIP

当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

const hobbies = reactive([ref("音乐"), ref("阅读"), ref("跳舞")]);
// 这里需要 .value
console.log(hobbies[0].value);

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

# 3、解构响应式对象

TIP

如果我们直接利用解构赋值来解构响应式对象

  • 当属性值为基本数据类型或 ref 对象(解包后为基本数据类型)时,解构后会失去响应性
  • 当属性值为引用数据类型或 ref 对象(解包后为引用数据类型)时,解构后仍保持响应性
<script>
  import { reactive, ref } from "vue";

  export default {
    setup() {
      // 解构响应式对象
      let { a, b, c, d } = reactive({
        a: ref("a"),
        b: ref(["b"]),
        c: ["c"],
        d: "d",
      });
      console.log(a); // 'a' 基本数据类型,失去响应式
      console.log(b); // Proxy(Array) {0: 'b'} 响应式对象,具有响应性
      console.log(c); // Proxy(Array) {0: 'c'} 响应式对象,具有响
      console.log(d); // 'd'  基本数据类型,失去响应式

      // 更新数据
      function update() {
        a = "aa";
        b[0] = "bb";
        c[0] = "cc";
        d = "dd";
      }

      return {
        a,
        b,
        c,
        d,
        update,
      };
    },
  };
</script>

<template>
  <div>a:{{ a }}--b:{{ b[0] }}--c:{{ c[0] }}--{{ d }}</div>
  <button @click="update">更新</button>
</template>

最终渲染效果如下图:

GIF2023-7-817-08-42

注:

观察控制台输出的结果,可以看到解构后

  • b 和 c 的值为 proxy 响应式代理对象,具有响应性
  • a 和 d 变量的值为基本数据类型,不具有响应性。

所以,点击更新后按扭后,b,c 的值更新后,页面中对应的值也被更新了。


TIP

如果解构的响应式对象为数组或 Map 等原生集合类型

  • 只有集合中成员为基本数据类型时会失去响应性
  • 集合成员为ref类型与引用类型都不会失去响应性,因为ref成员不会自动解包
<script>
  import { reactive, ref } from "vue";

  export default {
    setup() {
      // 解构响应式对象
      let [a, b, c, d] = reactive([ref("a"), ref(["b"]), ["c"], "d"]);
      console.log(a); // {value:'a'} ref对象,具有响应性
      console.log(b); // {value:Proxy(Array){0:'b'}} ref对象,具有响应性
      console.log(c); // Proxy(Array) {0: 'c'} 响应式对象,具有响性
      console.log(d); // 'd'  基本数据类型,失去响应式

      // 更新数据
      function update() {
        a.value = "aa";
        b.value[0] = "bb";
        c[0] = "cc";
        d = "dd";
      }

      return {
        a,
        b,
        c,
        d,
        update,
      };
    },
  };
</script>

<template>
  <div>a:{{ a }}--b:{{ b[0] }}--c:{{ c[0] }}--{{ d }}</div>
  <button @click="update">更新</button>
</template>

以上代码最终渲染结果如下:

GIF2023-7-817-22-18

注:

观察上图控制台打印的结果,可以看到

  • 变量 a,b 为 ref 对象,具有响应性
  • 变量 c 为 proxy 代理对象,具有响应性
  • d 变量为基本数据类型,不具有响应性

所以点击更新按扭后,a,b,c 变量的值更新后,页面也同步更新了。

那我们如何保持解构后的变量都具有响应性呢 ?这就需要用到接下来讲到的toRef()toRefs()方法

# 4、toRef() 方法

TIP

toRef()方法可以基于响应式对象上的一个属性,创建一个对应的 ref。

这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

语法:

const variable = toRef(proxyObject, key, defaultValue);
  • proxyObject 为响应式对象
  • key 为 响应式对象对应的原对象的属性
  • defaultValue 默认值,当转换对象身上不存在的值是会返回 undefined,如果有默认值,则会使用默认值

代码演示

<script>
  import { reactive, toRef } from "vue";
  export default {
    setup() {
      const obj = { a: 1, b: 2 };
      // obj为响应式代理对象
      const objProxy = reactive(obj);

      // toRef()方法将对象obj的属性a转换为一个响应式的ref对象 {value:1}
      let a = toRef(objProxy, "a");

      console.log(a); // {value:1}
      console.log(a.value); // 访问a的值

      // objProxy.a=10 时,对应a.value的值也变成了10,两者保持同步
      objProxy.a = 10;
      setTimeout(() => {
        // a.value=100时,对应objProxy.a的值也变成100,两者保持同步
        a.value = 100;
      }, 2000);

      return {
        a,
      };
    },
  };
</script>
<template>
  <div>{{ a }}</div>
</template>

以上代码最终渲染结果如下:

GIF2023-7-923-42-52

注:

通过渲染后的结果可以看到,objProxy.a = 10a.value = 100最终都会影响到 a 变量的值,也可以说影响到objProxy.a的值,同时都具有响应性。

# 4.1、toRef() 注意事项

  • toRef() 如果传入的对象非响应式的,则返回该对象上指定属性的ref对象。(只做了解)
let a = toRef({ a: 1 }, "a"); // {value:1}
// 访问a的值
console.log(a.value); // 1
  • toRef()转换一个对象身上不存在的属性时,返回的 ref 对象的 value 值为undefind,可以为其指定默认值
const objProxy = reactive({ a: 1, b: 2 });
// toRef()方法将对象{a:1,b:2}的属性a转换为一个响应式的ref对象
let c = toRef(objProxy, "c");
// 访问a的值
console.log(c.value); // undefined

c = toRef(objProxy, "c", "默认值");
console.log(c.value); // 默认值
  • 利用toRef()也可以返回一个只读的ref对象,当我们想创建一个只读的变量时可以使用
import { reactive, toRef } from "vue";
const obj = { a: 1, b: 2 };
// obj为响应式代理对象
const objProxy = reactive(obj);
// 返回值为只读的ref对象
let a = toRef(() => objProxy);
// 访问a的值
console.log(a.value);
// 以下赋值操作将抛出错误
// a.value = "ssss"

# 5、toRefs() 方法

TIP

  • 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。
  • 每个单独的 ref 都是使用 toRef() (opens new window) 创建的。

语法

toRefs(object); // object为响应式对象

如果我们想要一次性解构一个响应式对象的所有属性,则可以使用toRefs()

import { reactive, ref, toRefs } from "vue";
export default {
  setup() {
    const objProxy = reactive({ a: 1, b: 2 });
    const obj = toRefs(objProxy);
    console.log(obj); // {a:toRef(objProxy,"a"),b:toRef(objProxy,"b")}
    const { a, b } = obj;
    // a,b为ref对象,所以访问需要.value
    console.log(a.value, b.value); // 1  2
  },
};

代码演示

<script>
  import { reactive, ref, toRefs } from "vue";

  export default {
    setup() {
      const userInfo = reactive({
        userName: ref("艾编程"), // ref对象
        age: ref(33), // ref对象
        sex: "男",
        test: {
          a: {
            b: 3,
          },
        },
      });

      // toRefs()转换响应式对象,然后利用解构赋值解构属性
      let { userName, age, sex, test } = toRefs(userInfo);
      // 因为解构后的每个属性为ref对象,所以需要通过.value形式访问
      console.log("userName", userName.value); // userName 艾编程
      console.log("age", age.value); // age 33
      console.log("sex", sex.value); // sex 男
      console.log("test", test.value); // proxy对象

      function update() {
        userName.value = "清心"; // 失去响应性
        age.value = "44"; // 失去响应性
        sex.value = "女"; // 失去响应性
        test.value.a.b = 100; // 保持响应性
      }
      return {
        userName,
        age,
        sex,
        test,
        update,
      };
    },
  };
</script>

<template>
  <button @click="update">更新数据</button>
  <!--userInfo的属性为ref对象时,会自动解包-->
  <div>{{ userName }} -{{ age }}--{{ sex }}</div>
  <div>{{ test.a.b }}</div>
</template>

以上代码最终渲染效果如下:

GIF2023-5-2222-25-47

注意事项

toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef (opens new window)

# 6、shallowReactive() 方法

TIP

  • shallowReactive()相当于是reactive()的浅层作用形式。
  • 也就是shallowReactive()转换的响应式对象只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
<script>
  import { reactive, ref, shallowReactive } from "vue";
  export default {
    setup() {
      const info = shallowReactive({ count: ref(0), a: { b: 1 } });

      function update1() {
        // ref不会自动解包,需要调用value,值修改后会触发
        info.count.value = 100;
      }
      function update2() {
        // 并不会触发页面更新
        info.a.b = 100;
        // 以下方式会触发页面更新
        // info.a = { b: 100 }
      }

      return {
        info,
        update1,
        update2,
      };
    },
  };
</script>

<template>
  <button @click="update1">更新count</button>
  <button @click="update2">更新b的值</button>
  <div>{{ info.count }}</div>
  <div>{{ info.a.b }}</div>
</template>

以上代码最终渲染效果如下:

GIF2023-5-1815-55-26

# 7、readonly() 方法

TIP

接受一个对象 (不论是响应式还是普通的) 或是一个 ref (opens new window),返回一个原值的只读代理。

只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,会自动解包,但解包得到的值是只读的。

要避免深层级的转换行为,请使用 shallowReadonly() (opens new window) 作替代。

<script>
  import { reactive, readonly } from "vue";

  export default {
    setup() {
      const proxyObj = reactive({ a: 1, b: { c: 2 } });
      const readonlyObj = readonly(proxyObj);
      // 对象属性可读
      console.log(readonlyObj.a); // `
      console.log(readonlyObj.b.c); // 2
      // 以下对象属性赋值失败,同时在控制台抛出错误
      readonlyObj.a = 100;
      readonlyObj.b.c = 200;
    },
  };
</script>

# 8、isReactive() 方法

TIP

检查一个对象是否是由 reactive()shallowReactive() 创建的代理。

返回值为boolean布尔类型值,true表示是,false表示否

<script>
  import { reactive, isReactive, shallowReactive } from "vue";

  export default {
    setup() {
      const obj = { a: 1 };
      console.log(isReactive(obj)); // false
      console.log(isReactive(reactive(obj))); // true
      console.log(isReactive(shallowReactive(obj))); // true
    },
  };
</script>

# 四、深入响应式 API - 工具函数(二)

TIP

通过前面ref()方法的学习,我们对 ref 对象有了初步的了解,本小节我们将深入展开ref对象的学习。

本小节主要包含如下内容:

  • shallowRef()方法,创建浅层的 ref 对象
  • isRef()方法,检查某个值是否为 ref 对象
  • unref()方法,获取 ref 属性值的一种语法糖
  • customRef()方法,自定义 ref

# 1、shallowRef() 方法

TIP

shallowRef()方法相当于ref()的浅层作用形式,只对.value的访问是响应式的,对对象的其它属性值的写操作不支持响应式。

针对大型的数据结构,如果我们确实只需要浅层次的响应式,则可以利用shallowRef()帮助我们提升性能,因为减小了深层次的递归操作。

<script>
  import { ref, shallowRef } from "vue";
  export default {
    setup() {
      const state = shallowRef({ count: 1 });

      function update1() {
        // 不会触发页面的更新
        state.value.count = 100;
      }
      function update2() {
        // 会触发页面的更新
        state.value = { count: 200 };
      }
      return {
        state,
        update1,
        update2,
      };
    },
  };
</script>
<template>
  <button @click="update1">更新1</button>
  <button @click="update2">更新2</button>
  <div>{{ state.count }}</div>
</template>

以上代码最终效果如下:

GIF2023-5-1814-57-56

注:

点击更新 1 并不会触发页面的更新,只有点击更新 2 时,才会触发页面的更新。

因为 state 是被shallowRef转换的的一个浅层的响应式对象,所以state.value.count = 100并不会触发页面的更新,而state.value = { count: 200 }会触发页面的更新。

# 2、isRef() 方法

TIP

检查某个值是否为 ref 对象,如果是返回 true,否则返回 false

import { isRef, ref } from "vue";

export default {
  setup() {
    console.log(isRef(ref(0))); // true
    console.log(isRef(0)); // false
  },
};

# 3、unref() 方法

TIP

unref()方法的参数是 ref,则返回 ref 对象的 value 属性值,否则返回参数本身。

unref()方法是val = isRef(val) ? val.value : val计算的一个语法糖

import { isRef, unref, ref } from "vue";

export default {
  setup() {
    console.log(unref(ref("Hello"))); // Hello
    console.log(unref("Hello")); // Hello
    console.log(unref(ref({ a: 1, b: 2 }))); // Proxy(Object) {a: 1, b: 2}
    console.log(unref({ a: 1, b: 2 })); // {a: 1, b: 2}
  },
};

# 4、customRef() 自定义 ref

TIP

customRef()方法用来创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

语法

let value=0;
// 返回值为ref对象
const refObj=customRef((track,trigger)=>{
    // 返回一个对象
    return {
        get(){
            // 获取ref的值时,会调用get方法
            return value;
        },
        set(newValue){
            // 修改ref的值时,会调用set方法
            value=newValue
        }
    }
}

注:

为了帮助大家更好的理解customRef()的用法,大家可以先按以下思路来创建一个自定义的 ref,实现数据与视图的同步更新。

第一步:创建 createRef()函数

  • customRef()方法的返回值为一个ref,该方法接受一个回调函数作为参数。
  • createRef(value)方法直接将customRef()方法的返回值返回,即返回值为 ref
  • 参数value为需要转换为ref的原始值
  • 以下代码中count为原始值 0 转换后的 ref 对象
// createRef为自定义创建ref的方法,接受一个参数value,value为需要转换为ref的原始值
function createRef(value) {
  return customRef(() => {});
}

// 使用createRef将原始值 0 创建成 ref,即count为ref对象
const count = createRef(0);

第二步:customRef()回调函数的返回值

  • customRef()方法的回调函数返回一个带 get 和 set 的对象
  • 当访问 count 的值(count.vaue)时,会调用 get 方法获取到对应的值
  • 当修改 count 的值(count.value=100)时,会调用 set 方法来更新对应的值
function createRef(value) {
  return customRef(() => {
    return {
      // 当访问ref对象的value值时,会调用get方法
      get() {
        console.log(`get被调用了,此时返回的value值为:${value}`);
        return value;
      },
      // 当给ref对象的value重新赋值时,会调用set方法
      set(newValue) {
        value = newValue;
        console.log(`set被调用了,此时value的值更新为:${value}`);
      },
    };
  });
}

// 使用createRef将原始值 0 创建成 ref,即count为ref对象
const count = createRef(0);
// 读取ref中的值
console.log(count.value);
// 更新ref的value值
count.value = 100;

第三步:在组件模板中使用 count

  • 在组件模板中使用自定义的 ref 对象 count
  • 定义 update 方法,当点击更新按扭时,调用该方法更新 count 的值
<script>
  import { customRef } from "vue";

  export default {
    setup() {
      function createRef(value) {
        return customRef(() => {
          return {
            // 当访问ref对象的value值时,会调用get方法
            get() {
              console.log(`get被调用了,此时返回的value值为:${value}`);
              return value;
            },
            // 当给ref对象的value重新赋值时,会调用set方法
            set(newValue) {
              value = newValue;
              console.log(`set被调用了,此时value的值更新为:${value}`);
            },
          };
        });
      }

      // 使用createRef将原始值 0 创建成 ref,即count为ref对象
      const count = createRef(0);
      // 读取ref中的值
      console.log(count.value);

      function update() {
        // 更新ref的value值
        count.value = 100;
      }

      // 对外暴露属性与方法
      return {
        count,
        update,
      };
    },
  };
</script>

<template>
  <button @click="update">更新myRef的值</button>
  <div>myRef的值:{{ count }}</div>
</template>

以上代码最终渲染效果如下:

GIF2023-7-913-52-10

分析执行结果

  • 当我们点击对应的更新按扭更新count的值为 100 时,set 方法被调用了,但页面视图并没有同步更新。
  • 因为我们并没有在get 中收集依赖(建立对应属性与相关 DOM 的依赖,让 Vue 知道数据更新后要更新那些 DOM)
  • 也没有在set 方法中触发依赖(根据 get 方法中建立的依赖关系,触发 DOM 的更新)

第四步:customRef()回调函数的tracktrigger参数

customRef((track, trigger) => { })回调函数的两个参数

  • track()方法用来在 get 方法被调用时,收集依赖
  • trigger() 方法用来在 set 方法被调用时,触发依赖
<script>
  import { customRef } from "vue";

  export default {
    setup() {
      function createRef(value) {
        return customRef((track, trigger) => {
          return {
            // 当访问ref对象的value值时,会调用get方法
            get() {
              console.log(`get被调用了,此时返回的value值为:${value}`);
              track();
              return value;
            },
            // 当给ref对象的value重新赋值时,会调用set方法
            set(newValue) {
              value = newValue;
              trigger();
              console.log(`set被调用了,此时value的值更新为:${value}`);
            },
          };
        });
      }

      // 使用createRef将原始值 0 创建成 ref,即count为ref对象
      const count = createRef(0);
      // 读取ref中的值
      console.log(count.value);
      function update() {
        // 更新ref的value值
        count.value = 100;
      }

      return {
        count,
        update,
      };
    },
  };
</script>

<template>
  <button @click="update">更新myRef的值</button>
  <div>myRef的值:{{ count }}</div>
</template>

以上代码最终渲染效果如下:

GIF2023-5-2314-52-23

# 4.1、总结:customRef() 方法使用流程

第一步:

  • 创建一个自定义函数,该函数接受一个参数,这个参数为需要转换为 ref 的原始值
  • 函数的返回值为customRef()方法的返回值 ref
function createRef(value) {
  return customRef(() => {});
}

第二步:

  • 创建 customRef 回调函数的返回值,返回值为一个带有 get 和 set 的对象。
  • 当访问 ref 的 value 值时调用 get 方法,所有在此时需要处理的相关逻辑都可以写在 get 中
  • 当修改 ref 的 value 值时调用 set 方法,所有在此时南要处理的相关链辑都可以写在 set 中
function createRef(value) {
  return customRef(() => {
    return {
      get() {
        return value;
      },
      set(newValue) {
        value = newValue;
      },
    };
  });
}

第三步:

  • 在 get 方法返回值前调用track()方法,收集依赖
  • 在 set 方法更新值之后调用trigger()方法,触发依赖
function createRef(value) {
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        value = newValue;
        trigger();
      },
    };
  });
}

通过前面的学习,我们知道如何创建自定义 ref,那自定义 ref 应该在什么场景下使用呢?

注:自定义 ref 使用场景

  • 当我们在获取数据或更新数据时需要做一些额外的操作,就可以用自定义 ref。
  • 比如:页面视图的更新并不需要同步更新,而是数据在 200ms 内如果没有连续更新,则再更新视图。

接下来,我们就来学习下 Vue 官方提供的《自定义防抖 ref》案例

# 5、实战应用:自定义防抖 ref

TIP

当在输入框中连续输入内容的间隔时间超过500ms,则更新 h3 标签显示的内容,否则不更新。

如下:

GIF2023-5-2315-39-31

<script>
  import { customRef } from "vue";

  export default {
    setup() {
      function useDebouncedRef(value, delay = 500) {
        let timer = null; // 用来保存定时器
        return customRef((track, trigger) => {
          return {
            get() {
              track();
              return value;
            },
            set(newValue) {
              // 间隔时间不够500ms,则清除上一次定时器,重新计时
              clearTimeout(timer);
              timer = setTimeout(() => {
                value = newValue;
                trigger();
              }, delay);
            },
          };
        });
      }
      // 创建 ref
      const text = useDebouncedRef("hello");
      // 对外暴露text
      return {
        text,
      };
    },
  };
</script>

<template>
  <input v-model="text" />
  <h3>{{ text }}</h3>
</template>

# 6、总结

  • shallowRef() 用来创建浅层的 ref 对象,只对.value的访问是响应式的,对象的其它均为非响应式
const proxy = shallowRef({ a: 1 });
proxy.value = { a: 2 }; // 具有响应性,值被修改后页面视图会同步更新
proxy.value.a = 2; // 不具有响应性,值被修改后页面视图不会更新
  • isRef()判断一个对象是不是 ref 对象,是返回 true,不是返回 false
console.log(isRef(0)); // false
console.log(isRef(ref(0))); // true
  • unref()方法的参数是 ref,则返回 ref 对象的 value 属性值,否则返回参数本身。
const a = unref(ref(0)); // 0
const b = unref(0); // 0
const c = unref(reactive({ a: 1 })); // Proxy(Object){a:1}
  • customRef()用来自定义 ref 对象,主要掌如何定义一个 ref 对象及实现自定义防抖 ref
// value为ref对象value属性的原始值
function createRef(value) {
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        value = newValue;
        trigger();
      },
    };
  });
}
// 定义一个ref对象
const count = createRef(0);

# 五、setup() 函数参数

TIP

深入浅出 setup() 函数参数 props、context、什么情况下会使用 setup() 函数

# 1、参数 props

TIP

  • setup()函数有两个参数,第一个参数是组件的props,和标准的组件一致。
  • 一个 setup 函数的 props 是响应式的,并且会在传入新的props 时同步更新。
export default {
  props: ["list", "title"],
  setup(props) {
    console.log(props.title); // 访问传递的title属性
    console.log(props.list); // 访问传递的list属性
  },
};

代码示例

  • App.vue
<script>
  import { reactive, ref } from "vue";
  import List from "./components/List.vue";
  export default {
    setup() {
      const list = reactive(["新闻1..", "新闻2..", "新闻3.."]);
      const title = ref("新闻标题");

      // 更新方法
      function update() {
        (title.value = "更新后--标题"), list.push("新加内容一条");
      }
      return {
        list,
        title,
        update,
      };
    },
    components: {
      List,
    },
  };
</script>
<template>
  <List :list="list" :title="title" @event-update="update"></List>
</template>
  • List.vue
<script>
  export default {
    props: ["list", "title"],
    emits: ["eventUpdate"],
    setup(props) {
      setTimeout(() => {
        console.log(props.title); // 访问传递的title属性
        console.log(props.list); // 访问传递的list属性
      }, 3000);
    },
  };
</script>
<template>
  <button @click="$emit('eventUpdate')">更新</button>
  <h3>{{ title }}</h3>
  <ul>
    <li v-for="item in list">{{ item }}</li>
  </ul>
</template>

最终渲染效果如下:

点击更新按扭更新 props 的内容,3s后,setup()函数中的 props 的值也更新为了最新的。

GIF2023-7-914-23-39

# 1.1、解构 props

TIP

通过上面案例我们知道 props 参数的值为一个响应式代理对象,但如果直接解构props部分属性将失去响应性。因此推荐通过props.xxx的形式来使用 props

  • 如果我们确实需要解构props,但解构后对应变量仍需保持响应性,可以利用toRefs()工具函数来实现
setup(props) {
    let { title, list } = toRefs(props)
}
  • 如果我们需要将 props 中的某一个 prop 传递到外部,并保持其响应性,则可以利用toRef()工具函数来实现
setup(props) {
    let title = toRef(props, 'title')
}

# 2、参数 context

TIP

传入 setup 函数的第二个参数 context 是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值。

如下:

setup(props, context) {
    // context是非响应式的普通对象
    console.log("context", context)
    // 透传 Attributes(非响应式的对象(只读),等价于 $attrs)
    console.log("attrs", context.attrs)

    // 插槽(非响应式的对象,等价于 $slots)
    console.log("slots", context.slots)

    // 触发事件(函数)等价于 $emit)
    console.log("emit", context.emit)

    // 暴露组件公共属性(函数)
    console.log("expose", context.expose)
}

以下为上面代码在某个案例中的打印结果

image-20230709150617759

代码示例

  • App.vue
<script>
  import { reactive, ref } from "vue";
  import List from "./components/List.vue";
  export default {
    setup() {
      const title = ref("新闻标题");
      let list = reactive([1, 2, 3]);
      // 更新
      function update() {
        title.value = "最新动态";
      }
      return {
        title,
        list,
        update,
      };
    },
    mounted() {
      // 子组件List对外暴露了num属性,所以可以访问到
      console.log("num", this.$refs.myList.num); // 100
      // 子组件List并没有对外暴露count属性
      console.log("count", this.$refs.myList.count); // undefined
    },
    components: {
      List,
    },
  };
</script>
<template>
  <List :title="title" @update-event="update" class="list" ref="myList">
    <!--默认插槽内容-->
    <ul>
      <li v-for="item in list">{{ item }}</li>
    </ul>
    <!--具名插槽内容-->
    <template v-slot:footer>
      <a href="#">...点击查看更多</a>
    </template>
  </List>
</template>
  • List.vue
<script>
  import { ref } from "vue";
  export default {
    props: ["title"],
    emits: ["updateEvent"],
    setup(props, context) {
      // context是非响应式的普通对象
      console.log("context", context);
      // 透传 Attributes(非响应式的对象(只读),等价于 $attrs)
      console.log("attrs", context.attrs);

      // 插槽(非响应式的对象,等价于 $slots)
      console.log("slots", context.slots);

      // 触发事件(函数)等价于 $emit)
      console.log("emit", context.emit);

      // 暴露组件公共属性(函数)
      console.log("expose", context.expose);

      // 暴露组件公共属性
      const count = ref(100);
      const num = ref(2000);
      context.expose({ count });
    },
  };
</script>

<template>
  <div>
    <h3>{{ title }}</h3>
    <!--默认插槽-->
    <slot>显未主体内容</slot>
    <div class="footer">
      <!--具名插槽-->
      <slot name="footer"></slot>
    </div>
  </div>
</template>

image-20230710162733492

通过上图可以看到,context上下文对象是非响应式的,可以安全地解构

export default {
    // props是响应式的
    // attrs,slots非响应式(只读的) emit、expose为函数
    setup(props, { attrs, slots, emit, expose }) {
        ...

    }
}
  • attrsslots 都是有状态的对象,它们总是会随着组件自身的更新而更新
  • 这意味着你应当避免解构它们,并始终通过 attrs.xslots.x 的形式使用其中的属性
/* 不应该解构他们 */
setup(props, { attrs, slots, emit, expose }) {
    const {xx,xxx}=attrs;
    const {xx,xxx}=slots
    }

# 3、什么情况下会使用 setup() 函数

TIP

通过前面的学习,我们知道组合式 API 可以与选项式 API 共存,也就是说,我们可以在选项式 API 中使用setup()函数。

但在实际开发中,我们并不推荐两种方式混合使用,更希望在项目中选择其中的一种方式来开发。但在以下情况,选项式 API 会与组合式 API 共存。

  • 旧项目改造: 我们在改造旧项目时,旧项目使用的是选项式 API 书写的,但是我们现在想在原来的基础上集成基于组合式 API 的代码。
  • 非单文件组件: 在非单文件组件中我们想要使用组合使 API 时,就必需要使用到setup()函数。
// App.js
import { ref } from "https://unpkg.com/vue@3/dist/vue.esm-browser.js";
export default {
  setup() {
    const msg = ref("Hello Vue!!");
    return {
      msg,
    };
  },
  template: `<div>{{msg}}</div>`,
};

温馨提示

对于单文件组件开发中想要使用组合式 API,我们更推荐在<script setup>....</script>标签中来书写,代码更加简加及符合程序员编写代码的习惯。

在本章的第三个版块《单文件组件中使用组合式 API》中会讲到<script setup>

上次更新时间: 7/10/2023, 6:22:25 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X