vue3状态流转记录

刚用vue3的时候觉得习惯了之后非常爽,比vue2比起来要灵活,ref、reactive等新api写几个页面也基本理解用法了。

但是逐渐深入后发现遇到一些疑难问题的时候,排查起来非常困难,其依赖收集比起vue2复杂太多,我在写pinia插件的时候尤其痛苦,
感觉心智负担比原来重多了,所以这篇文章记录一下几个api在使用的时候里面的各种set、get的不同处理方式。

reactive

先从最简单的,什么都不嵌套的来梳理下

测试代码

var data = {a:{b:33}}

var reactiveData = reactive(data)

var c = computed(()=>{
  return reactiveData.a.b
})
const vv = ()=>{
  reactiveData.a.b = 44
};

get收集逻辑

这里的依赖收集是通过computed来触发的

  • 因为是使用的reactive,所以一开始生成reactiveData的时候只有最外层的对象被proxy了

  • 在computed里执行reactiveData.a

    • 执行Reflect.get(target, key, receiver)得到结果命名为res

    • 执行track(target, "get" , key)把reactiveData.a进行依赖收集,放入computed这个activeEffect里

    • 判断res是否是对象( 是,res为{b:33} ),执行 reactive(res),把内部对象也转为响应式后返回

  • 接上一步返回的结果接着执行a.b

    • 跟上面很像,只是没有执行最后一步

总结一下,如果reactive收集复杂对象,

  1. 初始化时只把外层变为响应式
  2. 当触发get时会把需要的每一层对象都变为响应式且放进对应的activeEffect里,并且会存储在一个原对象为key,响应对象为value的map
  3. 再次触发时,直接去外层get获取的还是原生对象,然后去map里换取响应对象

set响应逻辑

reactiveData.a = {b:55}

  • Reflect.set(target, key, value, receiver) 把set赋值给原对象
  • 触发相关监听

reactiveData.a.b = 55

  • 先从reactiveData.a触发get获取到子对象
  • 触发子对象的set方法,然后就和上面一样了

set的逻辑跟预期的一样,没什么难度

ref

测试代码

var data = {a:{b:33}}
var reactiveData = ref(data)

var c = computed(()=>{
  return reactiveData.value.a.b
})
const vv = ()=>{
  reactiveData.value.a ={b:44}
};

初始化

new RefImpl创建一个对象,value存储的是reactive后的对象,网上有文章说ref会把对象的每一层都变成响应式,但是我调试发现也是只有最外层变为响应式,当触发get的时候才动态变成响应式的

get收集逻辑

  • trackRefValue方法把ref也当作依赖收集起来
  • 返回_value也就是响应对象,后面跟reactive逻辑一致

set响应逻辑

reactiveData.value ={a:{b:44}}

  • Object.is判断是不是原值,不是的话_rawValue存原值,_value存响应的值
  • 触发相关监听

reactiveData.value.a ={b:44}

  • 通过ref的get返回响应式的对象,然后就走的reactive相关的set

toref

测试代码

var data = {a:{b:33},c:44}
var reactiveData = ref(data)
let toRefValue = toRef(reactiveData.value,'c')

初始化

  • 触发ref的get获取到响应对象

  • 获取对象相应属性c的值

  • isRef方法判断值是否是响应式的,是直接返回,不是则返回通过new ObjectRefImpl(object, key, defaultValue)创建的对象

  • _object存储object也就是reactiveData.value,_key存储key也就是c属性

get

this._object[this._key]直接从原对象取值

set

this._object[this._key] = newVal; 直接修改原对象

总结一下toRef,只是给一个值包一层value,这样可以使得响应式生效,实际的依赖收集和触发都是其原本对象来实现的,所以使用toRef的对象需要是响应式的对象才生效

toRefs

测试代码

var data = {a:{b:33},c:44}
var reactiveData = reactive(data)
let toRefsData = toRefs(reactiveData)

初始化

  • 首先判断一下传入的对象一定是reactive、readonly包裹的才能走下一步

  • 创建一个空对象,遍历原对象执行toRef(object, key)然后返回

set get

因为是原始对象,所以不存在set,get

unref

测试代码

var data = {a:{b:33},c:44}
var refData = ref(data)
let unrefData = unref(refData)

初始化

  • isRef(ref) ? ref.value : ref 简单直接返回响应式的value

toRaw

测试代码

var data = {a:{b:33},c:44}
var refData = ref(data)
let toRawData = toRaw(refData)

初始化

  • const raw = observed && observed["__v_raw"] 判断传入参数是否有__v_raw这个内置属性
  • return raw ? toRaw(raw) : observed 如果有这个内置属性那么继续遍历,没有则直接返回

readonly

测试代码

var data = {a:{b:33},c:44}
var refData = ref(data)
let readonlyData = readonly(refData)

初始化

  • 跟reactive一样调用createReactiveObject方法,只是isReadonly传true,Handler 传的是readonlyHandlers

get

  • Reflect.get(target, key, receiver)依然是调用这个方法获取返回值,然后返回的对象再调用readonly方法进行包裹

set

  • 不允许修改,给一个警告

markRaw

初始化

  • Object.defineProperty 给原对象加__v_skip属性,后面创建响应式(注意是创建,而不是创建之后使用get、set的时候)如果有这个属性就直接返回原值

这里值得注意,如果原来的对象就是响应式的,那么使用markRaw包裹依然是有响应式的

总结

在把所有的方法实现方式都读了一遍之后发现,所有方法都是只处理最外层的对象,内层对象只有在get的时候再进行相关proxy操作

posted @ 2022-12-01 17:35  爱吃巧克力的狗  阅读(88)  评论(0编辑  收藏  举报