baseHandlers.ts 文件
在 baseHandlers.ts
文件中针对 4 种 proxy 实例定义了不对的处理器。
由于它们之间差别不大,所以在这只讲解完全响应式的处理器对象:
export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys }
处理器对五种操作进行了拦截,分别是:
- get 属性读取
- set 属性设置
- deleteProperty 删除属性
- has 是否拥有某个属性
- ownKeys
其中 ownKeys 可拦截以下操作:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
其中 get、has、ownKeys 操作会收集依赖,set、deleteProperty 操作会触发依赖。
get
get 属性的处理器是用 createGetter()
函数创建的:
// /*#__PURE__*/ 标识此为纯函数 不会有副作用 方便做 tree-shaking const get = /*#__PURE__*/ createGetter() function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { // target 是否是响应式对象 if (key === ReactiveFlags.isReactive) { return !isReadonly // target 是否是只读对象 } else if (key === ReactiveFlags.isReadonly) { return isReadonly } else if ( // 如果访问的 key 是 __v_raw,并且 receiver == target.__v_readonly || receiver == target.__v_reactive // 则直接返回 target key === ReactiveFlags.raw && receiver === (isReadonly ? (target as any).__v_readonly : (target as any).__v_reactive) ) { return target } const targetIsArray = isArray(target) // 如果 target 是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一 if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // 不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。 // 如果不用 Reflect 来获取,在监听数组时可以会有某些地方会出错 // 具体请看文章《Vue3 中的数据侦测》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10 const res = Reflect.get(target, key, receiver) // 如果 key 是 symbol 并且属于 symbol 的内置方法之一,或者访问的是原型对象,直接返回结果,不收集依赖。 if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') { return res } // 只读对象不收集依赖 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 浅层响应立即返回,不递归调用 reactive() if (shallow) { return res } // 如果是 ref 对象,则返回真正的值,即 ref.value,数组除外。 if (isRef(res)) { // ref unwrapping, only for Objects, not for Arrays. return targetIsArray ? res : res.value } if (isObject(res)) { // 由于 proxy 只能代理一层,所以 target[key] 的值如果是对象,就继续对其进行代理 return isReadonly ? readonly(res) : reactive(res) } return res } }
这个函数的处理逻辑看代码注释应该就能明白,其中有几个点需要单独说一下:
Reflect.get()
- 数组的处理
builtInSymbols.has(key)
为 true 或原型对象不收集依赖
Reflect.get()
Reflect.get()
方法与从对象 (target[key])
中读取属性类似,但它是通过一个函数执行来操作的。
为什么直接用 target[key]
就能得到值,却还要用 Reflect.get(target, key, receiver)
来多倒一手呢?
先来看个简单的示例:
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { target[key] = value } }) p.push(100)
运行这段代码会报错:
Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
但做一些小改动就能够正常运行:
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { target[key] = value return true // 新增一行 return true } }) p.push(100)
这段代码可以正常运行。为什么呢?
区别在于新的这段代码在 set()
方法上多了一个 return true
。我在 MDN 上查找到的解释是这样的:
set()
方法应当返回一个布尔值。
- 返回
true
代表属性设置成功。 - 在严格模式下,如果
set()
方法返回false
,那么会抛出一个TypeError
异常。
这时我又试了一下直接执行 p[3] = 100
,发现能正常运行,只有执行 push
方法才报错。到这一步,我心中已经有答案了。为了验证我的猜想,我在代码上加了 console.log()
,把代码执行过程的一些属性打印出来。
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { console.log('get: ', key) return target[key] }, set(target, key, value, receiver) { console.log('set: ', key, value) target[key] = value return true } }) p.push(100) // get: push // get: length // set: 3 100 // set: length 4
从上面的代码可以发现执行 push
操作时,还会访问 length
属性。推测执行过程如下:根据 length
的值,得出最后的索引,再设置新的置,最后再改变 length
。
结合 MDN 的解释,我的推测是数组的原生方法应该是运行在严格模式下的(如果有网友知道真相,请在评论区留言)。因为在 JS 中很多代码在非严格模式和严格模式下都能正常运行,只是严格模式会给你报个错。就跟这次情况一样,最后设置 length
属性的时候报错,但结果还是正常的。如果不想报错,就得每次都返回 true
。
然后再看一下 Reflect.set()
的返回值说明:
返回一个 Boolean 值表明是否成功设置属性。
所以上面代码可以改成这样:
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { console.log('get: ', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set: ', key, value) return Reflect.set(target, key, value, receiver) } }) p.push(100)
另外,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。
通过上面的示例,不难理解为什么要通过 Reflect.set()
来代替 Proxy 完成默认操作了。同理,Reflect.get()
也一样。
数组的处理
// 如果 target 是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一 if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) }
在执行数组的 includes
, indexOf
, lastIndexOf
方法时,会把目标对象转为 arrayInstrumentations
再执行。
const arrayInstrumentations: Record<string, Function> = {} ;['includes', 'indexOf', 'lastIndexOf'].forEach(key => { arrayInstrumentations[key] = function(...args: any[]): any { // 如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。 // 所以这里的 this 指向 receiver,即 proxy 实例,toRaw 为了取得原始数据 const arr = toRaw(this) as any // 对数组的每个值进行 track 操作,收集依赖 for (let i = 0, l = (this as any).length; i < l; i++) { track(arr, TrackOpTypes.GET, i + '') } // we run the method using the original args first (which may be reactive) // 参数有可能是响应式的,函数执行后返回值为 -1 或 false,那就用参数的原始值再试一遍 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 } } })
从上述代码可以看出,Vue3.0 对 includes
, indexOf
, lastIndexOf
进行了封装,除了返回原有方法的结果外,还会对数组的每个值进行依赖收集。
builtInSymbols.has(key)
为 true 或原型对象不收集依赖
const p = new Proxy({}, { get(target, key, receiver) { console.log('get: ', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set: ', key, value) return Reflect.set(target, key, value, receiver) } }) p.toString() // get: toString // get: Symbol(Symbol.toStringTag) p.__proto__ // get: __proto__
从 p.toString()
的执行结果来看,它会触发两次 get,一次是我们想要的,一次是我们不想要的(我还没搞明白为什么会有 Symbol(Symbol.toStringTag)
,如果有网友知道,请在评论区留言)。所以就有了这个判断: builtInSymbols.has(key)
为 true
就直接返回,防止重复收集依赖。
再看 p.__proto__
的执行结果,也触发了一次 get 操作。一般来说,没有场景需要单独访问原型,访问原型都是为了访问原型上的方法,例如 p.__proto__.toString()
这样使用,所以 key 为 __proto__
的时候也要跳过,不收集依赖。
set
const set = /*#__PURE__*/ createSetter() // 参考文档《Vue3 中的数据侦测》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10 function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) // 如果原来的值是 ref,但新的值不是,将新的值赋给 ref.value 即可。 if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else { // in shallow mode, objects are set as-is regardless of reactive or not } const hadKey = hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { if (!hadKey) { // 如果 target 没有 key,就代表是新增操作,需要触发依赖 trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 如果新旧值不相等,才触发依赖 // 什么时候会有新旧值相等的情况?例如监听一个数组,执行 push 操作,会触发多次 setter // 第一次 setter 是新加的值 第二次是由于新加的值导致 length 改变 // 但由于 length 也是自身属性,所以 value === oldValue trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }
set()
的函数处理逻辑反而没那么难,看注释即可。track()
和 trigger()
将放在下面和 effect.ts 文件一起讲解。
deleteProperty、has、ownKeys
function deleteProperty(target: object, key: string | symbol): boolean { const hadKey = hasOwn(target, key) const oldValue = (target as any)[key] const result = Reflect.deleteProperty(target, key) // 如果删除结果为 true 并且 target 拥有这个 key 就触发依赖 if (result && hadKey) { trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result } function has(target: object, key: string | symbol): boolean { const result = Reflect.has(target, key) track(target, TrackOpTypes.HAS, key) return result } function ownKeys(target: object): (string | number | symbol)[] { track(target, TrackOpTypes.ITERATE, ITERATE_KEY) return Reflect.ownKeys(target) }
这三个函数比较简单,看代码即可。