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收集复杂对象,
- 初始化时只把外层变为响应式
- 当触发get时会把需要的每一层对象都变为响应式且放进对应的activeEffect里,并且会存储在一个原对象为key,响应对象为value的map
- 再次触发时,直接去外层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操作
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现