Vue 3 中的数据基于 JavaScript Proxy (代理) 实现响应式
( vue2 中的数据通过 Object.defineProperty() 方法和对数组变异方法的重写,实现响应式)
选项式 API
用 data 选项声明响应式状态,值为返回一个对象的函数。
- 在创建组件实例的时候会调用此函数
- 函数返回的对象会用响应式系统进行包装
data() { return { count: 1 } },
注意事项
- data 的顶层属性避免用
$
和_
开头(Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀,也为内部属性保留 _ 前缀。) - 未在data中定义的组件实例新属性,无法触发响应式更新。
- 始终通过 this 来访问响应式状态
data() { return { someObject: {} } }, mounted() { const newObject = {} this.someObject = newObject console.log(newObject === this.someObject) // false }
这里原始的 newObject 不会变为响应式。
组合式 API
方式一 ref 【推荐】
所有类型的数据,都推荐使用 ref 声明响应式。
import { ref } from 'vue' // 定义一个初始值为数字 0 的响应式变量 countRef const countRef = ref(0) // 得到一个带有 .value 属性的 ref 对象 console.log(countRef ) // { value: 0 } // 通过 .value 访问 ref 响应式变量的值 console.log(countRef.value) // 0 // 通过 .value 修改 ref 响应式变量的值 countRef.value++
- 参数为响应式变量的初始值(当参数是一个对象时,
ref()
会在内部调用reactive()
) - 返回一个带有 .value 属性的 ref 对象
- 建议变量名以
Ref
为后缀,以便识别其是一个 ref 对象
- 模板中使用时,ref 对象会自动解包,无需使用.value
<button @click="countRef++"> {{ countRef }} </button>
- ref 对象在作为响应式对象的属性被访问或修改时会自动解包。
const countRef = ref(0) // reactive 的用法详见下文 const state = reactive({ count:countRef }) console.log(state.count) // 0 ,无需写成 state.count.value state.count = 1 // 无需写成 state.count.value console.log(countRef .value) // 1
- ref 的另一个功能是获取模板引用(即 vue2 中的 this.$refs)
<script setup> import { ref, onMounted } from 'vue' // 获取到 ref 属性为 input 的模板元素的引用(ref 变量名必须和模板里的 ref 属性值相同) const input = ref(null) // 生命周期:DOM 挂载完成 onMounted(() => { // input 输入框获得焦点(注意此处需使用.value) input.value.focus() }) </script> <template> <!-- ref 属性为 input 的模板元素 --> <input ref="input" /> </template>
为什么需要 ref
因为 vue3 的响应式是通过 proxy 实现的,但 proxy 只能给对象添加响应式,无法给值类型的数据添加响应式,所以需要通过 ref() 函数先将值类型的数据包装成一个带 value 属性的 ref 对象,才能实现值类型数据的响应式。
为什么需要 .value
因为 ref() 函数返回的是一个 ref 对象,响应式变量的值存储在 ref 对象的value 属性中,所以在 js 中读取和修改响应式变量的值的时候,都需要 .value ,但在以下情况中,为了方便使用,vue 对 ref 对象进行了自动解包,所以无需 .value
- 模板中使用 ref 对象
- ref 对象作为响应式对象的属性被访问或修改
TS 中给 ref 声明类型
ref 会根据初始化时的值推导其类型
// 推导出的类型:Ref<number> const year = ref(2020)
但比较复杂的数据类型,需用 Ref
import { ref } from 'vue' import type { Ref } from 'vue' interface userInfo { id: number name: String } const userList: Ref<userInfo[]> = ref([])
方式二 reactive
用于创建对象类型数据的响应式。
import { reactive } from 'vue' const state = reactive({ count: 0 })
<button @click="state.count++"> {{ state.count }} </button>
- reactive() 无需对源数据添加类似 ref 的包装,可使对象本身具有响应性
- reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的
const raw = {} const proxy = reactive(raw) // 代理对象和原始对象不是全等的 console.log(proxy === raw) // false
reactive() 的局限性
- 只能用于对象类型数据 (对象、数组和如 Map、Set 这样的集合类型),不能用于 string、number 或 boolean 这样的值类型数据
- 替换整个对象会丢失响应式(Vue 的响应式跟踪是通过属性访问实现的),所以只能通过修改属性来修改 reactive() 创建的响应式变量。
- 解构会丢失响应式
const state = reactive({ count: 0 }) // 当解构时,count 已经与 state.count 断开连接 let { count } = state // 不会影响原始的 state count++
其他相关 API
vue 提供了专门的API 来实现在不丢失响应式的情况下,解构响应式对象(reactive 创建),它们不创造响应式 (如 ref , reactive),而是延续响应式 (toRef , toRefs) !
toRef()
基于响应式对象上的属性,创建 ref (与源属性保持同步)
- 第一个参数为 响应式对象(reactive 创建)
- 第二个参数为 属性名
const state = reactive({ foo: 1 }) // 基于响应式对象 state 上的属性 foo ,创建 ref const fooRef = toRef(state, 'foo') // 更改该 ref 会更新源属性 fooRef.value++ console.log(state.foo) // 2 // 更改源属性也会更新该 ref state.foo++ console.log(fooRef.value) // 3
https://cn.vuejs.org/api/reactivity-utilities.html#toref
toRefs()
将响应式对象转换为每个属性都指向源对象相应属性的 ref 的普通对象
- 推荐用法:合成函数返回响应式对象时,使用 toRefs,以便解构赋值时不丢失响应式。(详见下文的范例)
const state = reactive({ foo: 1, bar: 2 }) // 得到一个每个属性都是 ref 的普通对象 const stateAsRefs = toRefs(state) /* stateAsRefs 的类型:{ foo: Ref<number>, bar: Ref<number> } */ // 源属性改变,Refs 对象里的 ref 属性会同步改变 state.foo++ console.log(stateAsRefs.foo.value) // 2 // Refs 对象里的 ref 属性改变,源属性会同步改变 stateAsRefs.foo.value++ console.log(state.foo) // 3
应用范例:解构对象属性,简化模板引用
<!-- 组合式API --> <script setup> import { reactive, toRefs } from "vue"; function getMyInfo() { const me = reactive({ name: "朝阳", age: 35, }); // 使用 toRefs 避免丢失响应式 return toRefs(me); } // 通过解构赋值,便于模板中使用 let { name, age } = getMyInfo(); const changeName = () => { name.value = "张三"; }; </script> <template> <p>姓名:{{ name }}</p> <p>年龄:{{ age }}</p> <button @click="changeName">改名</button> </template>
isRef()
判断是否为 ref
let age = ref(35) if (isRef(age)) { }
isReactive()
判断一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。
isProxy()
判断一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理。
unref()
对 ref 对象解包,是 isRef(val) ? val.value : val
的语法糖