要触发更新,就需要我们在这之前进行依赖收集。但是新增的属性,并没有依赖收集,如何派发更新呢?借助之前已经收集依赖的属性,通知更新。
vue2新增属性的更新
对象新增属性的更新
因为vue2中是用Object.definedProperty实现响应式原理,所有原生不支持新增属性的拦截。vue2中提供了set方法供我们对新增的属性进行监听并触发更新。
function set (target: Array<any> | Object, key: any, val: any): any {
...
defineReactive(ob.value, key, val) //内部对新属性使用Object.definedProperty重新进行监听
ob.dep.notify() //触发新增属性的目标对象的更新,来代替新增属性不能更新的缺陷
return val
}
核心思路
- 手动调用set方法替代Object.definedProperty不能对新增属性进行监听的缺陷
- 因为新增属性没有收集依赖,所以借助新增属性的对象派发更新
数组调用常用api的更新
vue2中调用push,splice等数组api会触发更新,我们需要对原生api进行拦截,添加派发更新的操作。如果我们直接去修改Array.prototype
的原型方法,会污染数组的原型,正常调用数组api会被修改。所有最好的办法是创建一个继承数组原型的新对象,在新对象上进行扩展需要触发更新的增强方法。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto) //创建一个继承数组原型的新对象
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) { //在arrayMethods对象上扩展数组同名新方法
const result = original.apply(this, args) //调用数组原生api
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify() //触发数组收集的依赖的更新
return result
})
})
然而这就结束了吗?我们怎么在调用数组的方法时映射到arrayMethods
对象上呢?
所以我们需要在访问一个数组的时候对它进行拦截,代理到arrayMethods
对象上。
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if ('__proto__' in {}) {
protoAugment(value, arrayMethods) // 将数组的原型指向arrayMethods
} else {
copyAugment(value, arrayMethods, arrayKeys) // 对数组中扩展arrayMethods中的方法
}
this.observeArray(value)
} else {
this.walk(value)
}
}
...
}
核心思路
- 对原数组api进行拦截,生成一个新方法(调用原方法,派发更新)扩展到从数组原型继承的新对象
- 访问数组的时候代理到新创建的对象上
vue3新增属性的更新
因为vue3中响应式api用的是proxy,proxy代理的是一个对象,天生支持对对象新增属性的监听。但是和vue2同样的问题,新增的属性并没有做依赖收集啊,怎么触发更新呢?
新增属性的更新
先看一个例子
const obj = {a:1}
const proxyObj = reactive(obj)
effect(()=>{
console.log("... update", JSON.stringify(proxyObj));
})
proxyObj.b = 2
这种情况会触发更新么,会。JSON.stringify会对对象每个属性就行遍历,所以新增属性后遍历前后的值不相同,vue3中在遍历的时候用了一个特殊的标识进行依赖收集,然后在新增属性后触发那个特殊标识的收集的依赖的更新。
new Proxy(target,{
get,
set,
deleteProperty,
has,
ownKeys: function ownKeys(target: object): (string | number | symbol)[] {
track(target, "iterate", isArray(target) ? 'length' : Symbol("iterate")) //遍历数组用length作为特殊标识收集依赖,对象用Symbol("iterate")
return Reflect.ownKeys(target)
}
})
遍历数组和对象分别用不同的标识收集依赖,在新增属性的时候派发更新。
new Proxy(target,{
get,
set:function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
...
//判断是否是已有属性(数组则判断索引是否小于数组length,对象则通过hasOwnProperty判断是否对象中属性)
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: 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) {
trigger(target, "add", key, value) // 新增属性的更新
} else if (hasChanged(value, oldValue)) {
trigger(target, "set", key, value, oldValue)
}
}
return result
},
deleteProperty,
has,
ownKeys
})
再看下trigger函数,怎么处理新增属性的更新
function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// targetMap是个weakMap,存储target对应一个存储所有属性的map映射,map中是每个属性对象effect的set集合的映射 targetMap<target,map> map<prop,set> set<effeft>
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
//临时待执行的effect的set集合
const effects = new Set<ReactiveEffect>()
//把effetct添加到effects的集合中
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
//如果target是数组且key修改的是length的长度,例:数组[1,2,3] 修改 length=1则数组变为 [1],所以数组length之后的访问索引都变为undefined需要重新触发更新
if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case "add": //新增属性
if (!isArray(target)) {
add(depsMap.get(Symbol("itrator"))) //对象添加遍历时Symbol("itrator")标识收集的effect
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length')) //数组则添加遍历时length标识收集的effect
}
break
case "delete":
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case "set":
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
//执行每个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.options.scheduler(effect)
} else {
effect()
}
}
//遍历effects集合
effects.forEach(run)
}
核心思路
- 在遍历数组或对象的时候用一个特殊的标识作为key进行依赖收集
- 新增属性的时候触发这个特殊标识收集的依赖
调用数组api的更新
先看下proxy中对数组方法的拦截
new Proxy(target,{
get: function get(target: Target, key: string | symbol, receiver: object) {
...
const targetIsArray = isArray(target)
//如果是数组且key是arrayInstrumentations中属性则代理调用arrayInstrumentations中方法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
...
},
set,
deleteProperty,
has,
ownKeys
})
'includes', 'indexOf', 'lastIndexOf'方法的拦截。这些方法是遍历整个数组去查找某个元素,所以数组中任意一项值修改了的话,都有可能影响执行的结果,所以需要对每一项进行依赖收集,在修改的时候进行更新
(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
const arr = toRaw(this)
//对数组中每项进行依赖收集
for (let i = 0, l = this.length; i < l; i++) {
track(arr, "get", i + '')
}
// 执行原生方法
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
'push', 'pop', 'shift', 'unshift', 'splice'因为执行这些方法的时候,会对一些其他属性的访问,例如:length等。所以在执行这些方法的时候需要暂停依赖收集,执行完过后恢复收集。
(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
pauseTracking() //暂停收集
const res = method.apply(this, args)
resetTracking() //恢复收集
return res
}
})
核心思路
- 拦截原生api,做一些增强的操作
- 调用'includes', 'indexOf', 'lastIndexOf'方法时对数组每一项进行依赖收集
- 调用'push', 'pop', 'shift', 'unshift', 'splice'方法时暂停依赖收集,执行完毕后恢复
总结
1.不管是vue2还是vue3在新增属性的时候都是借助之前已经收集依赖的属性的做派发更新。vue2借助的是新增属性的那个对象访问的时候收集的依赖,vue3则是proxy代理后在遍历时进入ownKeys拦截方法中定义的特殊的值进行依赖收集。
2.拦截原生数组api进行增强,具有依赖收集或派发更新等能力