2024-04-25 vue3 ref reactive 源码响
响应式的实现
1:reactive
shallowReadonly:只读属性,只对第一层代理,不可以修改第一层属性值,一般不用
shallowReactive: 只对第一层代理,可以修改第一层的属性值,一般不用
reactive:对所有属性,包括深层次的属性都进行代理,全部可以修改。
readonly:对所有属性进行代理,包括深层次的属性,全都不可以修改。性能优化
使用proxy代理数据源,在get中进行依赖收集,在set中触发渲染。如果是只读,就不支持set,如果是shallow,只代理第一层属性。
vue2中的代理是直接递归全都代理,而在3中是懒代理,只有使用了才会去走代理。3中代理实现的方式是proxy。
在代理的时候,vue会对已经代理的数据缓存进weakmap,当新数据进行代理,会先从weakmap找,找不到才会进行代理。
2:依赖收集 effect
effect函数只要是用于依赖收集,具体流程是,effect接收一个函数,在函数执行的时候如果使用reactive或者ref变量,就会触发该变量的get方法,在get方法中会将当前活跃的effect放到全局变量 targetMap中。
effect可能会套effect
vue的处理方法是将每个effect放到一个栈中,当前的effect执行完毕后,将当前的effect从栈中剔出,然后将上一个effect设置为活跃的effect。先进后出
const a = reactive({
b: 1,
c: 2
})
effect(() => {
a.b
effect(() => {
a.c
})
})
在一个effect中调用多次同一个响应式变量,只收集一次。
const a = reactive({
b: 1
})
effect(() => {
a.b
a.b
})
在effect栈中,保证只存一个相同的effect,解决办法在往数组中添加时做下判断
每一个属性的依赖收集中保证只收集一次的方法:在vue中有一个全局变量targetMap,这个map存储着所有的响应式属性的依赖收集,具体结构是
targetMap = {a: {
b: {effect: effect._trackId}
// 这个map存储着引用b的effect,在给b的map添加effect时,
//会根据effect的_trackId判断是否已经存在。主要用于过滤重复收集
}}
3:get依赖收集
如果是对象,直接在get方法中将effect放入targetMap中即可。
如果是数组的方法,会进行特殊处理。
具体处理逻辑如下:
['includes', 'indexOf', 'lastIndexOf']
如果是 这三个方法,会对数组的每一个属性进行依赖收集
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
['push', 'pop', 'shift', 'unshift', 'splice']
如果是 这几个方法,会直接修改数组,接着返回,不进行依赖收集。注意:这几个方法虽然是改变数据源,但是走的是get,而不是set。
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling()
resetTracking()
return res
}
})
length
如果是length,走正常的收集逻辑
下标
如果是直接通过下标获取,则进行对象逻辑处理。走正常的收集逻辑。在targetMap中,数组的下标就是key,effect就是value
3:set触发更新
在修改响应式属性的值的时候,会触发代理的set方法,在这个方法中会先判断是新增还是修改,如果是修改会先看老值和新值是否相等,接着就是根据key获取targetMap中的相对应的effect对象,遍历执行相对应的effect函数即可。
如果是直接修改数据的长度,这里的处理是获取使用length对应的effect,以及大于数组新长度的effect。
源码处理逻辑:
if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
deps.push(dep)
}
})
}
4: ref实现
ref内部实现了一个 RefImpl class类,借助于get和set实现的响应式收集和触发更新,本质还是和vue2一样借助于object.defineproperty实现的。
这块要注意的是ref在内部会做下判断,如果接收的是一个对象会走reactive函数,实现响应式。reactive的返回值会赋给 RefImpl的_value属性。