响应式类方法

在配置式 API 中,我们一般将需要响应式的变量定义在 data 选项的属性里面,而在 Vue 3 的组合式 API 的 setup 方法中,我们还无法访问 data 属性,但是也可以定义响应式变量,主要用到 toReftoRefsrefreactive 和一些其他方法,其中有一些我们之前的代码中已经用过了,下面就来详细介绍一下它们的用法和区别。

ref 和 reactive

ref 方法

ref 方法用于 为数据添加响应式状态,既可以支持基本的数据类型,也可以支持复杂的对象数据类型,是 Vue 3 中推荐的定义响应式数据的方法,也是基本的响应式方法。需要注意的是:

  • 获取和修改数据值的时候需要加 .value。在模板中,Vue 会自动解包 ref 的值,因此你可以直接使用 变量 而无需使用 .value

  • ref 的本质是原始数据的拷贝,改变 简单类型数据 的值不会同时改变原始数据。

使用方法如示例代码 4-3-1 所示。

示例代码4-3-1 ref 方法

<div id="app">
    <component-b />
</div>
const componentB = {
    template: '<div>{{ name }}</div>',
    setup(props) {

        // 为基本数据类型添加响应式状态
        const name = Vue.ref('John')

        let obj = {count: 0};
        // 为复杂数据类型添加响应式状态
        const state = Vue.ref(obj)

        console.log(name.value); // 打印 John
        console.log(state.value.count); // 打印 0

        let newobj = Vue.ref(obj.count) // 注意是简单数据类型

        // 修改响应式数据不会影响原数据
        newobj.value = 1

        console.log(obj.count) // 打印 0

        return {
            name
        }
    }
}

Vue.createApp({
    components: {
        'component-b': componentB
    }
}).mount("#app")

需要注意的是,改变的这个数据必须是简单数据类型,即一个具体的值,这样才不会影响原始数据,如上面的代码中的 obj.count

ref 和对象

ref 也可以用于存储对象或数组。当你使用 ref 包裹对象时,Vue 会使这个对象的引用变成响应式的。需要注意的是,如果你想要改变对象内部的属性,仍然需要通过 .value 来访问它。

ref 包裹对象时,对象的响应式是深层的,这意味着当你将一个对象包裹在 ref 中时,Vue 会将这个对象的所有属性都变成响应式的。因此,任何对该对象属性的修改都会触发视图更新。

import { ref } from 'vue';

export default {
  setup() {
    const user = ref({
      name: 'John',
      age: 30
    });

    const changeName = () => {
      user.value.name = 'Jane';  // 通过 .value 修改对象属性
    };

    return {
      user,
      changeName
    };
  }
};

在这个例子中,user 是一个响应式的对象,当我们修改 user.value.name 时,Vue 会自动检测并更新视图。

ref 和数组

同样地,ref 也可以用于数组,Vue 会使数组变得响应式。

import { ref } from 'vue';

export default {
  setup() {
    const items = ref([1, 2, 3]);

    const addItem = () => {
      items.value.push(4);  // 数组操作
    };

    return {
      items,
      addItem
    };
  }
};

在这个例子中,items 是一个响应式数组。当我们通过 items.value.push(4) 增加一个新元素时,Vue 会自动更新数组并反映到界面上。

ref 和模板引用

ref 不仅可以用于响应式数据,还可以用于在模板中创建 DOM 元素的引用。你可以通过 ref 访问 DOM 元素或组件实例。

<template>
  <input ref="inputRef" type="text" />
  <button @click="focusInput">Focus Input</button>
</template>

<script>
export default {
  setup() {
    const inputRef = ref(null);

    const focusInput = () => {
      // 访问 DOM 元素并调用方法
      inputRef.value.focus();
    };

    return {
      inputRef,
      focusInput
    };
  }
};
</script>

关键点:

  • inputRef 被定义为 ref(null),它用来引用模板中的 input 元素。

  • inputRef.value 指向实际的 DOM 元素,inputRef.value.focus() 会让输入框获得焦点。

  • 在模板中,ref="inputRef" 将该 DOM 元素与 inputRef 变量关联起来。

在这个例子中,inputRef 用来引用 <input> 元素,使用 inputRef.value 可以访问该 DOM 元素并调用其方法,如 focus()

ref 和计算属性

你也可以使用 ref 与计算属性结合,实现更复杂的逻辑。例如,计算 ref 数据的某些派生值。

import { ref, computed } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const doubledCount = computed(() => count.value * 2);

    return {
      count,
      doubledCount
    };
  }
};

在这个例子中,doubledCount 是基于 count 的计算属性。当 count 发生变化时,doubledCount 会自动更新。

reactive 方法

reactive 方法用于为复杂数据添加响应式状态,只支持对象数据类型(如对象和数组),需要注意的是:

  • 获取数据值的时候不需要加 .value

  • reactive 的参数必须是一个对象,包括 JSON 数据和数组都可以,否则不具有响应式。

  • ref 一样,reactive 的本质也是原始数据的拷贝。

  • reactive 会对对象的每个属性(包括嵌套属性)进行递归处理,使得嵌套的对象和数组也变成响应式的。

ref 本质也是 reactiveref(obj) 等价于 reactive({value: obj}),使用方法如示例代码 4-3-2 所示。

<div id="app">
    <component-b />
</div>
const componentB = {
    template: '<div>{{ state.count }}</div>',
    setup(props) {

        // 为复杂数据类型添加响应式状态
        const state = Vue.reactive({count: 0})

        console.log(state.count); // 打印 0

        return {
            state
        }
    }
}

Vue.createApp({
    components: {
        'component-b': componentB
    }
}).mount("#app")

reactiveref 都是用来定义响应式数据的。reactive 更推荐定义复杂的数据类型,不能直接解构,ref 更推荐定义基本类型ref 可以简单地理解为是对 reactive 的二次包装,ref 定义数据访问的时候要多一个 .value

性能优化

在某些情况下,Vue 会对对象进行优化。Vue 3 对响应式对象使用了 Proxy,提供了高效的性能和灵活的响应式处理。但是需要注意,深层嵌套的对象可能会增加性能开销,尤其是当对象非常复杂时。如果你的对象不需要深层响应式,你可以考虑使用 ref 或手动将某些部分声明为非响应式(通过 markRaw)。

import { reactive, markRaw } from 'vue';

const state = reactive({
  user: markRaw({ name: 'Alice', age: 25 }),  // 不需要响应式的对象
  count: 0
});

toRef 和 toRefs

toRef 方法

toRef 方法我们在之前的 setup 方法中对 props 操作时已经使用过了,其一种使用场景是为 原响应式对象 上的属性新建单个响应式 ref,从而保持对其源对象属性的响应式连接。其接收两个参数:原响应式对象属性名,返回一个 ref 数据。例如使用父组件传递的 props 数据时,要引用 props 的某个属性且要保持响应式连接时就很有用。其另一种使用场景是接收两个参数:原普通对象属性名,此时可以对单个属性添加响应式 ref,但是这个响应式 ref 的改变不会更新界面。需要注意的是:

  • 获取数据值的时候需要加 .value

  • toRef 后的 ref 数据不是原始数据的拷贝,而是引用,改变结果数据的值也会同时改变原始数据。

  • 对于 原始普通数据 来说,新增加的单个 ref 改变,数据会更新,但是界面不会自动更新。

使用方法如示例代码 4-3-3 所示。

示例代码4-3-3 toRef 方法
<div id="app">
    <component-b user="John" />
</div>
const componentB = {
    template:'<div>{{statecount.count}}</div>',
    setup(props) {
        const state = Vue.reactive({ // 响应数据
            foo: 1,
            bar: 2
        })
        const fooRef = Vue.toRef(state, 'foo')
        fooRef.value++
        console.log(state.foo) // 打印2 会影响原始数据
        state.foo++
        console.log(fooRef.value) // 打印3 会影响fooRef数据
        const statecount = {// 普通数据
            count: 0,
        }
        const stateRef = Vue.toRef(statecount,'count')
        setTimeout(() => {
            stateRef.value = 1 // 界面不会更新
            console.log(statecount.count) // 打印1 会影响原始数据
        },1000)
        return {
            statecount,
        }
    }
}
Vue.createApp({
    components: {
        'component-b': componentB
    }
}).mount("#app")
const fooRef = Vue.toRef(state, 'foo')
const fooRef = ref(state.foo)

上面这个 ref 不会和 state.foo 保持同步,因为这个 ref() 接收到的是一个纯数值。

toRef 更多的使用场景是为对象添加单个响应式属性,而 toRefs 则是对完整的响应式对象进行转换。

toRefs 方法

toRefs 方法将原响应式对象转换为普通对象(可解构,但不丢失响应式),其中结果对象的每个属性都是指向原始对象相应属性的 ref,同时可以将 reactive 方法返回的复杂响应式数据进行 ES 6 解构。需要注意的是:

  • 获取数据值的时候需要加 .value

  • toRefs 后的 ref 数据不是原始数据的拷贝,而是引用,改变结果数据的值也会同时改变原始数据。

  • 如果我们直接对 reactive 返回的数据进行解构,这样会丢失响应式机制,采用 toRefs 包装并返回则会避免这个问题。

  • toRefs 只接收响应式对象参数,不可接收普通对象参数,否则会发出警告

toRefs 的作用是将一个响应式对象的每个属性包装成 ref。这对解构赋值特别有用,尤其是在你需要将响应式对象的属性传递给子组件或在某些函数中使用时。

Vue 3 中的响应式系统是基于 Proxy 实现的,它能够监控对象的属性变化。当你直接使用对象的属性时,Vue 会自动处理它的响应式特性。但当你对对象进行解构时,解构后的属性会丧失响应式特性。为了避免这种情况,我们可以使用 toRefs

使用方法如示例代码 4-3-4 所示。

示例代码4-3-4 toRefs 方法
<div id="app">
    <component-b  />
</div>
const componentB = {
    template:'<div>{{max}},{{count}}</div>',
    setup(props) {
        let obj = {
            count: 0,
            max: 100
        }
        const statecount = Vue.reactive(obj)
        const {count,max} = Vue.toRefs(statecount) // 方便解构
        setTimeout(()=>{
            statecount.max++
            console.log(obj.max) // 打印101 会影响原始数据,同时界面更新
        },1000)
        return {
            count,
            max
        }
    }
}
Vue.createApp({
    components: {
        'component-b': componentB
    }
}).mount("#app")

目前用得最多的还是使用 refreactive 来创建响应式对象,使用 toRefs 来转换成可以方便使用的解构的对象。

其它响应式类方法

shallowRef 方法、shallowReactive 方法和 triggerRef 方法

对于复杂对象而言,refreactive 都属于递归嵌套监听,也就是数据的每一层都是响应式的,如果数据量比较大,则非常消耗性能,而 shallowRefshallowReactive非递归监听,只会监听数据的第一层,如示例代码 4-3-5 所示。

示例代码4-3-5 shallowRef 方法、shallowReactive 方法和 triggerRef 方法
<div id="app">
    <component-b />
</div>
const componentB = {
    template:'<div>{{shallow1.person.name}} {{shallow2.person.name}} {{shallow2.greet}}</div>',
    setup(props) {
        const shallow1 = Vue.shallowReactive({
            greet: 'Hello, world',
            person: {
                name: 'John'
            }
        })

        const shallow2 = Vue.shallowRef({
            greet: 'Hello, world',
            person: {
                name: 'John'
            }
        })

        setTimeout(() => {
            // 这不会触发更新,因为 shallowReactive 是浅层的,只关注第一层数据
            shallow1.person.name = 'Ted'
        },2000)

        setTimeout(() => {
            // 这不会触发更新
            shallow2.value.person.name = 'Ted'
            // 这也不会触发更新
            shallow2.value.greet = 'Hi'
            // 只有当调用 triggerRef 会强制上面的更新
            Vue.triggerRef(shallow2)
        }, 1000)

        return {shallow1, shallow2}
    }
}
Vue.createApp({
    components: {
        'component-b': componentB
    }
}).mount("#app")

注意:如果是通过 shallowRef 创建的数据,那么 Vue 监听的是 .value 变化,并不是第一层的数据的变化。因此如果要更改 shallowRef 创建的数据可以调用 xxx.value = {},也可以使用 triggerRef 可以强制触发之前没有被监听到的更新。另外 Vue 3 中没有提供 triggerReactive,所以 triggerRef 不能触发 shallowReactive 创建的数据更新。

为什么需要 triggerRef

通常情况下,Vue 会自动跟踪 ref 对象的变化并相应地更新视图。然而,当你直接操作一个浅层 ref 对象的内部数据而不通过其外部引用时,Vue 可能不会察觉到这些变化。例如,当你直接修改一个复杂对象的内部属性时,Vue 可能不会检测到这些修改。

triggerRefVue 3 提供的一个函数,用于显式地触发 ref 的更新。

readonly 方法、shallowReadonly 方法和 isReadonly 方法

从字面意思上来理解,readonly 表示只读,可以将响应式对象标识成只读,当尝试修改时会抛出警告,shallowReadonly 方法设置第一层只读,isReadonly 方法判断是否为只读对象,如示例代码4-3-6所示。

示例代码4-3-6 readonly 方法、shallowReadonly 方法和 isReadonly 方法
<div id="app">
    <component-b />
</div>

const componentB = {
    template:'<div></div>',
    setup(props) {
        const obj = Vue.readonly({foo: {bar: 1}})

        console.log(Vue.isReadonly(obj)) // true

        obj.foo.bar = 2 // 失败警告:Set operation on key "bar" failed: target is readonly.

        const sobj = Vue.shallowReadonly({foo: {bar: 1}})

        sobj.foo.bar = 2 // 第二层可以修改

        return {}
    }
}
Vue.createApp({
    components: {
        'component-b': componentB
    }
}).mount("#app")

总结表格:

方法 功能 递归性 特点 适用场景

readonly

将对象或数组变为深层只读

深层

递归将每个属性变为只读,修改时抛出错误

防止修改对象或数组的任何属性

shallowReadonly

将对象或数组变为浅层只读

浅层

只将对象的第一层属性变为只读,嵌套属性仍然可修改

保护顶层属性,避免递归性能开销

isReadonly

检查对象是否为只读对象

-

返回布尔值,判断对象是否是只读对象,支持递归判断嵌套属性

检查对象是否已变为只读,调试或条件判断

isRef 方法、isReactive 方法和 isProxy方法

isRef 方法用于判断是否是 ref 方法返回对象,isReactive 方法用于判断是否是 reactive 方法返回对象,isProxy 方法用于判断是否是 reactive 方法或者 ref 方法返回对象。

总结表格:

方法 用途 返回值 特点 适用场景

isRef

判断一个值是否是由 ref 创建的响应式引用

true 或 false

判断值是否为 ref,可以是基本类型或对象的响应式引用。

调试、条件判断,确保操作的是 ref 类型。

isReactive

判断一个对象或数组是否是响应式对象

true 或 false

判断对象是否是通过 reactive 创建的深层响应式对象。

调试、条件判断,确保操作的是响应式对象。

isProxy

判断一个对象是否是一个代理对象(包括 reactive 和 readonly)

true 或 false

判断对象是否为 Vue 的代理对象(通过 Proxy 实现)。

调试、类型检查,确认对象是否为代理对象。

toRaw 方法和 makeRaw 方法

toRaw 方法可以返回一个响应式对象的 原始普通对象,可用于临时读取数据而无须承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。

makeRaw 方法可以标记并返回一个对象,使其永远不会成为响应式对象,如示例代码4-3-7所示。

示例代码4-3-7 toRaw 方法和 makeRaw 方法
<div id="app">
    <component-b />
</div>
    const componentB = {
        template:'<div>{{reactivecobj.bar}}</div>',
        setup(props) {
            const obj = { foo : 1}
            const reactivecobj = Vue.reactive(obj)
            const rawobj = Vue.toRaw(reactivecobj)
            console.log(obj === rawobj) // true
            setTimeout(()=>{
                rawobj.bar = 2 // 不会触发响应式更新
            },1000)
            const foo = {a:1} // foo无法通过reactive成为响应式对象
            console.log(Vue.isReactive(Vue.reactive(Vue.markRaw(foo)))) // false
            return {
                reactivecobj
            }
        }
    }
    Vue.createApp({
        components: {
            'component-b': componentB
        }
    }).mount("#app")