听说vue3数据的响应性会丢失?
刚从vue2转到vue3的朋友,大家一定都听说过vue3的响应式数据在某些情况下会丢失响应性。导致大家在setup()
中返回数据和在封装hooks
返回数据时都会战战兢兢颤颤巍巍,反复思考反复确认我这样返回没有问题吧?我返回的响应式数据的响应性不会丢失吧?
如果你也有这方面的困惑,那么请继续读下去,本文将全方位分析什么情况下vue3中的响应式数据的响应性会丢失,帮你在数据传递时,不再唯唯诺诺。
vue3数据响应式原理Proxy
Proxy对象用于创建一个对象
的代理,从而实现基本操作的拦截和定义。
代理对象
const origin = {};
const proxyObj = new Proxy(origin, {
get: (target, key , receiver) => {
console.log(`getting ${key}`);
return Reflect.get(target, key, receiver)
},
set: (target, key , value, receiver) => {
console.log(`setting ${key}`);
return Reflect.set(target, key, value, receiver)
}
});
origin.b = 'b'; // 什么都不会输出
proxyObj.a = 'a'; // 会输出 setting a
console.log(origin.a); // a
console.log(proxyObj.b); // getting b => b
注意: 只有Proxy返回的对象
在赋值或者取值时才会走到代理函数中;修改原对象或者从原对象中取值则不会
走到代理函数中。
代理deep对象
const origin = {};
const proxyObj = new Proxy(origin, {
get: (target, key , receiver) => {
console.log(`getting ${key}`);
return Reflect.get(target, key, receiver)
},
set: (target, key , value, receiver) => {
console.log(`setting ${key}`);
return Reflect.set(target, key, value, receiver)
}
});
console.log('=========start proxy origin ==========');
proxyObj.a = { name: 'a' }; // setting a
proxyObj.a.name = 'b'; // getting a
console.log('=========end set proxy origin ========');
代理数组
const list = [];
const proxyList = new Proxy(list, {
get: (target, key , receiver) => {
console.log(`getting ${key}`);
return Reflect.get(target, key, receiver)
},
set: (target, key , value, receiver) => {
console.log(`setting ${key}`);
return Reflect.set(target, key, value, receiver)
}
});
console.log('=========start set list=============');
list[0] = 1; // 什么都不会输出
console.log('=========end set list ==============')
console.log('=========start proxy proxyList ==========');
proxyList[1] = 2; // setting 1
proxyList.push(3);// getting push
// getting length
// setting 2
// setting length
console.log('=========end set proxy proxyList ========');
修改数组index索引值,能被代理对象监控到。
由上述例子可以看出Proxy
代理相较于vue2中的Object.defineProperty
数据代理有以下优点:
- 对象中新增属性可以被监听到
- 数组中修改索引值可以被监听到
- 数组中调用
push
、pop
等修改数据的函数也能被监听到
缺点:
- 数组中在调用
push
、pop
等函数时,会涉及到对数组length的监听
注意:Proxy只能代理对象,不能
代理基础数据类型数据。
所以vue3在组合式API中提供了两个将数据变成响应式的API,一个reactive()
将对象转换成响应式,一个ref()
将基础数据转换成响应式。
reactive
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 只保留了基础代码,去掉了一些只读、原生、Proxy对象缓存等代码
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
return proxy
}
可以看到,reactive返回了一个对象的Proxy代理对象,该Proxy对象监听了对象的get、set
方法。
createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 非只读数据
if (!isReadonly) {
// 如果是数组,并且是[push,pop, includes...]等直接返回值,不进行依赖收集
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 获取值
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
// 非只读数据进行依赖收集
track(target, TrackOpTypes.GET, key)
}
if (isRef(res)) {
// 如果是ref函数包裹的数据,直接进行数据返回
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// 如果是对象类型,则进行Proxy代理
return isReadonly ? readonly(res) : reactive(res)
}
// 返回结果信息
return res
}
}
主要流程:
- 非只读数据,判断是否是数组中的
includes、push、pop
等方法,如果是则直接值,不进行下面的操作。 - 获取值
- 如果是非只读数据,进行依赖收集
- 如果是ref类型数据,则直接返回
- 如果是值是个对象,则重新进行Proxy代理
createSetter()
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 只保留了主要流程
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
可以看出set
中最主要的操作就是当值有变化时,触发收集的依赖更新。
ref
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 收集依赖
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 依赖更新
triggerRefValue(this, newVal)
}
}
}
ref主要就是返回一个RefImpl
对象
RefImpl类主要流程:
- 构造函数中如果数据为对象则响应式,否则就返回数据
- 获取value时进行依赖收集,返回值数据
- 设置value的值时,如果值变更了,数据是对象就包装成响应式数据,触发依赖更新
至此,我们分析了一下 reactive
和ref
API的实现原理,下面我们分析下容易造成这两个API包裹的数据丢失响应性的场景。
场景
setup函数中直接结构返回reactive()返回的数据
// template
<div>{{ a }}</div> <!--会一直显示 a -->
//setup
setup() {
const reactiveObj = reactive({
a: 'a'
});
const onChangeA = () => {
reactiveObj.a = 'b';
console.log(reactiveObj.a); // b
}
return {
...reactiveObj,
onChangeA,
}
}
显然,template里的div会一直显示a,不会显示b,聪明的你一定已经知道答案了!
没错,上文我们知道reactive
返回的是Proxy对象,但是当...reactiveObj
将Proxy对象解构之后拿到的变量a就是一个普普通通的数值,不再有响应性。
变形
// template
<div>{{ a.a }}</div>
//setup
setup() {
const reactiveObj = reactive({
a: {
a: 'a'
}
});
const onChangeA = () => {
reactiveObj.a.a = 'b';
console.log(reactiveObj.a); // Proxy { a: 'b' }
}
return {
...reactiveObj,
onChangeA,
}
}
a.a
的值会随着点击事件的触发而变化么?
是的,会变化,我们再reactive()
函数中分析过get
方法,当对象的属性值还是对象时,那么还是会调用reactive()
将这层对象再次包裹成Proxy代理对象。所以结构之后的对象a
还是Proxy对象,所以会随着值的变化视图跟着变化。
reactive对象重新赋值
// template
<div>
{{ reactiveObj.a }}
</div>
// setup
setup() {
let reactiveObj = reactive({
a:'a'
});
reactiveObj = {
a: 'name'
}
const onChangeA = () => {
reactiveObj.a = 'b';
console.log(reactiveObj.a); // b
}
return {
reactiveObj,
onChangeA,
}
}
template视图中的数据会随着点击事件的触发而发生变化么?
是的,不会。因为只有被响应式处理的数据才会在修改数据时进行视图更新,我们给reactiveObj
重新赋值了一个没有响应性的对象,所以视图并不会更新。
封装hooks
function getReactiveObj() {
const reactiveObj = reactive({
a:'a'
});
return reactiveObj;
}
setup() {
let { a } = getReactiveObj();
const onChangeA = () => {
a = 'b';
console.log(a); // Proxy { a: 'b' }
}
return {
a,
onChangeA,
}
}
视图会随着a的值的变化而变化么?
是的,视图不会变化。本质还是解构带来的副作用,解构使数据的响应性丢失。不光是reactive包裹的数据,也包括props响应式数据,解构都可能造成这些数据的响应性丢失。
上述基本都是reative
对象重新赋值或者解构的场景下发生的响应式丢失场景,ref
数据则不太会发生响应性丢失的情况。因为给ref数据的.value赋值也会触发toReactive
并且进行依赖收集,除非是进行了很离谱的解构不然ref数据不太会出现响应性丢失。
解决方案
针对结构造成的数据的响应性丢失,有什么解决方案么?
可以尽量减少对响应式数据结构,如果实在避免不了,没关系,尤大大已经准备好了解决方案:
- toRef
- toRefs
示例
toRef
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
toRefs
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:{
foo: Ref<number>,
bar: Ref<number>
}
*/
源码
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): ToRef<T[K]> {
const val = object[key]
return isRef(val) ? val : (new ObjectRefImpl(object, key) as any)
}
export function toRefs<T extends object>(object: T): ToRefs<T> {
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(private readonly _object: T, private readonly _key: K) {}
get value() {
return this._object[this._key]
}
set value(newVal) {
this._object[this._key] = newVal
}
}
toRefs是toRef的多个key值版,如果想防止对象中的某个属性的响应式丢失,则使用toRef
函数;如果想防止整个对象因为结构导致响应式丢失,则使用toRefs
包裹整个对象。
至此,是否在vue3中对返回数据的响应性更有信心了呢?
转载自:https://juejin.cn/post/7231089810299027517