1、vue3-响应式 API:核心

218 阅读5分钟

一、声明响应式状态 ref()

  • 声明响应式状态,接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回(打印:RefImpl {value:Proxy {}});
  • 一般用于包装“基本数据类型”的响应式对象的实现方式,基本数据类型(如字符串、数字、布尔值等)
import { ref } from 'vue';//引入ref
const count = ref(0);定义一个响应式数据count,
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
  • 当 ref 的值是一个非原始值(对象)时,ref() 会在内部调用reactive()转换为响应式代理。
  • Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。
  • Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到;
const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}
  • 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 时会自动解包,我们不需要附加.value
<div>{{ count }}</div>

ref 解包细节

1、作为 reactive 对象的属性,

  • 一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:
const count = ref(0)
const state = reactive({
  count
})
console.log(state.count) // 0 ,无需写成 state.count.value
state.count = 1 // 无需写成 state.count.value
console.log(countRef.value) // 1
  • 如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
  • 只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。

2、数组和集合的注意事项

  • 与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型 (如 Map) 中的元素被访问时,它不会被解包:
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

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

3、在模板中解包的注意事项

  • 在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
const count = ref(0)
const object = { id: ref(1) }

//这个表达式按预期工作 - 自动解包
<div>{{ count + 1 }}</div>

//这个表达式不会按预期工作 - 不会自动解包,渲染的结果将是 [object Object]1
<div>{{ object.id + 1 }}</div>
//解决这个问题,我们可以将 id 解构为一个顶级属性
const { id } = object
//再去渲染
<div>{{ id.id + 1 }}</div>
  • 另一个需要注意的点是,如果 ref 是文本插值的最终计算值 (即 {{ }} 标签),那么它将被解包,因此以下内容将渲染为 1:
  • 该特性仅仅是文本插值的一个便利特性,等价于 {{ object.id.value }}。
{{ object.id }}

二、声明响应式状态 reactive(params)

  • 只能声明引用数据类型,返回的是一个原始对象params的响应式代理(Proxy对象)(打印:Proxy {})
  • 响应式转换是“深层”的:它会影响到所有嵌套的属性
  • 只有代理对象是响应式的,更改原始对象不会触发更新
let obj = reactive({  
  name: '小明',  
  age: 18  
})
obj.name;//小明
  • 在模板中使用:
<button>
  {{ state.age }}
</button>
  • 返回的是一个原始对象params的 Proxy,它和原始对象是不相等的
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
  • 为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

局限性

  • 1、有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
  • 2、不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用,赋值给 reactive 一个整个对象或 reactive 对象,会失去响应
let state = reactive({ count: 0 })  
// 以下2种整体赋值将导致 state 失去响应  
state = { count: 1 }
state = reactive({ count: 11 });
  • 2、对应解决方案
  • 不要直接整个对象替换,一个个属性赋值
  • 使用 Object.assign
  • 使用 ref 定义对象
  • 使用数组的 push 方法
//以下方式赋值都不会失去响应
let state = reactive({ count: 0 })  
state.count = 1
Object.assign(state, { count: 1 }) 
state = Object.assign(state, { count: 1 }) 

let state = ref({ count: 0 })  
state.value = { count: 1 } 
  • 3、对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接,ref定义对象解构也会失去响应式
const state = reactive({ count: 0 })

// 当解构时,count 已经与 state.count 断开连接,失去了响应性连接 
let { count } = state
// 不会影响原始的 state
count++ // state.count 值依旧是 0
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
  • 3、对应解决方案
const state = reactive({ count: 0 })
// 使用 toRefs 解构,后的属性为 ref 的响应式变量
let { count } = toRefs(state)
count.value++ // state.count 值改变为 1
  • 将 reactive 对象的属性赋值给变量(断开连接/深拷贝),对该变量的赋值不会影响原来对象的属性值。
let state = reactive({ count: 0 })  
// 赋值给 n,n 和 state.count 不再共享响应性连接  
let n = state.count  
// 不影响原始的 state  
n++  
console.log(state.count) // 0

三、readonly

  • 接受一个对象 (不论是响应式还是普通的) 或是一个ref,返回一个原值的只读代理
let rd = readonly({name:'哈哈'});
console.log(rd);//Proxy {name: "哈哈"}
rd.name = '新的'
console.log(rd.name);//哈哈