听说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数据代理有以下优点:

  • 对象中新增属性可以被监听到
  • 数组中修改索引值可以被监听到
  • 数组中调用pushpop等修改数据的函数也能被监听到

缺点:

  • 数组中在调用pushpop等函数时,会涉及到对数组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的值时,如果值变更了,数据是对象就包装成响应式数据,触发依赖更新

至此,我们分析了一下 reactiverefAPI的实现原理,下面我们分析下容易造成这两个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 作者:夏目斑

(0)
上一篇 2023年5月10日 上午10:42
下一篇 2023年5月10日 上午10:52

相关推荐

发表回复

登录后才能评论