在 Vue 3 的源码中,所有关于响应式的代码都在 vue-next/package/reactivity 下,其中 reactivity/srcindex.ts 中暴露了所有可以使用的方法。我们以常用的ref()方法为例,来看看 Vue3 是如何利用 Proxy的。
ref()方法的主要逻辑在 reactivity/src/ref.ts 中,其代码如下:
// 入口方法
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
// rawValue 表示原始对象,shallow 表示是否递归
// 如果本身已经是 ref 对象,则直接返回
if (isRef(rawValue)) {
return rawValue
}
//创建一个新的 RefImpl对象
return new RefImpl(rawValue, shallow)
}
createRef 这个方法接收的第二个参数是 shallow,表示是否是递归监听响应式,这个和另一个响应式方法 shallowRef()是对应的。在 RefImpl 构造函数中,有一个 value 属性,这个属性是由toReactive()方法返回的,toReactive()方法则在 reactivity/src/reactive.ts 文件中,代码如下:
class RefImpl<T> {
...
constructor(value: T,public readonly _shallow: boolean) {
this. _rawValue = _shallow ? value : toRaw(value)
// 如果是非递归,则调用 toReactive
this. _value = _shallow ? value : toReactive(value)
}
...
}
在reactive.ts 中,开始真正创建一个响应式对象,代码如下:
export function reactive(target: object) {
// 如果是 readonly,则直接返回,就不添加响应式了
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target, // 原始对象
false, // 是否 readonly
mutableHandlers, // proxy的 handler 对象 baseHandlers
mutableCollectionHandlers, // proxy的 handler 对象 collectionHandlers
reactiveMap // proxy对象映射
}
}
其中,createReactiveObject()方法传递了两种 handler,分别是 baseHandlers 和 collectionHandlers.如果 target 的类型是 Map、Set、 WeakMap、WeakSet,这些特殊对象则会使用 collectionHandlers:如果 target 的类型是 Object、Array,则会使用 baseHandlers; 如果是一个原始对象,则不会创建 Proxy对象,reactiveMap 会存储所有响应式对象的映射关系,用来避免同一个对象重复创建响应式。我们再来看看 createReactiveObject()方法的实现,代码如下:
function createReactiveObject(...) (
// 如果 target 不满足 typeof val === 'object',则直接返回 target
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: $(String(target))`)
}
return target
}
// 如果 target 已经是 proxy 对象或者只读,则直接返回
// exception: calling readonly() on a reactive object
if (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
return target
}
//如果 target 已经被创建过 Proxy对象,则直接返回这个对象
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
//只有符合类型的 target 才能被创建响应式
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
//调用Proxy API 创建响应式
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
}
// 标记该对象已经创建过响应式
proxyMap.set(target, proxy)
return proxy
}
可以看到在 createReactiveObiect0方法中,主要做了以下事情
- 防止只读和重复创建响应式。
- 根据不同的 target 类型选择不同的 handler。
- 创建 Proxy 对象。
最终会调用 new Proxy 来创建响应式对象。我们以 baseHandlers 为例,看看这个 handler 是怎么实现的。在 reactivity/src/baseHandlersts 可以看到这部分代码主要实现了这几个 handler,代码如下:
const get = /*#__PURE__*/ createGetter()
...
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
以 handler.get 为例,看看在其内部进行了什么操作,当我们尝试读取对象的属性时,便会进入get 方法,其核心代码如下:
function createGetter(isReadonly = false,shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
// 如果访问对象的 key是__v_isReactive,则直接返回常量
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 如果访问对象的 key是__v_isReadonly,则直接返回常量
return isReadonly
} else if (
// 如果访问对象的 key是__v_raw,或者原始对象,只读对象等直接返回 target
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)) {
return target
}
// 如果 target 是数组类型
const targetIsArray = isArray(target)
// 并且访问的 key 值是数组的原生方法,那么直接返回调用结果
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 求值
const res = Reflect.get(target, key, receiver)
// 判断访问的 key 是否是 Symbol 或者不需要响应式的 key,例如__proto__,__v_isRef,__isVue
if (isSymbol(key) ? builtInSymbols.has (key) : isNonTrackablekeys (key)) {
return res
}
// 收集响应式,为了后面的 effect 方法可以检测到
if (!isReadonly) {
track(target,TrackOpTypes.GET, key)
}
// 如果是非递归绑定,则直接返回结果
if (shallow) {
return res
}
// 如果结果已经是响应式的,则先判断类型,再返回
if (isRef(res)) {
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldunwrap ? res.value : res
}
// 如果当前 key 的结果也是一个对象,那么就要递归调用 reactive 方法对该对象再次执行响应式绑定逻辑
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
// 返回结果
return res
}
}
上面这段代码是 Vue 3 响应式的核心代码之一,其逻辑相对比较复杂,读者可以根据注释来理解。总结下来,这段代码主要做了以下事情:
- 对于handler.get 方法来说,最终都会返回当前对象对应 key 的结果,即 obi[key],所以这段代码最终会return 结果。
- 对于非响应式 key、只读 key 等,直接返回对应的结果。
- 对于数组类型的 target,key 值如果是原型上的方法,例如 includes、push、pop 等,则采用Reflect.get 直接返回。
- 在effect 添加收集监听 track,为响应式监听服务。
- 当前 key 对应的结果是一个对象时,为了保证 set 方法能够被触发,需要循环递归地对这个对象进行响应式绑定,即递归调用 reactive()方法。
handler.get 方法的主要功能是对结果 value 进行返回。接下来我们看看 handler.set 主要做了什么,其代码如下:
function createSetter(shallow = false) {
return function set(
target: object,
key: string l symbol,
value: unknown,// 即将被设置的新值
receiver: object
) : boolean {
// 缓存旧值
let oldValue = (target as any)[key]
if (!shallow) {
// 新旧值转换原始对象
value = toRaw(value)
oldValue = toRaw(oldValue)
// 如果旧值已经是一个 RefImpl对象且新值不是 RefImp1对象
// 例如 var v= Vue.reactive({a:1,b:Vue.ref({c:3}))场景的 set
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 直接将新值赋给旧值的响应式对象
oldValue.value = value
return true
}
}
// 用来判断是新增 key 还是更新 key 的值
const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
// 设置 set 结果,并添加监听 effect 逻辑
const result = Reflect.set(target, key, value, receiver)
// 判断 target 有没有动过,包括在原型上添加或者删除某些项
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target,TriggerOpTypes.ADD,key,value) // 新增 key 的触发监听
} else if (hasChanged(value,oldValue)) {
// 更新 key 的触发监听
trigger(target,TriggerOpTypes.SET,key,value,oldValue)
}
}
// 返回 set 的结果 true/false
return result
}
}
handler.set 方法的核心功能是设置 key 对应的值,即obj[key]= alue,同时对新旧值进行逻辑判断和处理,最后添加 trigger 触发监听 track 逻辑,以便于触发 effect。
如果读者感觉上述源码理解起来比较困难,笔者剔除一些边界和兼容判断,对整个流程进行梳理和简化,可以参考下面这段代码:
let foo = {
a:{
c:3,
d:{ e 4 }
},
b:2
}
const isObject = (val) => {
return val !== null && typeof val === 'object'
}
const createProxy = (target) => (
let p = new Proxy(target, {
get: (obj,key) => {
let res = obj[key] ? obj[key] : undefined
// 添加监听
track(target)
// 判断类型,避免死循环
if (isObject(res)) {
// 循环递归调用
return createProxy(res)
) else {
return res
}
},
set: (obj,key,value) => (
console.log('set')
obj[key] = value;
// 触发监听
trigger(target)
return true
}
})
return p
}
let result = createProxy(foo)
result.a.d.e = 6 // 打印出 set
当尝试去修改一个多层嵌套的对象的属性时,会触发该属性的上一级对象的 get 方法,利用这个方法就可以对每个层级的对象添加 Proxy 代理,这样就实现了多层嵌套对象的属性修改问题,在此基础上添加 track 和 trigger 逻辑,就完成了基本的响应式流程。我们将在后面的章节结合双向绑定来具体讲解 track 和 trigger 的流程。