4-Vue源码之【响应式】
前言
在 Vue3
中,我们使用 Proxy
代替 Vue2
中的 defineProperty
进行数据绑定。这里就涉及到了 2 个重要的 API ,reactive
和 ref
。
在使用上,reactive
一般用来描述引用数据类型。 ref
用来描述基础数据类型偏多。
当然 ref
内部也做了处理,如果传递一个 引用数据类型 ,则会先进行一层 reactive
处理,在进行 ref
处理。
先简单了解下这 2 个 API 的实现原理。
Reactive
该方法接受一个对象,并返回一个 Proxy 对象, Proxy
相比之前的 defineProperty
针对 数组 有了更好的处理,不再需要像 Vue2 那样去重写数组方法了
export function reactive(target: object) {
// target 必须为对象,否则无法被 new Proxy 绑定
if (!isObject(target)) {
return target
}
// target 必须在 targetType 定义的六个类型当中,否则直接返回 target
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 这里会根据不同的 TargetType 去选择合适的 handler,比如 如果是 Map,Set 这种,那么只需要去处理 get 选择器即可
// 我们这里暂时只讨论 array 和 object 的情况
// const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)
const proxy = new Proxy(target, baseHandlers)
return proxy
}
/**
* 根据原始类型 确定不同的 TargetType 用于在后面的 响应式中处理。
*
* 比如: 针对 Map,Set 类型,重写其 get 方法
* 因为我们常规调用 const map = new Map() 都是调用 map.get('xx').yy
*
* @param rawType
* @returns
*/
function targetTypeMap(rawType: string) {
// 跟据 TargetType 使用不同的 proxyHandler
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
function getTargetType(value: Target) {
// toRawType 就是通过 Object.prototype.toString.call 去获取最原始的类型
return targetTypeMap(toRawType(value))
}
源码里先去判断 target
的类型,然后再去 new Proxy
,重点在于 baseHandlers
的处理方式。
1. baseHandlers
const get = createGetter()
const set = createSetter()
// 其他几个暂时不考虑,主要考虑 get追踪依赖 / set触发依赖 的情形
export const baseHandlers: ProxyHandler<object> = {
get,
set,
// deleteProperty,
// has,
// ownKeys
}
get 方法
// 调用get,收集依赖
const createGetter = () => {
return function get(target: Target, key: string | symbol, receiver: object) {
// 这些 ReactiveFlags 的参数,用户可能不会调用,但是其他方法里会用到,比如 toRaw 里
if (key === ReactiveFlags.RAW) {
return target
}
const targetIsArray = isArray(target)
// 针对数组进行特殊处理。
// arrayInstrumentations 里保存了一些数组的方法: includes , indexOf , push, pop 等
// Vue3 重写了这些方法并存放到了 arrayInstrumentations 里
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 这里就会去调用 arrayInstrumentations[key]
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 1. 返回 key 对应的数据 res (一般情况下,大部分都是返回这个)
const res = Reflect.get(target, key, receiver)
// 2. 建立跟踪(收集依赖,响应式系统里最重要的一步)
// 后面会解释
track(target, TrackOpTypes.GET, key)
// 3. 如果返回的数据也是 object 那么再进行一次 reactive
if (isObject(res)) {
// 这么做避免了一开始就对所有数据进行深层次遍历。
// 而是在调用该 key 时,如果发现其对应的数据是对象,那么就再次进行 reactive 代理绑定
return reactive(res)
}
return res
}
}
set 方法
// 调用set,触发更新
const createSetter = () => {
return function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
// 先获取旧值(更新之前, target[key] 里保存的还是旧值)
let oldValue = (target as any)[key]
// 如果 target 为数组,且 key 为正整数,就看 key 是否比 length 小,false 则说明是 新增
// 如果 target 为对象,那么直接用 hasOwn 去判断,是否存在,false 则说明是 新增
// 【注】 对于数组,有非常好的优化效果。由于 数组 变更长度,会导致 length 改变,所以会触发多次 set。
// 【注】 当遇到 length 发生 set 时,会走到 hasOwn(target, key) 这里,导致 hadKey 为 true
// 【注】 紧接着下方, hasChanged 判断 新旧value 是否发生变化,由于只是 length 改变,所以这一次虽然触发了 set,但是不会去触发 trigger ,避免了渲染
const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 触发更新 trigger
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
// 之前想过针对数组,只考虑数组 length 发生 set 的时候去触发 trigger
// 后来发现不行,因为 vue 可以使用 watch, computed, effect 等去单纯依赖数组中的某一个元素
// 比如: const item = computed(()=> list[3]) 我这里只依赖了 list[3] ,那么 computed 的 effect函数就会被加入到 list[3] 的deps中
// 这样,我去更新 list 的值,就会触发到 list[3] 的 deps ,继而触发到触发到 computed
// if (key == 'length') {
// trigger(target, TriggerOpTypes.ADD, key, value)
// }
return result
}
}
get
和 set
中,响应式相关的最重要的就是 track
和 trigger
了,当然还有些小细节,比如上面说到的:
-
get
的时候再去判断是否需要进行深层次reactive
-
利用
hadKey
避免数组的多次渲染(数组发生长度变化时,会调用多次 set)
QA.1. 我们的数据是在何时 track 的?(即:何时调用的 get?)
主要有 2 种地方,JS部分 和 模板部分
Js 层的执行即可调用,模板层实际上在最后转换成 `Vnode` 时,也是属于了 JS 层,所以也是执行即可 track
【注】关于 track 和 trigger 需要结合 effect 啃,这里先单纯记住作用
Ref
由于我们的 Proxy
只接受对象,为了弥补这方面的不足,Vue3
创建了一个新的 API —— ref
用于对string, number 等基础数据类型进行处理
ref
无法使用 Proxy
,所以 Vue3
创建了一个 类, 类里面去设置了 get 和 set 的访问器
export const ref = <T>(tempValue: T) => {
return new RefImpl<T>(tempValue)
}
class RefImpl<T = unknown> {
private _value: T // 私有属性
public dep?: Dep // 这里存储的依赖是在 get 时存进去的一些方法,set 时就会调用这些方法。
constructor(defaultValue: T) {
// 构造函数进行判断,如果是对象,那么先用 reactive 进行 Proxy 处理,在用 _value 包裹一层
this._value = isObject(defaultValue) ? reactive(defaultValue as any) : defaultValue
}
get value() {
// track
trackRef(this)
return this._value
}
set value(val) {
// trigger
this._value = val
triggerRef(this)
}
}
【Tips】不一定使用 类,也可以使用普通对象,但这种:统一类,再调用实例的方式显得更香
QA.2. 为什么 ref 返回的数据,需要加上 .value?
因为采用了上面的 类的访问器 进行拦截,只有调用 `.value` 才是真正对数据拦截并处理
转载自:https://juejin.cn/post/7237516248803688504