hexon
发布于 2025-08-25 / 8 阅读
0

三、Vue3新语法

第2章中采用传统的选项式API的方式讲解了Vue的核心语法。随着Vue的版本迭代,还出现了一种新的方式 -- 组合式API,这也是本章的内容讲解所采用的方式。本章内容包括组合式API的了解、setup组合式API入口函数、利用ref函数定义响应式数据、利用reactive函数定义响应式数据、toRefs与toRef函数、readonly与shallowReadonly函数、shallowRef与shallowReactive函数、toRaw与markRaw函数、computed函数、watch函数、生命周期钩子函数。

本章将结合实际开发案例进行演示和讲解,主要目的是快速掌握Vue3的新语法。

组合式API的了解

Vue3除了沿用传统的选项式API的代码模式,还引入了组合式API,它允许开发人员以更好的方式编写代码。使用组合式API,开发人员可以将逻辑代码块组合在一起,从而编写出可读性高的代码。

回顾一下选项式API的代码书写方式,其实选项式API暴露的最主要的一个问题是:操作同一内容目标的代码被分散在不同的选项中,如 data、methods、computed、watch 及生命周期钩子函数等。

来看下下方代码,如果我们想要操作的只有count这一个数据对象,那么选项式API的代码看起来会十分凌乱。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组合式API的了解-选项api</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <p>count:{{ count }}</p>
      <p>double:{{ double }}</p>
      <button @click="increase">increase</button>
    </div>
    <script>
      const { createApp } = Vue
      createApp({
        // 响应式数据声明
        data() {
          return {
            count: 0,
          }
        },
        // 方法调用
        methods: {
          increase() {
            this.count++
          },
        },
        // 计算属性
        computed: {
          double() {
            return this.count * 2
          },
        },
        // 监控对象
        watch: {
          count(newVal, oldVal) {
            console.log(newVal, oldVal)
          },
        },
      }).mount('#app')
    </script>
  </body>
</html>

使用组合式API可以将代码组织成更小的逻辑片段,并将它们组合在一起,甚至在后续需要重用它们时可以进行抽离。

现在利用组合式API对上方代码进行重写,可以看到操作count数据对象的所有功能代码都被集中到一起,便于代码的编写、查看及后续维护。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组合式API的了解-组合API</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <p>count:{{ count }}</p>
      <p>double:{{ double }}</p>
      <button @click="increase">increase</button>
    </div>
    <script>
      const { createApp, ref, computed, watch } = Vue
      createApp({
        setup(props, context) {
          // 初始化ref响应式数据
          const count = ref(0)
          // 设置methods方法
          const increase = () => {
            count.value++
          }
          // 设置计算属性
          const double = computed(() => count.value * 2)
          // 设置watch监听,监控count值变化
          watch(count, (newVal, oldVal) => {
            console.log(newVal, oldVal)
          })
          // 返回响应式数据以及方法与属性计算操作
          return {
            count,
            increase,
            double,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

上方代码只是对组件式API进行了简单展示,其实组合式API的优点远不止于此。除了更灵活的代码组织、更好的逻辑重用,组合式API还提供了更好的TypeScript类型接口支持,Vue3本身也是使用TypeScript编写实现的,为了提升性能还实现了treeShaking(treeShaking直译为摇树,是一种消除死代码的性能优化理论,比如,现在想引入lodash第三方类库中的方法,但不做全部引入,只是引入单个方法,此时其他的方法都不会被打包处理,程序中会将其单个方法的代码抽离),从而产生更小的生产包和更少的网络开销。

setup组合式API入口函数

Vue3既能使用选项式API又能使用组合式API,那么应该如何区分代码方式呢?其实很好区分,Vue3为组合式API提供了一个setup函数,所有组合式API函数都是在此函数中调用的,它是组合式API的使用入口。setup函数接收两个参数:第1个参数是props对象,包含传入组件的属性;第2个参数是context上下文对象,包含attrs、emit、slot等对象属性。这两个参数在本节暂时不做深入讲解,在第4章中开始对它们进行学习。在使用组合式API定义响应式数据之前,有两个点需要我们重点关注:一个是setup函数必须返回一个对象,在模板中可以直接读取对象中的属性,以及调用对象中的函数;另一个是setup函数中的this在严格模式下是undefined,不能像选项式API那样通过this来进行响应式数据的相关操作,至于如何操作,后面章节会详细讲解。

请看下面的代码。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <title>setup组合API入口函数</title>
  </head>
  <body>
    <div id="app">
      <p>msg: {{msg}}</p>
      <button @click="handleClick">测试</button>
    </div>

    <script>
      'use strict'

      const { createApp } = Vue

      createApp({
        setup(props, context) {
          console.log(this) // this是undefined

          let msg = 'Hello prajnalab!'
          function handleClick() {
            alert('响应点击')
            msg += '--'
          }
          // 返回对象中的属性和函数, 模板中可以直接使用
          return {
            msg,
            handleClick,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

输出的this是undefined,对象中的msg属性值在模板中直接显示在页面上。点击 "测试" 按钮后,handleClick函数就会自动调用,从而显示警告提示。

但是这里要注意,我们在按钮的点击回调函数handleClick中更新了msg,而页面并不会自动更新。这是因为msg只是一个普通的字符串,并不是一个响应式数据。如何定义响应式数据呢?Vue3为开发者提供了 ref 和 reactive 等函数。

利用ref函数定义响应式数据

ref是Vue3组合式API中常见的用来定义响应式数据的函数。ref函数接收一个任意类型的数据参数作为响应式数据,由Vue内部保存。ref函数返回一个响应式的ref对象,通过ref对象的value属性可以读取或者更新内部保存的数据。

根据前面的学习,我们知道要想让模板操作ref对象,需要将ref对象添加到setup函数返回的对象中。由于ref对象是响应式的,因此在模板中操作ref对象比较特殊,不需要我们亲自添加 .value 去操作它内部的数据,只需要指定 ref 对象就可以实现操作,因为模板在编译时会自动添加 .value 来读取或更新value属性值

将上面所述过程通过代码来演示,具体如下。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>利用ref声明响应式数据</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <!-- 模板中不用.value, 内部在模板编译时会自动添加 -->
      <p>count:{{count}}</p>
      <button @click="increaseCount">增加count</button>
    </div>

    <script>
      const { createApp, ref } = Vue

      createApp({
        setup() {
          // 通过调用ref产生ref对象count, 并指定内部保存的数据初始值为0
          const count = ref(0)
          // 读取ref对象中的value数据
          console.log(count.value) // 0
          // 定义更新ref响应式数据的函数
          const increaseCount = () => {
            // 先读取value属性, 加1后再更新到value属性上
            count.value = count.value + 1
          }
          // 返回包含ref对象和更新函数的对象
          return {
            count,
            increaseCount,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

将上面所述过程通过代码来演示,具体如下。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>利用ref声明响应式数据</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <!-- 模板中不用.value, 内部在模板编译时会自动添加 -->
      <p>count:{{count}}</p>
      <button @click="increaseCount">增加count</button>
    </div>

    <script>
      const { createApp, ref } = Vue

      createApp({
        setup() {
          // 通过调用ref产生ref对象count, 并指定内部保存的数据初始值为0
          const count = ref(0)
          // 读取ref对象中的value数据
          console.log(count.value) // 0
          // 定义更新ref响应式数据的函数
          const increaseCount = () => {
            // 先读取value属性, 加1后再更新到value属性上
            count.value = count.value + 1
          }
          // 返回包含ref对象和更新函数的对象
          return {
            count,
            increaseCount,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

这个代码点击 "增加count" 按钮后,显示的数量会自动增加1,也就是count是响应式数据。

ref函数除了可以接收基础类型的数据,还可以接收对象或数组类型的数据。无论是在 JavaScript 代码中,还是在模板代码中,我们都可以进行读取或更新数据操作。需要注意的是,只有在JavaScript代码中才能通过添加 .value 来操作,而在模板中则不能通过添加 .value 来操作,请思考下方代码。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>利用ref声明响应式数据</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <!-- 模板中不用.value, 内部在模板编译时会自动添加 -->
      <p>person: {{person}}</p>
      <button @click="setNewPerson">指定新的人</button>
    </div>

    <script>
      const { createApp, ref } = Vue

      createApp({
        setup(props, context) {
          // 创建ref对象, 并指定内部初始值为一个人对象
          const person = ref({ name: 'Tom', age: 12 })

          function setNewPerson() {
            // 指定value为一个新的人对象
            person.value = { name: 'Jack', age: 23 }
          }

          return {
            person,
            setNewPerson,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

运行代码后,页面上显示的是Tom的信息,点击 "指定新的人" 按钮后,就变为了 Jack 的信息。

数组和对象的读取与更新采用的是相同的方式,这里不多做讲解。至此,可能还会有这样一个疑问:如果点击按钮不是指定一个新的人员信息,而是更新对象的中name或age属性值(如 person.value.age=24),那么页面会自动更新吗?答案是会更新,但这涉及 reactive 函数。

利用reactive函数定义响应式数据

上面讲解了利用ref函数专门来定义包含单个数据的响应式对象的方法,那么在应用中如果需要定义包含多个数据的响应式对象该怎么实现呢?Vue3提供了reactive函数,让开发者可以一次性定义包含多个数据的响应式对象。

reactive函数接收一个包含n个基础类型或对象类型属性数据的对象参数,它会返回一个响应式的代理对象,一般我们称此对象为 "reactive对象"。在JavaScript或模板中可以通过reactive对象直接读取或更新参数对象中的任意属性数据。需要强调一点,reactive函数进行的是一个深度响应式处理。也就是说,当我们通过reactive对象更新参数对象中的任意层级属性数据后,都会触发页面的自动更新

请看下面的代码。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>利用reactive声明响应式数据</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <ul>
        <li>msg: {{ state.msg }}</li>
        <li>person: {{ state.person }}</li>
        <li>courses: {{ state.courses }}</li>
      </ul>
      <button @click="updateMsg">更新msg</button>
      <button @click="updatePerson">更新person</button>
      <button @click="updateCourses">更新courses</button>
    </div>

    <script>
      const { createApp, reactive } = Vue

      createApp({
        setup(props, context) {
          // 使用reactive定义包含多个数据的响应式对象
          const state = reactive({
            msg: 'Hello Prajnalab', // 基本类型
            person: { name: 'Tom', age: 22 }, // 对象类型
            courses: ['JS', 'BOM', 'DOM'], // 数组类型
          })

          // 更新字符串msg的函数
          const updateMsg = () => {
            state.msg += '--'
          }
          // 更新对象person的函数
          const updatePerson = () => {
            state.person = { name: 'Jack', age: 33 }
          }
          // 更新数组courses的函数
          const updateCourses = () => {
            state.courses = ['JS2', 'BOM2', 'DOM2']
          }

          return {
            updateMsg,
            updatePerson,
            updateCourses,
            state,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

上方代码先使用 reactive 函数定义了包含基础类型、对象类型和数组类型的3个属性数据的响应式对象 state,然后分别定义了更新字符串msg、更新对象person和更新数组courses的3个函数,最后将state对象和这3个函数都返回模板中使用。在模板中通过state对象来读取内部的3个对象进行动态显示,同时将返回的3个函数分别绑定到3个按钮的点击事件上。代码的效果点击按钮即可体现,这里不再赘述。

前面提过,reactive函数进行的是深度响应式处理,结合刚才的person属性来说,不仅直接更新person对象会触发页面更新,更新person对象中的任意属性(name或age)也会触发页面更新,甚至我们给person对象添加一个新属性或者删除已有属性,页面同样会更新。将updatePerson函数修改为下方代码。

// 更新对象person的函数
const updatePerson = () => {
  // 指定新的person对象
  // state.person = {name: 'Jack', age: 33}

  // 更新对象中的已有属性
  state.person.name += '=='
  state.person.age += 2

  // 给对象添加新属性
  state.person.sex = '男'

  // 删除对象中的属性
  delete state.person.age
}

此时点击 "更新person" 按钮,这3种更新方式都能触发页面更新。需要注意的是,直接添加新属性和删除已有属性在Vue2中是不可以触发页面更新的,但是在Vue3中是可以的

reactive函数定义的数组数据同样进行的是深度响应式处理,我们不仅可以通过调用数组方法进行响应式更新,还可以通过下标进行响应式更新。将updateCourses函数修改为下方代码。

// 更新数组courses的函数
const updateCourses = () => {
  // 指定新的数组
  // state.courses = ['JS2', 'BOM2', 'DOM2']

  // 通过数组方法更新数组元素
  state.courses.splice(1, 1, 'Vue2')

  // 通过下标更新数组元素
  state.courses[2] = 'Vue3'
}

点击 "更新courses" 按钮,这两个方式都能触发页面更新。需要注意的是,在Vue2中直接通过下标更新数组元素不可以触发页面更新,但是在Vue3中是可以的

前面说过,先给ref函数传入一个对象或数组数据,再通过ref对象来操作对象或数组内的内部数据,发现也是响应式的,这是为什么呢?因为一旦 ref 函数内部的value数据是对象或数组,就会自动先创建一个包含此对象或数组的reactive对象,也就是代理对象,再保存给ref对象的value,比如下方代码。

// 创建 ref 对象,并指定内部初始值为一个人员信息
const person = ref({name: 'Tom', age: 12});

// 更新 value 对象内部的 age 属性
person.value.age = 13;

更新 age 属性就是一个响应式的数据更新,因为 person.value 是ref函数接收的人员信息的reactive对象,也就是代理对象,通过reactive对象去更新对象的内部属性,必然是一个响应式的数据更新,页面自然会更新。

这个底层Vue3中用的是Proxy,总之Vue3编译模板的时候做了处理!目前只要知道对象会包成reactive对象!

toRefs与toRef函数

在介绍toRefs与toRef函数之前,先来演示使用 reactive 函数进行代码简化的问题,下面是简化前的代码。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>利用reactive声明响应式数据</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <ul>
        <li>msg: {{ state.msg }}</li>
        <li>person: {{ state.person }}</li>
        <li>courses: {{ state.courses }}</li>
      </ul>
      <button @click="updateMsg">更新msg</button>
      <button @click="updatePerson">更新person</button>
      <button @click="updateCourses">更新courses</button>
    </div>

    <script>
      const { createApp, reactive } = Vue

      createApp({
        setup(props, context) {
          // 使用reactive定义包含多个数据的响应式对象
          const state = reactive({
            msg: 'Hello Prajnalab', // 基本类型
            person: { name: 'Tom', age: 22 }, // 对象类型
            courses: ['JS', 'BOM', 'DOM'], // 数组类型
          })

          // 更新字符串msg的函数
          const updateMsg = () => {
            state.msg += '--'
          }
          // 更新对象person的函数
          const updatePerson = () => {
            state.person = { name: 'Jack', age: 33 }
          }
          // 更新数组courses的函数
          const updateCourses = () => {
            state.courses = ['JS2', 'BOM2', 'DOM2']
          }

          return {
            updateMsg,
            updatePerson,
            updateCourses,
            state,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

在模板中,每次获取reactive函数中的数据都要写上 "state.",如果需要获取多次,则操作会重复多次。现在想简化一下代码,去掉 "state.",也就是像下面这样编写代码:

<ul>
  <li>msg: {{msg}}</li>
  <li>person: {{person}}</li>
  <li>courses: {{courses}}</li>
</ul>

同样在setup函数中返回的代码也需要做相应的修改,将reactive对象中的msg、person和course取出后分别放入返回的对象中。

return {
  msg: state.msg,
  person: state.person,
  courses: state.courses,
  updateMsg
}

或者利用扩展运算符简化代码。

return {
  ...state,
  updateMsg
}

但是,运行代码并点击按钮 "更新msg" 后,发面页面上的msg显示不会更新。这是因为后面的两个方式传入的msg、person和course属性值都是非响应式数据,而要想页面能自动更新,必须要求setup函数返回对象中的属性是ref对象或reactive对象。

如何让从reactive对象中读取出的属性也是响应式的呢?答案是:可以利用本节介绍的toRefs和toRef函数。toRefs函数能一次性将reactive对象包含的所有属性值都包装成ref对象,而toRef函数只能一次处理一个属性。

详细来说,toRefs函数接收一个reactive对象,内部会包装每个属性值生成ref对象,最后返回包含所有属性值的ref对象的对象。toRef函数接收reactive对象和属性名,内部会创建此属性名对应属性值的ref对象,并返回这个ref对象

一般我们会使用toRefs函数来解决上面的问题,代码如下。

// 生成包含多个 ref 对象的对象
const stateRefs = toRefs(state)

return {
  msg: statrRefs.msg,
  person: statrRefs.person,
  courses: statrRefs.courses,
  updateState
}

当然可以进一步简化代码,直接用扩展运算符解构toRefs函数生成的对象。

return {
  ...toRefs(state),
  updateState
}

那么如何使用toRef函数来处理,要如何编写代码呢?可以先多次调用 toRef 函数生成各个属性的ref对象,再添加到返回对象中,代码如下。

return {
  msg: toRef(state, 'msg'),
  person: toRef(state, 'person'),
  courses: toRef(state, 'courses'),
  updateState
}

虽然使用toRefs和toRef函数都能解决解构reative对象的属性读取响应式丢失的问题,但从代码可读性上来看,明显使用toRefs函数的代码更简洁。toRef函数更适用于只对单个属性进行处理的场景,而toRefs函数更适用于对所有属性进行处理的场景。

这里有的人可能在想,我不使用toRef,我就用ref(state.xxx)返回不也可以么?其实是不可以的,要理解数据源,两者对比如下:

特性

toRef

ref

数据源连接

保持与源数据的响应式连接

创建独立副本

源数据更新

自动同步更新

不会同步更新

适用场景

需要保持与源数据同步

需要独立的数据副本

ref相当于创建了一个新的响应式变量与reactive对象的属性没有关系了,我们模板中绑定的应该是整个reactive对象。

readonly与shallowReadonly函数

无论是reactive对象(也称为代理对象)还是ref对象,进行的都是深度响应式处理。也就是说,我们通过reactive对象或ref对象进行属性的深度读取和修改操作,修改后能触发页面的自动更新。而如果我们想产生一个只包含读取能力的reactive对象,就可以使用readonly与shallowReadonly函数

readonly和shallowReadonly函数接收的参数是一样的,其可以是一个原始的非响应式对象,也可以是响应式的reactive对象。只是readonly函数产生的reactive对象是深度只读的,而shallowReadonly函数产生的reactive对象只有外层属性是只读的,所有嵌套的内部属性都是可读/写的。

下面我们以接收一个reactive对象为例来演示readonly与shallowReadonly函数的使用方法,代码如下。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>readonly与shallowReadonly</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>

  <body>
    <div id="app">
      <h3>reactive对象显示</h3>
      <p>person.name:{{person.name}}</p>
      <p>person.addr.city:{{person.addr.city}}</p>

      <h3>readonly对象显示</h3>
      <p>rPerson.name:{{rPerson.name}}</p>
      <p>rPerson.addr.city:{{rPerson.addr.city}}</p>

      <h3>shallowReadonly对象显示</h3>
      <p>srPerson.name:{{srPerson.name}}</p>
      <p>srPerson.addr.city:{{srPerson.addr.city}}</p>

      <button @click="update1">通过reactive对象更新</button>
      <button @click="update2">通过readonly对象更新</button>
      <button @click="update3">通过shallowReadonly对象更新</button>
    </div>

    <script>
      const { createApp, reactive, readonly, shallowReadonly } = Vue

      createApp({
        setup() {
          // 生成深度可读写的reactive对象
          const person = reactive({
            name: '张三',
            addr: {
              city: '北京',
            },
          })

          // 生成深度只读的reactive对象
          const rPerson = readonly(person)

          // 生成浅只读的reactive对象
          const srPerson = shallowReadonly(person)

          const update1 = () => {
            person.name += '--' // 控制台没警告提示, 界面会正常更新
            person.addr.city += '++' // 控制台没警告提示, 界面会正常更新
          }

          const update2 = () => {
            rPerson.name += '--' // 控制台警告提示, 且界面不会更新
            rPerson.addr.city += '++' // 控制台警告提示, 且界面不会更新
          }

          const update3 = () => {
            srPerson.name += '--' // 控制台警告提示, 且界面不会更新
            srPerson.addr.city += '++' // 控制台没警告提示, 界面会正常更新
          }
          return {
            person,
            rPerson,
            srPerson,
            update1,
            update2,
            update3,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

点击三个按钮会产生不同的情况,在代码的注释中我已经注明出来了。

同时Vue3提供了用来判断是否是只读reactive对象的工具函数isReadonly,如果是通过readonly或shallowReadonly函数产生的只读对象,则isReadonly函数返回true,否则返回false,验证如下。

console.log(isReadonly(person)) // false
console.log(isReadonly(rperson)) // true
console.log(isReadonly(srperson)) // true

那么readonly与shallowReadonly函数进行只读对象转化操作的应用场景是什么呢?试想,如果在项目是开发过程中团队成员开发了一些功能组件,想要实现其他成员可以使用对应组件,但是不能修改其数据,也就是对功能进行一定的约束,就可以通过readonly与shallowReadonly函数将数据进行只读转化,这样既可以保证程序的高度可读性,也可以保证数据的安全性。

readonly 让我们可以创建既保持响应式又受保护的数据,这在组件通信和架构设计中非常有用!

shallowRef与shallowReactive函数

通过前面内容的学习我们知道,ref和reactive函数都会对数据进行深度响应式处理。也就是说,我们通过ref对象更新value属性,或者更新value对象属性中的嵌套属性,页面都会自动更新,通过reactive对象更新目标对象中的属性,或者更新目标对象中属性对象的嵌套属性,页面都会自动更新。如果我们只想进行外部属性的响应式处理,不想进行嵌套属性的响应式处理,则可以使用Vue3提供的shallowRef与shallowReactive函数来实现。

shallowRef函数是ref函数的浅层实现,接收一个原始对象,返回一个ref对象。ref对象内部保存的value就是传入的原始对象,而不是一个响应式的代理对象。这就意味着更新value是响应式的,但更新value对象内部的属性不是响应式的。

shallowReactive函数是reactive函数的浅层实现,接收一个原始对象,返回一个代理对象(也称为reactive对象)。对于原始对象中嵌套的属性对象,shallowReactive函数并没有创建对应的代理对象,也就是说,属性对象不是响应式的,更新属性对象中的属性,页面不会自动更新。

下面我们通过代码来演示一下shallowRef与shallowReactive函数的使用方法。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>shallowRef与shallowReactive</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <h3>课程信息:</h3>
      <ul>
        <li>名称: {{course.title}}</li>
        <li>天数: {{course.days}}</li>
      </ul>

      <h3>人员信息:</h3>
      <ul>
        <li>姓名: {{person.name}}</li>
        <li>城市: {{person.addr.city}}</li>
      </ul>

      <button @click="update1">浅更新ref对象</button> &nbsp;
      <button @click="update2">浅更新reactive对象</button> &nbsp;
      <button @click="update3">深更新ref对象</button> &nbsp;
      <button @click="update4">深更新reactive对象</button>
    </div>

    <script>
      const { createApp, shallowRef, shallowReactive } = Vue

      createApp({
        setup() {
          // 定义浅ref对象
          const course = shallowRef({
            title: '',
            days: 0,
          })
          console.log(course)

          // 定义浅reactive对象
          const person = shallowReactive({
            name: '',
            addr: {
              city: '',
            },
          })
          console.log(person)

          // 直接更新value属性,界面会更新
          const update1 = () => {
            course.value = {
              // 界面会更新
              title: 'Vue技术栈',
              days: 40,
            }
          }

          // 更新外层属性,界面会更新
          const update2 = () => {
            person.name = '李四'
            person.addr = {
              city: '北京',
            }
          }

          // 更新value对象内部属性,界面不会更新
          const update3 = () => {
            course.value.title = 'React技术栈'
            course.value.days = 20
          }

          // 更新深层的属性,界面不会更新
          const update4 = () => {
            person.addr.city = '上海'
          }

          return {
            course,
            person,
            update1,
            update2,
            update3,
            update4,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

点击不同的按钮会产生不同的情况,在代码的注释中我已经注明出来了。

那么在什么场景下使用shallowRef与shallowReactive函数呢?如果页面中需要动态显示一个包含嵌套的对象或数组,就需要将其定义成响应式数据。当然我们可以选择使用ref或reactive函数实现,但如果嵌套对象中的属性并不需要单独更新,则可以选择使用shallowRef或shallowReactive函数实现。由于shallowRef和shallowReactive函数只做了外部(单层)的响应式处理,因此相比ref和reactive函数来说内存占用更小,处理性能也更高。

toRaw与markRaw函数

如果我们想得到一个reactive对象内部包含的原始对象,就可以选择使用toRaw函数。如果我们不想让一个原始对象包装成reactive响应式对象,就可以选择使用markRaw函数。

toRaw函数接收的参数为一个reactive对象,返回值为内部包含的整个原始对象。

markRaw函数接收的参数为一个原始对象,返回值还是这个原始对象,但其被添加了不能转换为reactive对象的标识属性 __v_skip,该值为 true。当我们将这个对象会传入reactive函数时,返回的就不是代理对象了,而是参数对象本身。也就是说,它并不是响应式对象。

简单理解,一个是转成原始对象(toRaw),一个是标记为不会被转成响应式对象(markRaw)。

下面通过代码来演示toRaw与markRaw函数的使用方法。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>toRaw与markRaw</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <p>state.name: {{state.name}}</p>
      <p>state.addr.city: {{state.addr.city}}</p>
      <button @click="test1">测试toRaw</button>
      <hr />
      <p>state2.name: {{state2.name}}</p>
      <p>state2.addr.city: {{state2.addr.city}}</p>
      <button @click="test2">测试markRaw</button>
    </div>
    <script>
      const { createApp, reactive, toRaw, markRaw } = Vue
      createApp({
        setup() {
          // 定义reactive对象
          const state = reactive({
            name: '张三',
            addr: {
              city: '北京',
            },
          })

          // 测试使用toRaw
          const test1 = () => {
            // 通过toRaw得到reactive对象内部包含的原始对象
            const rawPerson = toRaw(state)
            console.log(rawPerson)
          }

          // 原始对象
          const person2 = {
            name: '李四',
            addr: {
              city: '上海',
            },
          }

          // 标记一个原始对象为不可以reactive的对象,并返回这个对象
          const markPerson2 = markRaw(person2)
          console.log(markPerson2) // 多了一个__v_skip为true的属性

          // 对markRaw的对象进行reactive处理,会被原样返回,它不是响应式的
          const state2 = reactive(markPerson2)
          console.log(state2) 
          console.log(markPerson2 === person2, state2 === markPerson2) // true true

          // 测试更新markRaw的reactive对象,界面不会更新
          const test2 = () => {
            state2.name += '--'
            state2.addr.city += '--'
            console.log(state2.name, state2.addr.city) // 值改变了,界面不会刷新,因为不是响应式的
          }

          return {
            state,
            state2,
            test1,
            test2,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

代码的运行效果我已在注释中说明,这里要注意的是,markRaw是直接修改原始对象,给其添加一个 __v_skip为true的属性。

那么,在什么情况下需要使用toRaw与markRaw函数呢?当我们想得到reactive对象中包含的整个原始对象时,toRaw函数就是一个不错的选择。当我们为其他模块提供一个包含多个数据的对象时,如果我们不需要,也不希望外部使用者将其处理为响应式对象,就可以先使用markRaw函数来处理这个对象后再返回它。

computed函数

Vue3的组合式API中的computed函数与选项式API中的computed属性的功能是一样的,只是在语法的使用上不同而已,但不管是在组合式API还是在选项式API中,都是既可以只指定getter,也可以指定getter(指get方法)和setter(指set方法)的。

computed函数接收的参数可以是一个函数,也可以是一个包含get方法和set方法的对象。当参数为函数时,就是指定了getter,用来返回动态计算的结果值;当参数为对象时,就是指定了getter和setter,此时既可以通过getter返回动态计算的结果值,也可以通过setter监听计算属性的修改。

computed函数的返回值是计算属性对象,它本质上是一个ref对象,getter返回的值会被自动保存到其value属性上。当我们修改计算属性对象的value属性值时,setter就会被自动调用

下面通过代码演示computed函数的使用方法。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>computed</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <h3>测试只带get的计算属性</h3>
      <p>数量: {{count}}</p>
      <p>姓名: {{person.name}}</p>
      <p>信息(get计算属性): {{info}}</p>

      <button @click="update">更新</button>

      <h3>测试带get和set的计算属性</h3>
      <input type="text" v-model="doubleCount" />
    </div>
    <script>
      const { createApp, ref, reactive, computed } = Vue

      createApp({
        setup() {
          // 定义ref对象
          const count = ref(0)
          // 定义reactive对象
          const person = reactive({
            name: '张三',
          })

          // 定义只有getter的计算属性
          const info = computed(() => {
            return `${person.name}完成的数量${count.value}`
          })

          // 定义有getter和setter的计算属性
          const doubleCount = computed({
            get() {
              console.log(info)
              return count.value * 2
            },
            set(value) {
              debugger
              count.value = value / 2
            },
          })

          // 更新ref或reactive数据
          const update = () => {
            count.value += 1
            person.name += '--'
          }

          return {
            count,
            person,
            info,
            update,
            doubleCount,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

在上面的代码中,我们先分别定义了一个ref对象count和一个reactive对象person,然后调用computed函数来定义计算属性info,传入的参数为用于动态计算属性的get函数。computed函数返回的info对象,本质上是一个ref对象,可以通过控制进行查看,如下图。

根据控制台输出的结果可以看出,内部 "_value" 的值就是执行get函数,在读取前面的ref和reactive响应式数据后,计算并返回的结果。同时返回了计算属性对象,在模板中就可以读取计算属性显示。

其它的特点,比如缓存,通过input去修改计算属性的值(通过set方法),和之前在选项式API中是一样的效果,这里不再赘述。

watch函数

Vue3提供的组合式API函数watch,从功能上看,与选项式API的watch配置和$watch方法相同,但在语法使用上还是有些许差别的。

$watch见官网:https://cn.vuejs.org/api/component-instance.html#watch

watch函数可以监听一个或多个响应式数据,当响应式数据发生变化时,监听的回调就会自动执行。

watch函数接收3个参数,具体如下。

(1)第1个参数是被监听的一个或多个响应式数据,该参数有3种形式,具体如下。

① 一个reactive对象或ref对象。

② 返回reactive对象中基础类型属性的函数。

③ 包含任意多个reactive对象、ref对象或函数的数组。

(2)第2个参数是监听回调函数,该回调函数可以接收两个参数,具体如下。

① 一个新值或包含多个新值的数组。

② 一个旧值或包含多个旧值的数组。

(3)第3个参数是可选的配置对象,包含是否立即执行的immediate和是否深度监听的deep。

请阅读下方代码。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>watch函数</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <p>count: {{countObj.count}}</p>
      <p>person: {{person.name}}-{{person.addr.city}}</p>

      <button @click="update">更新</button>
    </div>
    <script>
      const { createApp, ref, reactive, watch } = Vue

      createApp({
        setup() {
          const countObj = ref({
            count: 0,
          })
          const person = reactive({
            name: '张三',
            addr: {
              city: '北京',
            },
          })

          const update = () => {
            // 准备更新ref或reactive数据
          }

          return {
            countObj,
            person,
            update,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

在上面的代码中,我们分别定义了一个ref对象countObj和一个reactive对象perosn,同时定义了一个准备更新数据的update函数,并通过return返回数据。在模板中读取了ref对象和reactive对象的数据并进行动态显示,将update函数绑定给按钮的点击监听。运行后的页面效果如下图所示。

此时使用watch函数来监听ref对象,默认进行的是浅监听,如果需要进行深度监听,则需要配置deep为true,代码如下。

// 浅监听ref对象
watch(countObj, (newVal, oldVal) => {
  console.log('countObj 监听', newVal, oldVal);
});

// 深度监听ref对象
watch(
  countObj,
  (newVal, oldVal) => {
    console.log('countObj 深度监听', newVal, oldVal);
  },
  { deep: true }
);

如果对ref对象数据进行浅更新,那么两个监听的回调都会执行。

const update = () => {
  // 对 ref 对象数据进行浅更新
  countObj.value = {count: 2};
}

如果对ref对象数据进行深度更新,那么前面浅监听的回调不会再执行了。

const update = () => {
  // 对 ref 对象数据进行深度更新
  countObj.value.count = 3;
}

使用watch函数监听reactive对象,默认进行的是深度监听。在按钮的点击回调中,无论是person对象的浅更新,还是深度更新,监听的回调都会执行,代码如下。

// 监听 reactive 对象,默认进行的是深度监听
watch(person, (newVal, oldVal) => {
  console.log('person change', newVal, oldVal)
})

const update = () => {
  // 浅更新
  person.name += '--';
  // 深度更新
  person.addr.city += '==';
}

如果我们要监听的是reactive对象代理的一个基础类型属性,比如现在想要监听name属性,如果直接给watch函数传入person.name,则程序会直接报错。这是因为它是一个基础类型的值,而不是一个响应式的值,代码如下。

// 错误写法
watch(perosn.name, (newVal, oldVal) => {
  console.log('person.name change', newVal, oldVal);
});

控制台会警告:

Vue3针对这种情况,提供了函数式的写法,可以传入一个函数,函数内部返回要监听的这个值。

// 正确的写法
watch(() => person.name, (newVal, oldVal) => {
  console.log('person.name change', newVal, oldVal);
});

const update = () => {
  // 浅更新
  person.name += '--';
};

当用户点击 "更新" 按钮更新name属性时,监听的回调就会自动执行。

如果我们要监听多个不同的数据,要怎么处理呢?其实watch函数可以接收一个包含多个要监听数据的数组。同时,回调函数接收的第1个参数就是由多个被监听数据的最新值组成的数组,第2个参数是旧值的数组。

// 监听 ref 对象和 reactive 对象中的 name 属性
watch([countObj, () => person.name], (newVals, oldVals) => {
  console.log('countObj或person.name变化了', newVals, oldVals);
})

上面的代码监听的是ref对象countObj和reactive对象person中的name属性,其监听的回调接收的newVals就是最新countObj的value和最新person的name组成的数组,而oldVals就是旧countObj的value和旧person的name组成的数组。

如果在更新函数中对ref对象进行浅更新,监听的回调就会执行,代码如下:

const update = () => {
  // 对 ref 对象数据进行浅更新
  countObj.value = {count: 2};
}

如果在更新函数中通过reactive对象person更新name属性,监听的回调就会执行,代码如下。

const update = () => {
  perosn.name += '---';
};

如果想让监听的回调在初始时执行一次,就可以配置immediate为true。需要强调的是,还可以在监听的回调中执行异步操作,而这在计算属性的get函数中是不可以实现的,代码如下。

watch(countObj, (newVal, oldVal) => {
  console.log('立即执行的监听', newVal, oldVal);
  // 可以执行异步操作
  setTimeout(() => {
    alert('2秒后的提示');
  }, 2000)
}, {immediate: true});

const update = () => {
  countObj.value = {count: 2};
};

在初始化时监听的回调就会执行一次,且在2秒后显示警告提示,当然在点击按钮后,监听的回调还会再次执行。

官方文档明确说过,计算属性的get函数里面不要产生副作用代码和异步操作!

生命周期钩子函数

组合式API的生命周期钩子函数和选项式API的生命周期钩子函数存在一定的差异。组合式API引入了setup函数来进行初始化操作,包括定义响应式数据、监听响应式数据、更新响应式数据等,而选项式API中的beforeCreate和created是在初始化过程中调用的两个生命周期钩子函数,所以Vue3中去掉了这两个生命周期钩子函数,用setup函数来代替它们

除在初始阶段利用setup函数替换生命周期钩子函数beforeCreate和created以外,在组合式API中,挂载、更新、销毁的生命周期钩子函数都以函数监听的方式实现,包括onBeforeMount、onMoutned、onBeforeUpdate、onUpdated、onBeforeUnmont、OnUnmounted。值得一提的是,要想使用这些生命周期钩子函数,必须先从Vue中引入,然后在setup函数中调用,生命周期钩子函数的调用结果主要通过回调函数的形式实现。

组合式API和生命周期钩子函数共有4个阶段,如下图所示。

前面讲解过选项式API的生命周期钩子函数,所以这里我们先阅读代码,再结合文字分析代码。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>生命周期钩子函数</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>

  <body>
    <div id="app">
      <p ref="mRef">{{ message }}</p>
      <button @click="message = 'Prajnalab拥有广大的学习资源'">
        更新内容
      </button>
    </div>

    <script>
      const {
        createApp,
        ref,
        onBeforeMount,
        onMounted,
        onBeforeUpdate,
        onUpdated,
        onBeforeUnmount,
        onUnmounted,
      } = Vue

      const app = createApp({
        setup(props, context) {
          // 声明一个ref元素对象
          const mRef = ref(null)
          // 声明响应式数据message
          const message = ref('欢迎来到Prajnalab')
          // onBeforeMount没有完成挂载,有message却没有ref元素对象
          onBeforeMount(() => {
            console.log('onBeforeMount', message, mRef.value)
          })
          // onMounted已经完成挂载,message与ref元素都存在
          onMounted(() => {
            console.log('onMounted', message, mRef.value)
          })
          // 更新之前的界面
          onBeforeUpdate(() => {
            console.log('onBeforeUpdate')
            debugger
          })
          // 更新之后的界面
          onUpdated(() => {
            console.log('onUpdated')
          })
          // 实例完全卸载前,仍旧有元素对象
          onBeforeUnmount(() => {
            console.log('onBeforeUnmount', mRef.value)
            debugger
          })
          // 实例完全卸载,不存在元素对象
          onUnmounted(() => {
            console.log('onUnmounted', mRef.value)
          })

          return {
            mRef,
            message,
          }
        },
      })

      app.mount('#app')
      setTimeout(() => {
        app.unmount()
      }, 5000)
    </script>
  </body>
</html>

结合上面的代码分析组合式API的生命周期钩子函数。

(1)setup:在setup初始阶段,利用ref函数定义一个响应式数据message,并设置其初始值为 "欢迎来到Prajnalab"。

(2)onBeforeMount:结合模板中的p元素添加ref属性mRef,并在脚本中设置mRef类型为ref类型,在onBeforeMount生命周期钩子函数中打印message和mRef.valye,可以看到message有数据,而mRef.value为null。这是因为DOM元素现在仍存在于内存中,并没有完成挂载。

(3)onMounted:这一阶段网页的el元素对象成功地将虚拟DOM内容渲染到了真实DOM对象上,此时打印mRef.value,其值是一个p元素标签的内容,说明挂载已经完成。

(4)onBeforeUpdate:我们可以点击页面中的按钮来修改响应式数据message,在onBeforeUpdate生命周期钩子函数中设置一个debugger断点查看效果,最终会发现页面中仍旧显示的是数据修改之前的DOM元素内容。

(5)onUpdated:与onBeforeUpdate类似,在onUpdated生命周期钩子函数中设置debugger断点,查看到的是数据发生改变以后的页面。

(6)onBeforeUnmount:当这个生命周期钩子函数被调用时,组件实例依然拥有全部的功能。同样也在该生命周期钩子函数中进行debugger断点调试。在利用定时器进行unmount销毁操作后,则可以查看到即将进入onBeforeUnmount生命周期阶段。此时页面中仍旧保留DOM显示的状态,控制台中的mRef.value仍旧有元素对象。

(7)onUmounted:该生命周期钩子函数在一个组件实例被销毁后调用,对应组件实例的DOM对象也将不复存在。继续进行debugger断点调试,程序就会进入onUmounted生命周期阶段。此时组件原本指向的DOM对象已经清空,页面中也不再显示任何的DOM元素内容。