平时在Vue3的项目开发中,大家使用最多的是响应式核心API:
此外,Vue3还提供了10个响应式进阶API,在某些场景下也是非常好用的,下面介绍一下这些API的用途,对它们的源码做个简要分析
1、shallowRef()
和
ref()
不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对.value
的访问是响应式的。
shallowRef()
常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。
简化源码:
js
// /packages/reactivity/src/ref.ts
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
function createRef(rawValue: unknown, shallow: boolean) {
return new RefImpl(rawValue, shallow) // shallow为true
}
class RefImpl<T> {
private _value: T;
public dep?: Dep = undefined //存储副作用函数
// 第二个参数 公共属性 __v_isShallow 为 true
constructor(value: T, public readonly __v_isShallow: boolean) {
// 不会调用toReactive去转换为深层次响应
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this) // 访问.value,收集依赖
return this._value; // 返回传入的value
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
triggerRefValue(this, newVal) //触发更新
}
}
}
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
// 追踪副作用函数
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
// ref.dep是Set结构,方便去重
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep;
return dep;
};
export function trackEffects(dep: Dep) {
//将当前活跃的副作用函数收集到ref.dep中
dep.add(activeEffect!);
activeEffect!.deps.push(dep);
}
shallowRef返回的数据结构:
示例:
用于那些只需要跟踪引用变化而不关心内部属性变化,对所有深层的对象不会做任何处理,这使得对深层级属性的访问也会变得更快
js
import { shallowRef, watch } from "vue";
// 不需要关心对象里面属性改变
const myData = shallowRef({ name: "John", age: 30, height: 188 });
watch(myData, () => {
console.log("trigger");
});
// 修改属性,不触发监听
myData.value.name = "James";
// 修改.value引用,才触发监听
myData.value = { name: "James" };
js
import { shallowRef, computed } from "vue";
const myData = shallowRef(
Array(50)
.fill(0)
.map((_, index) => ({ id: index }))
);
const firstTen = computed(() => {
return myData.value.slice(0, 9);
});
// 修改.value引用,firstTen重新计算
myData.value = [1, 2, 3];
// 数组内部属性改变,firstTen不会重新计算
myData.value[0] = "first";
myData.value.unshift("first");
shallowRef
还可以将 Vue 的响应性系统与外部状态管理方案集成,将外部状态放在一个 shallowRef
中。一个浅层的 ref 中只有它的 .value
属性本身被访问时才是有响应性的,而不关心它内部的值。当外部状态改变时,替换此 ref 的 .value
才会触发更新。
js
// 将redux集成到vue中
// store/index.js
import { createSlice, configureStore } from "@reduxjs/toolkit";
import { shallowRef } from "vue";
// 创建一个切片counterSlice
const counterSlice = createSlice({
name: "counter",
initialState: {
count: 1,
},
reducers: {
increment: (state) => {
state.count++;
},
},
});
// 创建一个store
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
// 将store的状态放在shallowRef中
export const shallowState = shallowRef(store.getState());
// 订阅store状态变化,重新给.value赋值
store.subscribe(() => {
shallowState.value = store.getState();
});
export const { increment } = counterSlice.actions;
js
// shallowState是浅响应的,在template中使用,render函数被收集依赖中
// dispatch派发,subscribe中订阅最新状态,修改shallowState.value,模板更新
<script setup>
import { store, increment, shallowState } from "./store/index";
function handleClick() {
// 派发更新
store.dispatch(increment());
}
</script>
<template>
<button @click="handleClick">浅响应 shallowRef</button>
<p>{{ shallowState.counter.count }}</p>
</template>
2、triggerRef()
强制触发依赖于一个
shallowRef
的副作用,这通常在对浅引用的内部值进行深度变更后使用
简化源码:
js
// /packages/reactivity/src/ref.ts
export function triggerRef(ref: Ref) {
// 在shallowRef的set方法中,也是调用此函数触发更新
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref);
const dep = ref.dep;
if (dep) {
triggerEffects(dep);
}
}
// /packages/reactivity/src/effect.ts
export function triggerEffects(dep: Dep | ReactiveEffect[]) {
const effects = isArray(dep) ? dep : [...dep];
// 把dep中存储的副作用函数,遍历执行
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect);
}
}
}
function triggerEffect(effect: ReactiveEffect) {
if (effect !== activeEffect || effect.allowRecurse) {
effect.run();
}
}
示例:
当对一个大型数据用shallowRef
浅响应后,又确实需要在内部某个属性变化,触发副作用更新,而且没必要改成ref
深层响应,就手动triggerRef
触发更新
js
<script setup>
import { shallowRef, watchEffect, triggerRef } from "vue";
const shallow = shallowRef({
greet: "Hello, world",
});
// 触发该副作用第一次打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet);
});
function handleClick() {
// 不会触发副作用,因为是浅层的响应
shallow.value.greet = "Hello, universe";
// 手动触发
triggerRef(shallow);
}
</script>
<template>
<button @click="handleClick">triggerRef</button>
<p>{{ shallow.greet }}</p>
</template>
3、customRef()
customRef()
预期接收一个工厂函数作为参数,这个工厂函数接受track
和trigger
两个函数作为参数,并返回一个带有get
和set
方法的对象。一般来说,
track()
应该在get()
方法中调用,而trigger()
应该在set()
中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
简化源码:
js
// /packages/reactivity/src/ref.ts
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
return new CustomRefImpl(factory) as any
}
class CustomRefImpl<T> {
public dep?: Dep = undefined
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
public readonly __v_isRef = true
// 非常清晰,传入的工厂函数factory,收到两个函数参数
//() => trackRefValue(this) 收集副作用
// () => triggerRefValue(this) 触发副作用更新
// 自己决定何时何处调用来收集和触发副作用
constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get
this._set = set
}
get value() {
// 调用自定义的get返回
return this._get()
}
set value(newVal) {
//触发自定义的set
this._set(newVal)
}
}
示例:
4、shallowReactive()
和
reactive()
不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
简化源码:
js
// /packages/reactivity/src/reactive.ts
export function shallowReactive<T extends object>(
target: T
): ShallowReactive<T> {
return createReactiveObject(target, false, shallowReactiveHandlers);
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>
) {
const proxy = new Proxy(target, baseHandlers);
return proxy;
}
// /packages/reactivity/src/baseHandlers.ts
export const shallowReactiveHandlers = new MutableReactiveHandler(true);
class MutableReactiveHandler extends BaseReactiveHandler {
//参数shallow为true
constructor(shallow = false) {
super(false, shallow);
}
}
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly _shallow = false
) {}
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly; // false
const shallow = this._shallow; // true
const res = Reflect.get(target, key, receiver);
if (!isReadonly) {
// 收集依赖
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
//直接返回res,不会走到下一步把res递归变成响应式
return res;
}
if (isRef(res)) {
// 数组和整数key跳过,其他会解包返回,也就是返回res.value
// 由于是浅响应,在上一步if中返回了
// 也就是官网说的值为 ref 的属性不会被自动解包了
return isArray(target) && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// 如果 isReadonly 是 true,那么直接返回 readonly(res)
// 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
return isReadonly ? readonly(res) : reactive(res);
}
}
}
示例:
见官网 shallowReactive 示例;
reactive可以对访问ref值属性自动解包,shallowReactive不能
js
const count = ref(0);
const obj = reactive({ count });
const shallowObj = shallowReactive({ count });
console.log(count.value === obj.count); // true
console.log(obj.count); // 0
console.log(count.value === shallowObj.count); //false
console.log(shallowObj.count);
/**
RefImpl {
dep: undefined
__v_isRef: true
__v_isShallow: false
_rawValue: 0
_value: 0
value: 0
}
*/
官网中还提示到
浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。
js
const state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
});
// 把浅响应数据放到深层次响应对象中,可能会给自己挖坑
const obj = reactive({ state });
// 不会触发更新,因为state已经是一个浅响应式对象,
// 在reative中读取到state属性已经是一个响应式对象,直接返回不会再往下递归转换
obj.state.nested.bar++;
5、shallowReadonly()
和
readonly()
不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
简化源码:
js
// /packages/reactivity/src/reactive.ts
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return createReactiveObject(target, true, shallowReadonlyHandlers);
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>
) {
const proxy = new Proxy(target, baseHandlers);
return proxy;
}
// /packages/reactivity/src/baseHandlers.ts
export const shallowReadonlyHandlers = new ReadonlyReactiveHandler(true);
class ReadonlyReactiveHandler extends BaseReactiveHandler {
// 传递参数 shallow 为true
constructor(shallow = false) {
super(true, shallow);
}
}
class BaseReactiveHandler implements ProxyHandler<Target> {
// 接手2个参数都为true,即浅只读
constructor(
protected readonly _isReadonly = false,
protected readonly _shallow = false
) {}
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly; //true
const shallow = this._shallow; //true
const res = Reflect.get(target, key, receiver);
// isReadonly为true,根层级的属性被读取时不收集依赖
// 也就是根层级的属性变成了只读
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// shllow为true 直接返回res,
if (shallow) {
// res如果是一个响应式对象,这里并没有递归去把里面的属性变成只读
// 所以对res里面的属性读取依然会收集依赖,修改依然会触发更新
return res;
}
//...
}
}
再来看一下readonly
源码的不同之处
js
// 不同之处仅在于:
// readonly 没有传参数true,也就是get里面的 shallow 为默认的false
export const readonlyHandlers = new ReadonlyReactiveHandler()
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly; //true
const shallow = this._shallow; // false
const res = Reflect.get(target, key, receiver);
// 递归每一层级的属性走到这里,都不会收集依赖
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// false ,进入下一步
if (shallow) {
return res;
}
// res是ref值
if (isRef(res)) {
// 和reactive一样会解包,返回res.value,因为key没有收集依赖,只读
return isArray(target) && isIntegerKey(key) ? res : res.value
}
// res是对象
if (isObject(res)) {
// isReadonly 为true,递归属性变成只读
return isReadonly ? readonly(res) : reactive(res);
}
}
示例:
详见 官网示例
6、toRaw()
toRaw()
可以返回由reactive()
、readonly()
、shallowReactive()
或者shallowReadonly()
创建的代理对应的原始对象。这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
因为响应式数据一旦修改,就会触发副作用函数更新,甚至更新UI界面,但是有些操作,不希望在计算属性、侦听器或其他具有自动依赖追踪的场景中触发更新,那就需要拿到原始数据修改
还有当需要将响应式对象传递给不支持响应式的第三方库或函数时,可以使用 toRaw
获取原始对象
简化源码:
js
// /packages/reactivity/src/reactive.ts
export const enum ReactiveFlags {
RAW = '__v_raw'
}
// 如果是原始对象,直接返回
// 如果是响应式对象,返回属性 __v_raw
export function toRaw<T>(observed: T): T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
// 无论是reactive,readonly,shallowReactive 还是shallowReadonly
// 都会将这个基类的get方法作为proxy的handler的get拦截读取操作
class BaseReactiveHandler implements ProxyHandler<Target> {
get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.RAW) {
return target;
}
}
}
示例:
js
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
const state = {
foo: 1,
nested: {
bar: 2,
},
};
const refState = ref(state);
// 如果ref数据.value的读取值是对象,也会调用reactive转换为响应式
// 故也能调用toRaw获取原始对象
console.log(toRaw(refState.value) === state); // true
7、markRaw()
将一个对象标记为不可被转为代理。返回该对象本身。
简化源码:
js
// /packages/reactivity/src/reactive.ts
export const enum ReactiveFlags {
SKIP = '__v_skip',
}
export const def = (obj: object, key: string | symbol, value: any) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}
// 给对象打上标记,增加属性 '__v_skip'
export function markRaw<T extends object>(value: T): Raw<T> {
def(value, ReactiveFlags.SKIP, true)
return value
}
// reactive,shallowReactive,readonly,shallowReadonly
// 都会调用createReactiveObject
// ref 会调用reactive,也会调用 createReactiveObject
// 对象使用markRaw标记属性__v_skip
// 在 createReactiveObject函数中会发挥作用
// reactive
export function reactive(target: object) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
// shallowReactive
export function shallowReactive<T extends object>(
target: T
): ShallowReactive<T> {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
)
}
// readonly
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}
// shallowReadonly
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}
function createReactiveObject(target: Target) {
// targt有属性__v_skip,返回 TargetType.INVALID
// 所以,对象被markRaw标记后,直接返回原对象,
// 不会往下走到proxy代理
const targetType = getTargetType(target);
if (targetType === TargetType.INVALID) {
return target;
}
//...
}
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value));
}
示例:
详见 官网示例
8、effectScope()
创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理
关于这个api的介绍,github上有个 RFC。
这个api是提供给写库和插件用,写业务组件基本用不着,effectScope就是为了在组件外使用computed,watch的时候,更方便的管理副作用的收集和销毁,防止内存泄漏。
在组件内使用,不需要,在组件初始化执行setup的时候,computed和watch会被调用,他们产生的副作用effect会被自动收集绑定到当前组件实例上,在组件卸载的时候,他们也会被清除,
源码中,组件初始化和卸载时:
js
// /vue-3.3.7/packages/runtime-core/src/component.ts
// 创建组件实例
export function createComponentInstance(vnode: VNode) {
const instance: ComponentInternalInstance = {
vnode,
// 收集组件中副作用
// 至于如何收集下面有分析
scope: new EffectScope(true),
// ...
};
return instance;
}
// 卸载组件
const unmountComponent = (instance: ComponentInternalInstance) => {
const { scope } = instance;
// stop effects in component scope
scope.stop();
};
但是在组件外或者第三方包里面使用dcomputed和watch就需要手动收集和清除副作用了, 例如:
js
const disposables = []
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
disposables.push(() => stop(doubled.effect))
const stopWatch1 = watchEffect(() => {
console.log(`counter: ${counter.value}`)
})
disposables.push(stopWatch1)
const stopWatch2 = watch(doubled, () => {
console.log(doubled.value)
})
disposables.push(stopWatch2)
// 清除副作用
disposables.forEach((f) => f())
disposables = []
可以看到,手动收集副作用是很麻烦,也很容易忘记收集它们,这可能会导致内存泄漏和其他意外行为。这个时候就需要一个api来实现大一统,方便收集和清除。
官网示例:
js
const scope = effectScope()
// 会收集computed,watch,watchEffect创建的副作用
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()
effectScope简要源码
js
// /vue-3.3.7/packages/reactivity/src/effectScope.ts
let activeEffectScope: EffectScope | undefined
export function effectScope(detached?: boolean) {
return new EffectScope(detached)
}
export class EffectScope {
private _active = true;
// 收集的副作用
effects: ReactiveEffect[] = [];
// 收集清理副作用时的回调
cleanups: (() => void)[] = [];
// 父scope
parent: EffectScope | undefined;
// 收集子scope
scopes: EffectScope[] | undefined;
// 当前scope在父scope中的位置
private index: number | undefined;
constructor(public detached = false) {
this.parent = activeEffectScope;
// 当detached为false,this.index就是effectScope在父scope中的索引
// 如果为true,跟父scope分离,不被父scope收集
if (!detached && activeEffectScope) {
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this
) - 1;
}
}
get active() {
return this._active;
}
run<T>(fn: () => T): T | undefined {
if (this._active) {
const currentEffectScope = activeEffectScope;
try {
// 先将activeEffectScope设置当前effectScope实例
activeEffectScope = this;
// 执行fn,fn里面执行computed、watch,
// 将它们创建的副作用收集到当前 activeEffectScope
return fn();
} finally {
activeEffectScope = currentEffectScope;
}
}
}
on() {
activeEffectScope = this;
}
off() {
activeEffectScope = this.parent;
}
stop(fromParent?: boolean) {
if (this._active) {
let i, l;
// 清除副作用
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop();
}
// 执行onScopeDispose()传入的回调
for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]();
}
// 递归清理子scope,
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true);
}
}
// 从父scope中删除自己
if (!this.detached && this.parent && !fromParent) {
const last = this.parent.scopes!.pop();
if (last && last !== this) {
this.parent.scopes![this.index!] = last;
last.index = this.index!;
}
}
this.parent = undefined;
this._active = false;
}
}
}
// 更新scope中的副作用effects
// 在computed,watch里面会用到收集副作用
export function recordEffectScope(
effect: ReactiveEffect,
scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
scope.effects.push(effect)
}
}
在组件初始化时,会将全局activeEffectScope
设置为组件scope
js
// /vue-3.3.7/packages/runtime-core/src/component.ts
function setupStatefulComponent(instance: ComponentInternalInstance) {
const Component = instance.type as ComponentOptions;
const { setup } = Component;
if (setup) {
setCurrentInstance(instance);
}
}
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
// 当前 activeEffectScope = instance.scope
instance.scope.on()
}
在组件内执行setup()
时,收集副作用,此时activeEffectScope是instance.scope(组件实例scope); 在组件外执行scope.run()
时,收集副作用,此时activeEffectScope就是调用run函数的scope。
真正执行收集是在调用computed、watch的时候:
computed 执行收集副作用
js
///vue-3.3.7/packages/reactivity/src/computed.ts
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
const cRef = new ComputedRefImpl(getter);
return cRef as any;
}
export class ComputedRefImpl<T> {
public readonly effect: ReactiveEffect<T>;
constructor(getter: ComputedGetter<T>) {
this.effect = new ReactiveEffect(getter, () => {});
}
}
class ReactiveEffect {
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
// 此时scope为undefined
recordEffectScope(this, scope)
}
}
export function recordEffectScope(
effect: ReactiveEffect,
// 没传,默认全局的 activeEffectScope
scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
// 如果是在组件中执行,那么此时scope就是组件instance.scope
// 如果是在scope.run()里面执行,此时scope就是effectScope创建的scope
scope.effects.push(effect)
}
}
watch收集副作用:
js
// /packages/runtime-core/src/apiWatch.ts
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
return doWatch(source as any, cb, options);
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
const effect = new ReactiveEffect(getter, scheduler);
// ...
}
// 接下来跟computed里面一样
9、getCurrentScope()
获取当前活跃的effect作用域,可能是组件scope,或者是通过effectScope创建的scope
js
// /vue-3.3.7/packages/reactivity/src/effectScope.ts
export function getCurrentScope() {
return activeEffectScope
}
10、onScopeDispose()
在当前活跃的
effect 作用域
上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。这个方法可以作为可复用的组合式函数中
onUnmounted
的替代品,它并不与组件耦合,因为每一个 Vue 组件的setup()
函数也是在一个 effect 作用域中调用的。
js
export function onScopeDispose(fn: () => void) {
if (activeEffectScope) {
// 在上面effectScope的源码中,执行stop方法时,
// 会将cleanups中所有回调遍历执行
activeEffectScope.cleanups.push(fn)
}
}