前言:
针对vue3
官网中, 响应式:进阶
API 中, 我们在上一章中给大家讲解了shallowRef
, shallowReactive
, shallowReadonly
几个API的使用.
本章主要对剩下的API 进行讲解, 我们先看一下官网中进阶API 都有那些
对于剩下这些API, 你需要了解他们创建目的, 是为了解决之前的API存在的那些痛点问题, 这样你就能更好的了解使用他们的细节.工作中就可以有的放矢的选择不同的API.
1. triggerRef
我们首先来分析一下triggerRef
API 的使用
1.1. triggerRef 针对的痛点问题
我们先看一个痛点问题:
对于ref
响应式数据的变化, vue
帮我们处理副作用. 比如,页面的更新, watchEffect
侦听器回调函数的调用等.
但对于浅层响应数据, 比如shallowRef
创建的数据, 其深层并不具有响应性, 也就是说vue
并没有监测这些数据的变化, 当对深层数据进行修改时, 并不会触发副作用, 比如页面不会自动刷新.
triggerRef
API 就是为了解决shallowRef
浅层响应式数据深层修改问题.
当深层修改时, 会强制触发依赖于一个浅层 ref
的副作用,这通常在对浅引用的内部值进行深度变更后使用。
1.2. triggerRef 类型
类型:
function triggerRef(ref: ShallowRef): void
triggerRef
API 函数接收一个shallowRef
API 创建的数据, 作用就是强制触发这个浅层ref
数据的副作用.
1.3. triggerRef 使用示例
示例:
<template>
<div>
<h3>shallowReadonly</h3>
<div>{{ count }}</div>
<div>{{ count2 }}</div>
<button @click="change">修改数据源</button>
</div>
</template>
<script lang="ts">
import { defineComponent, readonly, ref, shallowReadonly, shallowRef, triggerRef, watchEffect } from 'vue'
export default defineComponent({
setup() {
const count = ref({ num: 0 })
const count2 = shallowRef({ num: 0 })
// 对于ref 数据, 是深层响应式,
// 因此当我们通过count.value.num++ 修改数据时,依然会触发watchEffect副作用函数
watchEffect(() => {
console.log('count.value.num', count.value.num)
})
// 因为shallowRef 数据不是深层响应式, 只有.value 整体修改才会触发响应式
// 因为当我们通过count2.value.num++ 修改数据时,不会出发watchEffect 副作用函数
// 同时视图也不会发生更改
watchEffect(() => {
console.log('count2.value.num', count2.value.num)
})
// 修改数据
const change = () => {
// count.value.num++
count2.value.num++
// 如果希望shallowRef 深层数据修改后,触发视图更新
// 那么就需要使用triggerRef 手动触发更新
triggerRef(count2) // 手动更新count2
}
return { count, count2, change }
}
})
</script>
通过示例的运行结果, 你也可以看出. shallowRef
创建响应式数据, 在深层数据发生变化时, 不会触发页面更新 和watchEffect
的处理函数. 因为深层不具有响应性.
当我们手动调用triggerRef
函数, 并将shallowRef
创建数据作为参数, 就是告诉vue
, 我们需要强制执行shallowRef
数据的副作用. 此时页面将会更新, watchEffect
处理函数也会自动执行
1.4. triggerRef 使用小结
在理解triggerRef
API 的使用后, 针对该API, 我做了以下小结
triggerRef
常与shallowRef
搭配使用triggerRef
会强制更新以shallowRef
数据作为依赖的副作用,ref
数据会自动触发这些副作用
我们需要注意的是: vue3
只提供了triggerRef
这个方法,但没有提供triggerReactive
的方法。 也就是说triggerRef
【不可以】去更改 shallowReactive
创建的数据
2. toRaw
根据一个 Vue 创建的代理返回其原始对象
2.1. toRaw 针对的问题
在vue3
中, 我们通过 reactive()
、readonly()
、shallowReactive()
shallowReadonly()
四个API 创建的响应式数据, 本质上就是通过Proxy
创建的代理对象.
但有时我们在做数据传输时, 我们并不需要传响应式数据, 我们只想传最基本的原始对象.
toRaw
API 的作用就是返回 reactive()
、readonly()
、shallowReactive()
,shallowReadonly()
创建的代理对应的原始对象。
2.2. toRaw 类型
toRaw 函数签名
function toRaw<T>(proxy: T): T
toRaw
API 函数接收一个Proxy
代理对象(响应式对象)作为参数,
2.3. toRaw 使用示例
示例:
<template>
<div>
<h3>shallowReactive</h3>
<div>{{ user }}</div>
<button @click="change">修改数据源</button>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRaw } from 'vue'
export default defineComponent({
setup() {
// 代理目标对象
const obj = { name: '张三', age: 18 }
// reactive 处理的代理对象
const user = reactive(obj)
// 控制触发代理对象
console.log('user', user)
// 使用toRaw, 参数是代理对象, 返回代理对象的目标对象
console.log('toRaw(user)', toRaw(user))
console.log('toRaw(user) === obj', toRaw(user) === obj) // true
// 修改数据
const change = () => {
user.name = '李四'
}
return { user, change }
}
})
</script>
通过控制台输出结果, 你可以看出, toRaw
就是获取代理对象的原目标对象.
这是一个可以用于临时读取而不引起代理访问/跟踪
开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
这句话来自于官网, 这句话你可以这么理解,
代理对象具有响应性, 可以理解为vue
在监测这个数据的变化, 这个监测会消耗性能. 如果你的操作不要触发副作用, 就没有必要 使用具有响应性的代理对象.
比如调用接口时传入的参数, 就可以使用toRaw
去掉代理对象的外壳, 获取到原始对象传入接口.
3. markRaw
markRaw
函数的作用就是将一个对象转为不可代理对象.
如果使用reactive
API , 也不会代理markRaw
函数返回的对象, 会直接返回原对象.
示例:
<template>
<div>
<h3>shallowReactive</h3>
<div>{{ user }}</div>
<button @click="change">修改数据源</button>
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw, reactive } from 'vue'
export default defineComponent({
setup() {
// 代理目标对象
const obj = { name: '张三', age: 18 }
// 将obj原始对象标记为不可代理
const markObj = markRaw(obj)
// reactive 处理的代理对象
const user = reactive(markObj)
// user 不是代理对象
console.log('user', user)
// 修改数据
const change = () => {
user.name = '李四'
}
return { user, change }
}
})
</script>
控制台输出:
通过控制台输出结果, 可以看出, 通过markRaw
处理过的对象具有一个__v_skip
的属性, 用于标记这个对象不能创建代理对象, 即响应式数据.
尽管你将该对象传入reactive
, 返回的也不是一个代理对象, 而是原对象.
既然不是响应数据,修改user.name
时, 就不会触发视图更新
该API的作用就是, 帮助你给一些你不希望创建为代理对象的原始对象添加标记.
4. effectScope
4.1. effectScope 作用
在vue3
的使用过程中,我们可能会针对同一个响应式数据创建多个副作用.比如computed
, watch
, watchEffect
等.
再次过程中, 如果关闭某个副作用, 比如watch
创建的侦听器, 就需要通过返回值关闭. 那么多个副作用你就需要一个一个关闭. 使用相对麻烦
effectScope
字面意思就是副作用作用域, 可以理解为, 该函数创建一个作用域, 将所有的副作用放在共同一个作用域中, 如果以后想统一关闭副作用, 就可以使用作用域整体关闭.
4.2. effectScope
类型
function effectScope(detached?: boolean): EffectScope
interface EffectScope {
run<T>(fn: () => T): T | undefined // 如果作用域不活跃就为 undefined
stop(): void
}
effectScope
函数返回一个作用域对象, 即EffectScope
类型.
该作用域对象上具有run
, stop
方法, 同时run
方法接收一个回调函数作为参数.
4.3. effectScope 使用方式
通过effectScope
函数创建一个 effect
作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。
示例:
<template>
<div>
<h3>shallowReactive</h3>
<div>{{ count }}</div>
<button @click="change">修改数据源</button>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, effectScope, markRaw, reactive, ref, shallowReactive, toRaw, watch, watchEffect } from 'vue'
export default defineComponent({
setup() {
// 创建ref 数据
const count = ref(10)
// 创建副作用作用域
const scope = effectScope()
// 控制台输出 effect 作用域
console.log("scope", scope);
// 收集运行的副作用
scope.run(() => {
// 计算属性副作用
const computedCount = computed(() => count.value * 2
)
// watch 侦听副作用
watch(
count,
() => {
console.log('computedCount', computedCount.value)
console.log('watch count', count.value)
}
)
// watchEffect 副作用
watchEffect(() => {
console.log('watchEffect count', count.value)
})
})
console.log('scope', scope)
// 2秒以后关闭所有的副作用
setTimeout(() => {
scope.stop()
}, 2000)
// 修改数据
const change = () => {
count.value++
}
return { count, change }
}
})
</script>
控制台输出结果:
通过控制台输出的effect
作用域对象, 你可以看到, 作用域将回调函数中的副作用进行了收集, 存储在effects
属性上.
同时effect
作用域对象原型对象上具有run
收集副作用的方法, stop
关闭副作用的方法.
5. getCurrentScope
getCurrentScope
函数返回当前活跃的 effect 作用域。
在前一个API中, 给大家讲解了effectScope
函数, 该函数执行后会返回一个effect
作用域, 通过调用effect
作用域对象的run
方法收集所有副作用. 我们就可以在run
方法的回调函数中, 通过getCurrentScope
函数获取到正在活跃的effect
作用域对象.
示例:
// 创建副作用作用域
const scope = effectScope();
console.log("scope", scope);
// 收集运行的副作用
scope.run(() => {
// 计算属性副作用
const computedCount = computed(() => count.value * 2);
// watch 侦听副作用
watch(count, () => {
console.log("computedCount", computedCount.value);
console.log("watch count", count.value);
});
// watchEffect 副作用
watchEffect(() => {
console.log("watchEffect count", count.value);
});
// 通过 getCurrentScope() 获取当前真正活跃的 effect 作用域对象
const effectScope = getCurrentScope();
console.log("getCurrentScope", effectScope === scope);
// 控制台输出结果: getCurrentScope true
});
示例中, 我们通过effectScope
创建了一个effect
作用域对象, 当调用该作用域对象的run
方法,传入回调函数, 会自动执行回调函数, 收集副作用, 并将收集到的副作用保存在副作用effect
作用域中. 也就是说, 在执行回调函数时, 我们创建的scope
就是活跃的effect
作用域
之后,我们通过执行getCurrentScope
函数获取当前活跃的副作用
作用域, 和之前我们创建的作用域对比, 发现getCurrentScope
获取的就是我们创建的effect
作用域.
其实每一个组件都有一个effect
作用域, 用于收集组件内所有的副作用. 组件更新函数本身也就是一个副作用. 这也就是响应式数据变化后, 页面会重新渲染的原因.
以及组件被销毁后, vue3
会通过组件的effect
作用域清理组件内收集的所有副作用
该API 在工作中并不常使用到. 甚至一个项目里连一次都不会用到.
6. onScopeDispose
该API 函数主要用于调试, 工作中也不怎么常用, 其作用就是在当前活跃的副作用(effect)
作用域对象上注册一个调试的回调函数. 在effect
作用域关闭时, 会自动调用注册的回调函数,.
示例:
// 创建副作用作用域
const scope = effectScope();
console.log("scope", scope);
// 收集运行的副作用
scope.run(() => {
// 计算属性副作用
const computedCount = computed(() => count.value * 2);
// watch 侦听副作用
watch(count, () => {
console.log("computedCount", computedCount.value);
console.log("watch count", count.value);
});
// watchEffect 副作用
watchEffect(() => {
console.log("watchEffect count", count.value);
});
// 在当前活跃的 effect 作用域对象上注册一个回调函数
onScopeDispose(() => {
console.log("当前effectScope 停止");
});
});
// 2秒以后关闭所有的副作用
setTimeout(() => {
scope.stop();
}, 2000);
示例中, 我们在effectScope
收集副作用时, 通过onScopeDispose
函数注册了一个回调函数.
在effectScope
副作用作用域, 即scope
对象调用stop
方法时, 会自动执行注册的回调函数. 多用于功能调试
7. 结语
至此, 就把vue3
中响应式进阶API 中剩余的API函数给大家讲完了, 这里比较常用的API 有triggerRef
, toRaw
, markRaw
, effectScope
, 其余两个API 函数并不怎么常用.
这里尤其要注意effectScope
, 使用好了可以给代码增色不少.