**
类似于渲染系统,vue3.0的响应式系统也有自己的一套flag,用于标记目标对象target(通常是我们传入的数据源)的一些特性
export const enum ReactiveFlags {
skip = '__v_skip',
isReactive = '__v_isReactive',
isReadonly = '__v_isReadonly',
raw = '__v_raw',
reactive = '__v_reactive',
readonly = '__v_readonly'
}
// 为目标对象target,即待转化为响应式对象的源对象做标记
interface Target {
__v_skip?: boolean // 跳过,不对target做响应式处理
__v_isReactive?: boolean // target是响应式的
__v_isReadonly?: boolean // target是只读的
__v_raw?: any // target对应的原始数据源,未经过响应式代理
__v_reactive?: any // target经过响应式代理后的数据源
__v_readonly?: any // target经过响应式代理后的只读数据源
}
createReactiveObject:
将target转化为响应式对象
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 被转化的target必须是object
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
return target
}
// target已经被转化为响应式对象,响应式对象的__v_readonly、__v_reactive分别挂载转化为
// proxy的只读、非只读的对象
if (
hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
) {
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// 只有target在可转化类型白名单里才会进行转化,否则直接返回target
// canObserve会校验target的__v_skip是否为true、target类型是否在白名单里、target是否
// 为冻结对象,三折同时满足才会继续转化
if (!canObserve(target)) {
return target
}
// 转化proxy对象,并将转化后的对象挂载到target的__v_readonly(只读模式)或__v_reactive(非只读模式)属性上
const observed = new Proxy(
target,
// Set、WeakSet、Map、WeakMap类型用的collectionHandlers,其他对象类型使用baseHandlers
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
def(
target,
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
observed
)
return observed
}
数组元素查找方法拦截重写:
响应式系统对数组的indexOf、lastIndexOf、includes进行了拦截重写,调用元素查找方法时,会对数组中的元素进行依赖收集,手机依赖之后,会优先执行Array原型上对应自带的查找方法,但是有可能元素是响应式数据,比如ref数据,原始value被转化为ref对象,因此需要更改为查找响应式数据对应的raw数据。
const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayInstrumentations[key] = function(...args: any[]): any {
// 重写的方法一定是通过proxy调用的,因此this指向proxy,所以对原数组操作的话
// 需要访问对应的原始数据__v_raw
const arr = toRaw(this) as any
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)
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
}
}
})
createGetter:
在createReactiveObject方法里创建Proxy对象时会为Proxy设置handlers,createGetter就是创建getter拦截器的。
涉及到Reflect的可以参考下面链接或自行官网查找:
Reflect:Reflect详解
其实中心思想很简单,记住一点就可以:Reflect中的receiver指向属性的实际调用者。比如,在Reflect配合Proxy使用时,通常会涉及到receiver这个参数,当我们访问某个key时,会触发Proxy对应的拦截器(handler),而receiver正是指向访问属性并触发handler的真正源头对象,我们触发handler自然是通过Proxy实例对象,此时分两种情况:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// 访问对应标志位的处理逻辑
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (
key === ReactiveFlags.raw &&
receiver ===
(isReadonly
? (target as any).__v_readonly
: (target as any).__v_reactive)
// receiver指向调用者,这里的判断是为了保证触发拦截handler的是proxy对象本身
// 而非proxy的继承者。触发拦截器的两种途径:1⃣️访问proxy对象本身的属性;2⃣️访问
// 访问对象原型链上有proxy对象的对象的属性,因为查询属性会沿着原型链向下游依次
// 查询,因此同样会触发拦截器
) {
// 通过proxy对象本身访问__v_raw属性,返回target本身,即响应式对象的原始值
return target
}
// 访问Array对象上的方法,vue3.0对数组的includes、indexOf、lastIndexOf方法
// 进行了重写,存储在arrayInstrumentations里
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 这里在调用indexOf等数组方法时是通过proxy来调用的,因此
// arrayInstrumentations[key]的this一定指向proxy实例
// 也即receiver
return Reflect.get(arrayInstrumentations, key, receiver)
}
// Proxy预返回值
const res = Reflect.get(target, key, receiver)
// key是symbol或访问的是__proto__属性不做依赖收集和递归响应式转化,直接返回结果
if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
return res
}
// 只读target无需收集依赖,因为属性不会变化,因此无法触发setter,也就不会触发依赖更新
if (!isReadonly) {
// 通过track函数将依赖存储到对应的全局仓库中
track(target, TrackOpTypes.GET, key)
}
// 浅转换至将target的第一层值转化为响应式,不做递归转化
if (shallow) {
return res
}
// 访问属性已经是ref对象,保证访问ref属性时得到的是ref对象的value属性值,数组除外
if (isRef(res)) {
// ref unwrapping, only for Objects, not for Arrays.
return targetIsArray ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
// key对应的值如果是对象,需要递归的进行响应式转化,readonly和reactive函数最终
// 调用的都是createReactiveObject函数,只是触发的响应式转化模式不同
// ⚠️此处有一点需要注意,3.0版本对响应式做出了优化。在2.0版本中,响应式转化
// 是在初始化阶段一次性递归转化完成,3.0的处理方式则更加优雅,只有getter拦截到
// 对象时,才会继续向对象的下游做响应式转化,这样的好处就是:只有真正访问到的数据
// 才会做转化,如果我们定义了某个数据,但是实际上并没有使用到它,那么我们不会触发
// getter,自然也就不会继续做响应式转化,这样就做到了“按需转化”
return isReadonly ? readonly(res) /* 只读响应式转化 */ : reactive(res) /* 非只读响应式转化 */
}
return res
}
}
track:
track方法是触发getter过程中的做依赖收集的函数,会将收集到的依赖添加到依赖仓库,结构为target -> key -> dep
export function track(target: object, type: TrackOpTypes, key: unknown) {
// activeEffect是当前处于激活状态的effect,也就是触发getter时当前访问的effect,
// 如果没有对应的激活态effect,也就没必要做依赖收集了
if (!shouldTrack || activeEffect === undefined) {
return
}
// targetMap为全局数据响应关系仓库,为Map类型,key为组件数据源,每个数据源对应Map中的
// 一个key,value是一个Set,Set中存放的是数据源中每个key对应的deps,即收集的依赖
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 数据源中每个key对应的依赖仓库
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 添加激活订阅者,deps里存储的effects是收集的订阅者们,相当于vue2.0中的watcher
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
createSetter:
响应式对象proxy对应的setter handler,用来做更新派发
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) {
// 通过setter入参的新值获取它的原始值,新传入值可能是响应式数据,如果直接和
// target上的原始值比较是没有任何实际意义的
value = toRaw(value)
// target不是数组,且旧值为ref,新值非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
// 这里其实就是为了判断receiver是proxy实例还是原型链上有proxy的对象,只有前者会触发
// 更新派发,防止通过原型链触发拦截器触发更新。
// 这里处理的非常巧妙,看下createGetter代码,在getter handler中做了一层拦截,当访问
// __v_raw属性时,只有receiver本身时proxy实例时才会返回target,即原始目标对象。再看
// 下面的createSetter代码,toRaw会访问receiver的__v_raw属性,从而触发getter
// handler,由于我们已经做了原型链访问拦截,所以在setter里如果receiver位于原型链上,
// 那么是访问不到__v_raw属性的,因此确保了只有receiver本身是proxy实例才触发更新派发
if (target === toRaw(receiver)) {
// 区分是新增属性和固有属性
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
trigger:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>() // 普通effect集合
const computedRunners = new Set<ReactiveEffect>() // 计算effect集合
// add方法用来将依赖Map中的对应dep Set添加到局部集合中,以便后续触发(run)
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
// effect和当前激活的effect引用相同,且shouldTrack = true时不会将
// 遍历的effect推入局部Set,shouldTrack为true表示开启依赖收集模式,
// 该模式下,activeEffect是当前正在收集中的依赖,依赖收集未完成不能
// 派发该依赖的更新
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 清空依赖时,将target对应的依赖map中的依赖全部添加到局部Set,准备trigger
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
// 数组的处理要特殊一些,比如push等方法的拦截会触发数组的length属性,
// 具体可以看下文末的例子e.g.,会详细讲解为什么拦截数组的length属性
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// key不是undefined就会触发依赖局部添加,只不过如果是新增属性对应的dep为空
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
// 对于新增属性或者删除属性的处理
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
// 新增或删除属性,如果是数组,会触发length对应的依赖,如果书普通对象,会触发
// ITERATE_KEY对应的依赖;如果是Map设置属性,会触发ITERATE_KEY
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
// Object新增属性不会触发getter,因此无依赖收集,需要触发ITERATE_KEY收集的依赖
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
// 对于Map新增或删除属性,统一触发MAP_KEY_ITERATE_KEY所对应的依赖
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
// run函数用来批量执行add方法添加过的effect
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
// 非立即执行的effect,即需要使用schedular调度器的effect,比如计算effect
effect.options.scheduler(effect)
} else {
// 立即执行的effect,无需特殊调度处理
effect()
}
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
createInstrumentationGetter(ES6集合、映射):
vue3.0对ES6新增数据结构单独做了一套拦截处理机制,因为直接用普通对象的Proxy拦截机制无法对这些新的数据结构做统一拦截处理。vue3.0的做法简单直接,直接对这些新增class的方法进行了重写,然后在proxy的getter handler做拦截,这个做法是不是似曾相识,在2.0时代使用defineProperty时,由于对数组比较无奈,也是对数组的方法进行了重写以便拦截。
方法的重写比较简单,源码就不放了,有兴趣的可自行去阅读。重写涉及到的方法有has、delete、add、clear、get、set、forEach,属性有size,规则类似于普通对象和数组:
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
// 指定对应版本的重写方法集
const instrumentations = shallow
? shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
// ... 省略无关代码
// 核心处理逻辑,如果访问的是重写的方法,直接食用重写方法,否则正常访问值即可
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
PS:
vue3.0除了对getter、setter进行了拦截,同时也对deleteProperty和has进行了拦截,但是相对比较简单,deleteProperty是在删除非原型属性时触发trigger,has则是触发track(和vue对数组重写的indexOf等方法类似,检索元素会收集依赖)。本文不展开介绍,有兴趣的可以自行去读源码~
effect:
effect相当于vue2.0中的watcher,灵感来源于react的hooks,但vue3.0整体的设计风格是函数式的,融入了composition API将各个功能模块做的更加通用化,拥有很高的灵活性,使用时的约束要少得多,vue2.0主要是面向对象式的写法,很多功能模块都是封装在class中,推崇的主要是option API,虽然这样的好处是看似很多工作都在框架内部完成了,但是灵活性要低得多,而且做大的弊病是代码复用率很低,比如我想在多处复用一份响应式数据源,正常来说就实现起来不太容易。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// fn本身就是effect函数
if (isEffect(fn)) {
fn = fn.raw // effect.raw是effect的执行函数
}
// 创建一个新的effect函数
const effect = createReactiveEffect(fn, options)
// 执行effect函数
if (!options.lazy) {
// lazy在computedEffect会用到,懒更新,不执行effect,而是暴露给外部,由computed
// 控制流程来控制effect的执行时机。其他情况立即执行effect函数即可
effect()
}
return effect
}
options参数为effect的配置项,结构如下:
export interface ReactiveEffectOptions {
lazy?: boolean // 计算属性懒更新
computed?: boolean // 计算属性
scheduler?: (job: ReactiveEffect) => void // 调度处理
onTrack?: (event: DebuggerEvent) => void // 依赖收集时的钩子
onTrigger?: (event: DebuggerEvent) => void // 更新派发时的钩子
onStop?: () => void // effect被stop时的钩子
}
createReactiveEffect:
创建effect函数,effect函数中的fn是执行函数,和vue2.0 watcher.run()类似,fn函数执行会触发对应target[key]的getter,完成依赖收集,在依赖收集前后采用栈数据结构(effectStack)来做effect的执行调度,保证当前effect的优先级最高,并及时清除已收集依赖的内存。
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
// effectStack是一个全局effect栈,用于effect调度。
if (!effectStack.includes(effect)) { // 防止effect重复添加
// effect不在effect栈中,即首次做当前effect的收集工作,
// 将依赖仓库中对应的dep清除掉当前effect,保证effect栈中
// 存储的effect都是已经被依赖仓库收集的effect,同时保证
// 依赖仓库中不包含未被收集的依赖
cleanup(effect)
try {
enableTracking()
// effect压栈
effectStack.push(effect)
// 将当前effect设置为全局激活effect,在getter中会收集activeEffect持有的effect
// activeEffect表示当前依赖收集系统正在处理的effect
activeEffect = effect
// 执行effect的执行函数,比如访问target[key],会触发getter,在getter中
// 将activeEffect收集到依赖仓库。
// fn在render时就是render逻辑,即生成vnode并patch到真实dom那一套流程
return fn(...args)
} finally {
// 此时fn执行完毕,依赖已收集到依赖仓库,因此将当前effect出栈,及时释放内存
effectStack.pop()
resetTracking()
// 将activeEffect恢复到effect栈中最后一个effect,即上次的activeEffect,继续
// 做该effect的收集工作
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++ // effect的uid
effect._isEffect = true // 标记是否是effect
effect.active = true // effect睡否处于激活状态
effect.raw = fn // effect的执行函数
effect.deps = [] // 包含当前effect的dep数组
effect.options = options // effect的配置项,包含配置项及hooks
return effect
}
createRef:
用于创建ref对象,用于将某个值转化为响应式对象,不同于reactive方法的是,reactive只能接受object类型的入参,ref可以接受基本类型和引用类型。比如,我们可以这样创建一个响应式对象:
const state = ref('ref test');
这样生成的state会挂载getter和setter对value属性进行代理拦截,访问value时会通过track函数进行依赖收集,设置value同样会通过track派发更新。
通常在业务中使用时更推荐直接使用ref方法来生成响应式对象,原因之一如下:
ref可接受多类型
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
function createRef(rawValue: unknown, shallow = false) {
// 已经是ref对象直接返回
if (isRef(rawValue)) {
return rawValue
}
// shallow开启时,代表是浅转化模式,只对value进行拦截代理,不对value进行递归响应式转化
let value = shallow ? rawValue : convert(rawValue)
const r = {
__v_isRef: true, // 标示是否为ref对象
get value() {
// 依赖收集
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
// 当值发生变化时派发更新,需要比较原始值(raw),因为newVal有可能是proxy响应式
// 对象,比较proxy的原始值具有更强的准确性
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
// 将新设置的值进行对应的响应式转化
value = shallow ? newVal : convert(newVal)
trigger(
r,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
}
return r
}
计算属性是vue对发布订阅设计模式和响应式系统的一个很妙的实践,源码实现非常简洁,缺实现了很实用的使用效果,在实际开发中一个computed真的能帮你节省很多不必要的代码。
我们先抛开vue源码,如果让我们自己设计一个computed,作为业务使用方,它的伪代码应该是类似这样的:
targetVal = computed(() => {
return val1 + val2;
})
想要达到的效果:***由响应式数据自动计算出的一个target值供业务使用,当计算依赖的响应式数据发生改变时,动态的计算出最新的target。***并且,期待computed函数包装后的同样返回一个响应式数据。
既然是数据发生变化就做某件事,那么大方向上自然而然的就会想到用发布订阅模式来处理这类场景,只是细节如何处理的问题。
知道了想要什么,那我们再看源码就会清晰很多了。
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// computed函数可接受两种类型的入参,1⃣️ComputedGetter,也就是我们传入的
// () => {return a + b}这种回调,这类computed是只读的,如果我们为生成的
// computedRef赋值,会warning;2⃣️包含ComputedGetter和ComputedSetter
// 的options,该模式下允许使用者为computedRef.value赋值,此时的computedRef
// 完全就是一个响应式对象,因此需要在getter中收集依赖
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true
let value: T
let computed: ComputedRef<T>
// 当触发getter时,getter中的响应式数据会触发数据对应的getter拦截器,因此下面的
// runner会被当作依赖收集到各数据对应的dep中
const runner = effect(getter, {
// lazy用于标示effect是懒更新,即effect包装时不立即执行,而是
// 返回外部由外部决定执行时机
lazy: true,
// mark effect as computed so that it gets priority during trigger
// 标示是计算属性,在trigger时保证优先执行
computed: true,
// 调度,trigger中检查effect.options中到有scheduler时,会优先执行scheduler回调
// 通常非立即执行effect可借助scheduler做一些中间态的过度行为,配合effect外部执行
// effect本身的时机,形成一条时序可控的操作流程,比如计算属性的时序如下:
// 1⃣️getter中的响应数据更新,触发trigger派发更新,trigger中会触发effect的
// scheduler回调,将dirty置为true,等待触发computedRef;2⃣️getter中的响应数据更新
// 同时会触发渲染effect的执行,渲染effect立即执行,其getter会触发render函数,重新
// 生成vnode -> patch,期间会访问计算属性,触发computedRef的getter,此时由于
// 已经在scheduler中将dirty置为true,因此computedEffect的getter会重新计算最新值。
scheduler: () => {
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})
computed = {
__v_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
// dirty控制get新的计算值的时机,只有dirty为true时才会触发新的计算
if (dirty) {
// 执行runner时会触发effect的getter,也就是我们传入的计算函数,
// 计算函数会触发对应响应式数据的getter -> track,执行依赖收集,因此计算依赖
// 的值会将computed对应的effect存入想对象的dep,当依赖的响应数据
// 变化时,触发对应的setter -> trigger,trigger中对computedEffect有
// 相应的处理逻辑,可移步trigger查看~
value = runner()
dirty = false
}
// 如果computed非只读,就需要收集依赖,用于值改变时的trigger
track(computed, TrackOpTypes.GET, 'value')
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any
return computed
}
数组方法的触发
const arr = [2, 3, 6, 7, 8]
const proxy = new Proxy(arr, {
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);
}
})
proxy.push(2)
打印结果如下:
get push
get length
set 3 2
set length 4
说明调用push方法,会访问数组的push和length属性,然后会设置key为3的值等于2,并修改数组长度,因此setter会拦截3和length。pop、shift、unshift也是类似。
proxy.splice(1, 1)
再看下splice的触发结果:
get splice
get length
get constructor
get 1
get 2
set 1 6
get 3
set 2 7
get 4
set 3 9
set length 4
因为涉及到遍历访问和移位赋值操作,因此触发的log较多。
总结:
push这类增加元素的方法,由于是新增元素,因此需要关注getter和setter中均触发过的key,如果某个key只是setter中触发了,但是getter并未触发因此该key就不会收集依赖,因此在trigger时无法派发有效的更新。
仔细观察会发现push的过程中getter和setter同时触发了length属性,因此getter时length属性做了对应的依赖收集工作,那么我们在setter时只需要拦截数组的length属性,对length属性的dep进行更新派发就可以了。