• 十二、组合API(2)


    本章概要

    • 响应式 API
      • reactive() 方法
      • watchEffect() 方法
      • 解构响应性状态
      • 深入 watchEffect()
      • ref
      • readonly
      • computed
      • watch

    11.3 响应式 API

    Vue 3.0 的核心功能主要是通过响应式 API 实现的,组合 API 将他们公开为独立的函数。

    11.3.1 reactive() 方法

    reactive() 方法可以对一个 JavaScript 对象创建响应式状态。在 HTML 页面中,可以编写如下代码:

    <script src="https://unpkg.com/vue@next">script>
    <script>
      // 响应式状态
      const state = Vue.reactive({
        count:0
      })
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在但文件组件中,可以编写如下代码:

    import { reactive } from 'vue'
    // 响应式状态
    const state = reactive({
      count:0
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5

    reactive() 方法相当于 Vue 2.x 中的 Vue.observable() 方法。

    11.3.2 watchEffect() 方法

    上述代码返回的 state 是一个响应式对象,可以在渲染期间使用它。由于依赖关系的跟踪,当 state 对象发生变化时,视图会自动更新。
    在 DOM 中渲染内容被认为是一个“副作用”,要应用和自动重新应用基于响应式的 state 对象,可以使用 watchEffect API 。
    完整代码如下:

    DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>title>
    head>
    
    <body>
        <script src="https://unpkg.com/vue@next">script>
        <script>
            const { reactive, watchEffect } = Vue;
            const state = reactive({
                count: 0
            })
    
            watchEffect(() => {
                document.body.innerHTML = `count is ${state.count}`
            })
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    watchEffect() 方法接收一个函数作为参数,它会立即运行该函数,同时响应式的跟踪其依赖项,并在依赖项发生更改时重新运行该函数。watchEffect() 方法类似于 Vue 2.x 中的 watch 选项,但是她不需要分离监听的数据源和副作用回调。组合 API 还提供了一个 watch() 方法,其行为与 Vue 2.x 中的 watch 选项完全相同。

    在谷歌浏览器中打开上述页面,页面中初始显示内容为:count is 0,在浏览器的 Console 窗口 输入:state.count = 1 ,会发现页面内容同时发生了更新。如下:
    在这里插入图片描述

    11.3.3 解构响应性状态

    当要使用一个较大的响应式对象的一些属性时,可能会考虑使用 ES6 的对象解构语法获得想要的属性。如下:

    DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>title>
    head>
    
    <body>
        <div id="app">
            <p>作者:{{author}}p>
            <p>书名:{{title}}p>
        div>
        <script src="https://unpkg.com/vue@next">script>
        <script>
            const { reactive, toRefs } = Vue;
            const app = Vue.createApp({
                setup() {
                    const book = reactive({
                        author: '吴承恩',
                        year: '2022',
                        title: '西游记',
                        description: '师徒四人打怪升级的故事',
                        price: '222'
                    })
                    let { author, title } = book;
                    return {
                        author,
                        title
                    }
                }
            })
            const vm = app.mount('#app');
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    但是通过这种解构,author 和 title 的响应性将丢失,在 Chrome 浏览器的 Console 窗口修改 vm.author 或 vm.title,就会发现页面内容并没有被更新。如下:
    在这里插入图片描述

    遇到这种情况,需要将响应式对象转换为一组 ref,这些 ref 将保留到源对象的响应式连接。这个转换是通过调用 toRefs() 方法完成的,该方法将响应式对象转换为普通对象,其中结果对象上的每个属性都是指向原始对象中相应属性的 ref。
    修改上述代码,如下:

    let { author, title } = toRefs(book);
    
    • 1

    再次修改 vm.author 或 vm.title ,可以发现页面内容也随着更新了,如下:
    在这里插入图片描述

    Vue 3.0 中还有一个 toRef() 方法,改方法是为响应式源对象的某个属性创建 ref,然后可以传递这个 ref ,并保持对其源属性的响应性连接。修改上述代码,调用 toRef() 方法分别为 book 对象的 author 和 title 属性创建 ref 对象。如下:

    const { reactive, toRef } = Vue;
    const app = Vue.createApp({
        setup() {
            const book = reactive({
                author: '吴承恩',
                year: '2022',
                title: '西游记',
                description: '师徒四人打怪升级的故事',
                price: '222'
            })
            const author = toRef(book,'author');
            const title = toRef(book,'title');
            return {
                author,
                title
            }
        }
    })
    const vm = app.mount('#app');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    当把一个 prop 的 ref 传递给组合函数时,toRef() 方法就很有用了。如下:

    export default {
        setup(props){
            useSomeFeature(toRef(props,'foo'))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    11.3.4 深入 watchEffect()

    当 watchEffect() 方法在组件的 setup() 函数或生命周期钩子中被调用时,监听器(watcher)被链接到组件的声明周期中,并在组件卸载(unmounted)时自动停止。在其它情况下,watchEffect() 方法返回一个停止句柄,可以调用该句柄显示地停止监听器。如下:

    const stop = watchEffect(() => {
      //...
    })
    // 之后想要停止监听器,可以调用 stop() 函数
    stop()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    有时候,watchEffect() 方法将执行异步副作用,当它失效时,需要清除这些副作用。例如,在副作用完成之前状态发生了改变,watchEffect() 方法可以接收一个 onInvalidate() 函数,该函数可用于注册一个无效回调,无效回调将在下面两种情况发生时被调用:

    • 副作用将再次运行
    • 监听器被停止(例如,如果在组件的 setup() 函数或生命周期钩子中使用 watchEffect() 函数,则当组件被卸载时停止)

    看如下代码:

    watchEffect(onInvalidate => {
      const token = performAsyncOperation(id.value)
      onInvalidate(() => {
        // id 已更改或监听器已停止时,取消挂起的异步操作
        token.cancel()
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在执行数据抓取时,effect() 函数(即副作用)通常是异步函数。代码如下:

    const data = ref(null);
    watchEffect(async() => {
      // 在 Promise解析之前注册清理函数
      onInvalidate(() => {...})
      data.value = await fetchData(props.id)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    异步函数隐式地返回一个 Promise ,但是清理函数需要在 Promise 解析之前立即注册。此外,Vue 依赖返回的 Promise 来自动处理 Promise 链中的潜在错误。
    看以下代码:

    DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>title>
    head>
    
    <body>
        <div id="app">{{ count }}div>
    
        <script src="https://unpkg.com/vue@next">script>
        <script>
            const { ref, watchEffect } = Vue;
            const vm = Vue.createApp({
                setup() {
                    const count = ref(0);
                    watchEffect(() => {
                        console.log(count.value);
                    })
                        return {
                        count
                    }
                }
            }).mount('#app');
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    当初始运行时,将同步记录 count 值,之后当 count 发生变化后,传入 watchEffect() 方法的回调函数会在组件更新后被调用。需要注意的是,第一次运行时在挂载组件之前执行的,如果想要在一个监听的 effect() 函数中访问 DOM 模板的 ref ,则需要在 mounted 钩子中执行 watchEffect() 方法。如下:

    onMounted(() => {
      watchEffect(() => {
        // 访问DOM 或模板的 ref
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在需要同步或在组件更新之前重新运行监听的 effect() 函数的情况下,可以给 watchEffect() 方法传递一个附加的选项对象,在选项对象中使用 flush 选项,该选项的默认值为 ‘post’,即在组件更新后在此运行监听的 effect() 函数。

    // 同步触发
    watchEffect(
      () => {
        // ...
      },
      {
        flush:'sync'
      }
    )
    // 在组件更新之前触发
    watchEffect(
      () => {
        // ...
      },
      {
        flush:'pre'
      }
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    11.3.5 ref

    reactive() 方法为一个 JavaScript 对象创建响应式代理,如果需要对一个原始值(如字符串)创建响应式代理对象,一种方式是将该原始值作为某个对象的属性,调用 reactive() 方法为该对象创建响应式代理对象,另一种方式就是使用 Vue 给出的另一个方法 ref ,该方法接收一个原始值,返回一个响应式和可改变的 ref 对象,返回的对象只有一个 value 属性指向内部值。
    看以下代码:

    DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>title>
    head>
    
    <body>
        <script src="https://unpkg.com/vue@next">script>
        <script>
            const { ref, watchEffect } = Vue;
            const state = ref(0)
    
            watchEffect(() => {
                document.body.innerHTML = `count is ${state.value}`
            })
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    此时取值需要访问 state 对象的 value 属性。当 ref 作为渲染上下文中的属性返回(从 setup 返回的对象)并在模板中访问时,它将自动展开为内部值,不需要在模板中添加 .value。代码如下:

    DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>title>
    head>
    
    <body>
        <div id="app">
            <span>{{ count }}span>
            <button @click="count ++">Increment countbutton>
        div>
        <script src="https://unpkg.com/vue@next">script>
        <script>
            const { ref } = Vue;
            const app = Vue.createApp({
                setup() {
                    const count = ref(0);
                    return {
                        count
                    }
                }
            })
            app.mount('#app')
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    当 ref 作为响应式对象的属性被访问或更改时,它会自动展开为内部值,其行为类似于普通属性。如下:

      const count = ref(0)
      const state = reactive({
          count
      })
      console.log(state.count); //0
      state.count = 1;
      console.log(count.value); //1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ref 展开仅在嵌套在响应式对象内时发生,当从数组或本地集合类型(如 Map)中访问 ref 时,不会执行展开操作。如下:

    const books = reactive([ref('你好')])
    // 需要添加 .value
    console.log(books[0].value)
    
    const map = reactive(new Map([['count',ref(0)]]))
    // 需要添加 .value
    console.log(map.get('count').value);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    11.3.6 readonly

    有时候希望跟踪响应对象(ref 或 reactive)的变化,但还希望阻止从应用程序的某个位置对其进行更改。
    例如,有一个提供的响应式对象时,想要防止它在注入的地方发生更改,为此,可以为原始对象创建一个只读代理。如下:

    import { reactive,readonly } from 'vue'
    const original = reactive({ count : 0})
    const copy = readonly(original)
    // 改变original将触发依赖 copy 的观察者
    original.count++
    // 修改copy 将失败并导致警告
    copy.count++
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    11.3.7 computed

    computed() 方法与 computed 选项作用一样,用于创建依赖于其它状态的计算属性,该方法接收一个 getter 函数,并为 getter 返回的值返回一个不可变的响应式 ref 对象。如下:

    const count = ref(1)
    const plusOne = computed(() => count.value+1) 
    console.log(plusOne.value) //2
    plusOne.value++ // error
    
    • 1
    • 2
    • 3
    • 4

    使用 组合API 实现反转字符串:

    DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>计算属性title>
    head>
    
    <body>
        <div id="app">
            <p>原始字符串: {{ message }}p>
            <p>计算后的反转字符串: {{ reversedMessage }}p>
        div>
    
        <script src="https://unpkg.com/vue@next">script>
        <script>
            const { ref, computed } = Vue;
            const vm = Vue.createApp({
                setup() {
                    const message = ref('你好,世界!');
                    const reversedMessage = computed(() =>
                        message.value.split('').reverse().join('')
                    );
                    return {
                        message,
                        reversedMessage
                    }
                }
            }).mount('#app');
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    与 computed 选项一样,computed() 方法也可以接受一个带有 get() 和 set() 函数的对象来创建一个可写的 ref 对象。如下:

    const count = ref(1)
    const plusOne = computed({
      get:() => count.value + 1,
      set:() => {
        count.value = val -1
      }
    })
    plusOne.value = 1
    console.log(count.value) //0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    另一个示例:

    DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>计算属性的getter和settertitle>
    head>
    
    <body>
        <div id="app">
            <p>First name: <input type="text" v-model="firstName">p>
            <p>Last name: <input type="text" v-model="lastName">p>
            <p>{{ fullName }}p>
        div>
    
        <script src="https://unpkg.com/vue@next">script>
        <script>
            const { ref, computed } = Vue;
            const vm = Vue.createApp({
                setup() {
                    const firstName = ref('Smith');
                    const lastName = ref('Will');
                    const fullName = computed({
                        get: () => firstName.value + ' ' + lastName.value,
                        set: val => {
                            let names = val.split(' ')
                            firstName.value = names[0]
                            lastName.value = names[names.length - 1]
                        }
                    });
                    return {
                        firstName,
                        lastName,
                        fullName
                    }
                }
            }).mount('#app');
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    11.3.8 watch

    watch() 方法等同于 Vue 2.x 的 this.$watch()方法,以及相应的 watch 选项。watch() 方法需要监听特定的数据源,并在单独的回调函数中应用副作用。默认情况下,它也是惰性的,即只有当被监听的数据发生变化是,才会调用回调函数。
    与 watchEffect() 方法相比,watch() 方法有以下功能:

    • 惰性的执行副作用
    • 更具体的说明什么状态应该触发监听器重新运行
    • 访问被监听状态的前一个值和当前值

    watch() 与 watchEffect() 方法共享行为,包括手动停止、副作用失效(将 onInvalidate 作为第 3 个参数传递给回调)、刷新时间和调试。
    监听的数据源可以是返回值的 getter 函数,也可以是直接的 ref 对象。如下:

    const state = reactive({ count : 0 })
    // 监听返回值的 getter 函数
    watch(
      () => state.count,
      (count,prevCount) => {
        // ...
      }
    )
    
    // 直接监听一个 ref 对象
    watch(count,(count,prevCount) => {
      // ...
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    监听器还可以使用数组同时监听多个数据源,如下:

    watch([fooRef,barRef],([foo,bar],[prevFoo,prevBar]) => {
      // ...
    })
    
    • 1
    • 2
    • 3
  • 相关阅读:
    5、Spring之Bean生命周期源码解析(销毁)
    华为OD机试真题-堆内存申请-2023年OD统一考试(C卷D卷)
    C语言--分段函数--switch语句
    学习教授LLM逻辑推理11.19
    sklearn【F1 Scoree】F1分数原理及实战代码!
    关于JDK8中的字符串常量池和String.intern()方法理解
    Java遍历集合元素并修改
    算法练习15——加油站
    聚水潭和金蝶云星空接口打通对接实战
    wordpress网站搭建(centos stream 9)
  • 原文地址:https://blog.csdn.net/GXL_1012/article/details/127987574