理解 Proxy 和 Reflect
未使用Reflect.get:
当原始数据的bar使用了this.foo,再在watch监听obj.foo,最后修改obj.foo
js
const data = {
foo: 1,
get bar() {
return this.foo
}
};
// 对原始的数据进行代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
track(target, key);
return target[key];
},
// 拦截写入操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
trigger(target, key);
}
})
watch(() => console.log(obj.bar))
obj.foo++
结果:
只执行了watch的effect,是因为在obj的getter函数中的this是指向data,所以访问obj.bar跟访问data.bar是一样的,data并不是proxy数据所以不会跟effect建立联系,就需要通过Reflect.get来修正this
使用Reflect.get:
diff
const data = {
foo: 1,
get bar() {
return this.foo
}
};
// 对原始的数据进行代理
const obj = new Proxy(data, {
// 拦截读取操作
- get(target, key) {
+ get(target, key, receiver) {
track(target, key);
- return target[key];
+ return Reflect.get(target, key, receiver)
},
// 拦截写入操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
trigger(target, key);
}
})
watch(() => console.log(obj.bar))
obj.foo++
结果:
watch先执行一次effect会触发obj.bar的getter通过Reflect.get修改this指向obj就触发了obj.foo,所以obj.foo也跟watch的effect建立了联系,修改obj.foo就会触发effect
代理 Object
一个普通对象的所有可能的读取操作:
- 访问属性:obj.foo
- 判断对象或原型上是否存在给定的 key:key in obj
- 使用 for...in 循环遍历对象:for (const key in obj) {}
get
js
const data = { foo: 1 }
// 对原始的数据进行代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver)
},
})
in 操作符
js
const data = { foo: 1 }
// 对原始的数据进行代理
const obj = new Proxy(data, {
// 拦截读取操作
has(target, key) {
track(target, key);
return Reflect.has(target, key)
},
})
watch(() => "foo" in obj)
结果:
for...in
例子:
由于ownKeys只有target并没有key,所以需要定义一个key
js
const data = { foo: 1 }
const ITERATE_KEY = Symbol()
// 对原始的数据进行代理
const obj = new Proxy(data, {
// 拦截读取操作
ownKeys(target) {
track(target, ITERATE_KEY);
return Reflect.ownKeys(target)
},
})
watch(() => {
for (const key in obj) {
console.log(key)
}
})
结果:
添加新属性:
向obj添加a属性并未重新遍历,这是因为向obj添加a属性会触发set,set向depsMap根据key也就是a拿到对应的effect,但是一开始obj并没有a属性所以depsMap并没有a
diff
const data = { foo: 1 }
const ITERATE_KEY = Symbol()
// 对原始的数据进行代理
const obj = new Proxy(data, {
// 拦截读取操作
ownKeys(target) {
track(target, ITERATE_KEY);
return Reflect.ownKeys(target)
},
})
watch(() => {
for (const key in obj) {
console.log(key)
}
})
+ obj.a = 1
解决思路:
当添加属性时,将那些与 ITERATE_KEY 相关联的副作用函数也取出来执行,这样添加新属性时就会触发ITERATE_KEY 相关联的副作用函数
diff
function trigger(target, key) {
// 获得对应key的effect set
const depsMap = bucket.get(target);
+ // 取得与 ITERATE_KEY 相关联的副作用函数
+ const iterateEffects = depsMap.get(ITERATE_KEY)
if (!depsMap) return;
console.log("depsMap", depsMap)
const effects = depsMap.get(key);
// 临时的 set
const effectsToRun = new Set();
// 将副作用函数 effect 取出并执行
effects && effects.forEach(effect => {
// 如果trigger触发的副作用函数和当前正在执行的函数相同,则跳过
if (effect !== activeEffect) {
effectsToRun.add(effect);
}
});
+ // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
+ iterateEffects && iterateEffects.forEach(effectFn => {
+ if (effectFn !== activeEffect) {
+ effectsToRun.add(effectFn)
+ }
+ })
effectsToRun.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
修改已有属性问题:
新加属性或者修改现有属性上面的代码都会执行,对于forin来说修改现有数据只需要遍历一次,增加新属性才需要重新遍历
在修改属性时在setter判断是新增属性还是修改现有属性,把type传入trigger再判断如果是修改现有属性就不执行与 ITERATE_KEY 相关联的副作用函数,这样forin只有新增属性时才执行
diff
const obj = new Proxy(data, {
// 拦截写入操作
set(target, key, newVal, receiver) {
+ // 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
+ const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// 把副作用函数从桶里取出并执行
+ trigger(target, key, type);
return res
},
})
- function trigger(target, key) {
+ function trigger(target, key, type) {
// 获得对应key的effect set
const depsMap = bucket.get(target);
if (!depsMap) return;
// 取得与 ITERATE_KEY 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effects = depsMap.get(key);
// 临时的 set
const effectsToRun = new Set();
// 将副作用函数 effect 取出并执行
effects && effects.forEach(effect => {
// 如果trigger触发的副作用函数和当前正在执行的函数相同,则跳过
if (effect !== activeEffect) {
effectsToRun.add(effect);
}
});
+ if (type === "ADD") {
+ // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
+ iterateEffects && iterateEffects.forEach(effectFn => {
+ if (effectFn !== activeEffect) {
+ effectsToRun.add(effectFn)
+ }
+ })
+ }
effectsToRun.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
删除属性
首先检查被删除的属性是否属于对象自身,然 后调用 Reflect.deleteProperty 函数完成属性的删除工作,只有 当这两步的结果都满足条件时,才调用 trigger 函数触发副作用函数 重新执行
diff
const obj = new Proxy(data, {
+ deleteProperty(target, key) {
+ // 检查被操作的属性是否是对象自己的属性
+ const hasKey = Object.prototype.hasOwnProperty.call(target, key)
+ // 使用 Reflect.deleteProperty 完成属性的删除
+ const res = Reflect.deleteProperty(target, key)
+ // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
+ if(hasKey && red) {
+ trigger(target, get, "DELETE")
+ }
+ return res
+ }
})
由于删除操作会使得对象的键变少,它会影响 for...in 循环的次数,因此当操作类型为 'DELETE' 时,也应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行
diff
function trigger(target, key, type) {
+ if (type === "ADD" || type === "DELETE") {
+ // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
+ iterateEffects && iterateEffects.forEach(effectFn => {
+ if (effectFn !== activeEffect) {
+ effectsToRun.add(effectFn)
+ }
+ })
}
合理地触发响应
当值没有发生变化时, 应该不需要触发响应
js
const obj = { foo: 1 }
effect(() => {
console.log(obj.foo)
})
obj.foo = 1
effect先自执行一次打印1,再obj.foo再修改一次打印1
解决思路:
- 修改属性会先触发setter所以在setter判断
- 用
target[key]
拿到之前的旧值,再跟newVal判断是否真正修改了,如果修改了才执行trigger
diff
const obj = new Proxy(data, {
// 拦截写入操作
set(target, key, newVal, receiver) {
+ // 拿到旧值
+ const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
+ // 比较新值与旧值,只要当不全等的时候才触发响应
+ if (oldVal !== newVal) trigger(target, key, type);
return res
},
})
NaN问题
NaN 与 NaN 进行不全等比较总会得到true,这仍然会触发响应,并导致不必要的更新
js
NaN === NaN // false
NaN !== NaN // true
const obj = { foo: NaN }
effect(() => {
console.log(obj.foo)
})
obj.NaN = 1
解决思路:
因为NaN跟NaN进行全等时为false,当oldVal === oldVal || newVal === newVal
为true就能排除NaN
diff
const obj = new Proxy(data, {
// 拦截写入操作
set(target, key, newVal, receiver) {
// 拿到旧值
const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
+ // 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
+ if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
+ trigger(target, key, type);
+ }
return res
},
})
原型上继承属性问题
obj上没有bar属性,parent设置为child的原型,在effect函数中访问child.bar
会创建child的bar的depsMap,由于child上没有bar所以会向原型上找proto.bar会创建proto的bar的depsMap,所以child.bar = 2
会触发child的bar的depsMap、proto的bar的depsMap打印两次2
js
function reactive(data) {
return new Proxy(data, {
// 省略前文讲解的拦截函数
}
const obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
// 使用 parent 作为 child 的原型
Object.setPrototypeOf(child, parent)
effect(() => {
console.log(child.bar)
})
child.bar = 2
解决思路:
- child、parent的setter中的target都是对应原始对象,但是receiver都指向child
- 在getter中添加可以返回target的row属性,在setter访问row会触发getter拿到target再跟setter中的target比较,如果相同说明receiver就是 target 的代理对象
diff
function reactive(data) {
return new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
+ if (key === "row") return target
track(target, key);
return Reflect.get(target, key, receiver)
},
// 拦截写入操作
set(target, key, newVal, receiver) {
console.log(target, receiver.row, target === receiver.row)
// 拿到旧值
const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
+ if (target === receiver.row) {
// 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
}
return res
},
})
}
浅响应与深响应
js
function reactive(obj) {
return createReactive(obj)
}
function shallowReactive(obj) {
return createReactive(obj, true)
}
深响应
读取obj.foo.bar时,首先要读取 obj.foo 的值,但得到是普通的对象非响应对象,所以在副作用函数中访问 obj.foo.bar时,是不能建立响应联系的
解决思路:
在getter得到的是原始数据对象就递归传入reactive转成响应数据
diff
function reactive(data) {
return new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
if (key === "row") return target
track(target, key);
// 得到原始值结果
+ const res = Reflect.get(target, key, receiver)
+ // 调用 reactive 将结果包装成响应式数据并返回
+ if (typeof res === "object" && res !== null) return reactive(res)
+ return res
}
})
}
结果:
浅响应
只有对象的第一层属性是响应的,第二层及更深层次的属性则不是响应的
解决思路:
- 单独封装个createReactive函数,并支持传入isShallow来控制是否是浅响应
- 如果是浅响应就直接返回原始值,否则就递归转换成深响应
js
function createReactive(obj, isShallow = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
if (key === "row") return target
track(target, key);
// 得到原始值结果
const res = Reflect.get(target, key, receiver)
// 如果是浅响应,则直接返回原始值
if (isShallow) return res
// 调用 reactive 将结果包装成响应式数据并返回
if (typeof res === "object" && res !== null) return reactive(res)
return res
}
})
}
结果:
深只读和浅只读
js
function readonly(obj) {
return createReactive(obj, false, true)
}
function shallowReadonly(obj) {
return createReactive(obj, true /* shallow */, true)
}
深只读
解决思路:
- 只读是属性改动时触发的,涉及到属性改动操作有set、deleteProperty
- createReactive添加isReadonly参数默认值为false,在set、deleteProperty判断isReadonly,如果为true就直接报错
diff
-function createReactive(obj, isShallow = false) {
+function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截写入操作
set(target, key, newVal, receiver) {
+ if (isReadonly) {
+ console.warn(`属性 ${key} 是只读的`)
+ return true
+ }
// 拿到旧值
const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
if (target === receiver.row) {
// 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
}
return res
},
deleteProperty(target, key) {
+ if (isReadonly) {
+ console.warn(`属性 ${key} 是只读的`)
+ return true
+ }
// 检查被操作的属性是否是对象自己的属性
const hasKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用 Reflect.deleteProperty 完成属性的删除
const res = Reflect.deleteProperty(target, key)
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
if (hasKey && red) {
trigger(target, get, "DELETE")
}
return res
}
})
}
结果:
只读时不绑定effect
当一个数据为只读时,不应该跟effect关联起来
js
const obj = readonly({ foo: 1 })
effect(() => {
console.log(obj.foo)
})
obj.foo = 2
解决思路:
- 因为在effect读取值会触发getter,所以在getter判断是否只读
- 只有非只读才能执行track建立响应联系
- 如果原始值是对象并且isReadonly为true,继续递归readonly让更深层次属性转换成只读
diff
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
if (key === "row") return target
// 非只读的时候才需要建立响应联系
+ if (!isReadonly) track(target, key);
// 得到原始值结果
const res = Reflect.get(target, key, receiver)
// 如果是浅响应,则直接返回原始值
if (isShallow) return res
// 调用 reactive 将结果包装成响应式数据并返回
if (typeof res === "object" && res !== null) {
+ // 如果数据为只读,则调用 readonly 对值进行包装
+ return isReadonly ? readonly(res) : reactive(res)
}
return res
}
})
}
浅只读
只需要修改 createReactive 的第二个参数,因为isShallow为true就直接返回原始值
js
function shallowReadonly(obj) {
return createReactive(obj, true, true)
}
代理数组
所有对数组元素或属性的"读取"操作。
- 通过索引访问数组元素值:arr[0]。
- 访问数组的长度:arr.length。
- 把数组作为对象,使用 for...in 循环遍历。
- 使用 for...of 迭代遍历数组。
- 数组的原型方法,如 concat/join/every/some/find/findIndex/includes 等,以及其他所有不改变原数组的原型方法。
数组的索引与 length
通过索引设置元素值时,可能会隐式地修改length
数组的原长度为1,并且在副作用函数中访问了length属性。然后设置数组索引为1的元素值,这会导致数组的长度变为2,应该触发副作用函数重新执行。但目前的实现还做不到这一点
js
const arr = reactive(["foo"])
effect(() => {
console.log(arr.length)
})
arr[1] = "bar"
解决思路:
- 在track中length已经跟effect建立关联,但在setter中打印key能访问到
arr[1]
的key,但是trigger并没有根据length拿到对应的effect - 判读target是数组再判断key跟target的length大小,key小于length说话就是修改数据,否则就是新增
diff
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截写入操作
set(target, key, newVal, receiver) {
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 拿到旧值
const oldVal = target[key]
+ // 如果代理目标是数组,则检测被设置的索引值是否小于数组长度,
+ // 如果是,则视作 SET 操作,否则是 ADD 操作
+ const type = Array.isArray(target) ? Number(key) < target.length ? "SET" : "ADD" : Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
if (target === receiver.row) {
// 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
}
return res
},
})
}
- 只有在是新增并且target是数组时,才把length关联的effect拿出添加到effectsToRun,修改是不会改变length所以不需要判断
diff
function trigger(target, key, type) {
+ if (type === "ADD" && Array.isArray(target)) {
+ // 取出与 length 相关联的副作用函数
+ const lengthEffects = depsMap.get("length")
+ lengthEffects && lengthEffects.forEach(effectFn => {
+ if (effectFn !== activeEffect) {
+ effectsToRun.add(effectFn)
+ }
+ })
+ }
}
结果:
修改数组的length属性
当修改 length 属性值时,只有那些索引值大于或等于新的 length 属性值的元素才需要触发响应
js
const arr = reactive(["foo"])
effect(() => {
console.log(arr[0])
})
arr.length = 0
解决思路:
- 设置length一般都是设置数字代表是数组的长度,意味着赋值给length的值是最大的key,只有target的索引值比length的值大才需要触发effect,newVal就是length的值需要传入trigger进行比较
diff
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截写入操作
set(target, key, newVal, receiver) {
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 拿到旧值
const oldVal = target[key]
// 如果代理目标是数组,则检测被设置的索引值是否小于数组长度,
// 如果是,则视作 SET 操作,否则是 ADD 操作
const type = Array.isArray(target) ? Number(key) < target.length ? "SET" : "ADD" : Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
if (target === receiver.row) {
// 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
- trigger(target, key, type);
+ trigger(target, key, type, newVal);
}
}
return res
},
})
}
- 只有在target是数组并且修改length时触发,因为
arr[0]
通过索引值来访问,所以depsMap存放在关于索引值对应的effect,遍历depsMap跟newVal也就是length的值比较,如果索引值比length大就添加到effectsToRun
diff
function trigger(target, key, type, newVal) {
// 获得对应key的effect set
const depsMap = bucket.get(target);
if (!depsMap) return;
+ if (Array.isArray(target) && key === "length") {
+ // 对于索引大于或等于新的 length 值的元素,
+ // 需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行
+ depsMap.forEach((effects, key) => {
+ if (key >= newVal) {
+ effects.forEach(effectFn => {
+ if (effectFn !== activeEffect) {
+ effectsToRun.add(effectFn)
+ }
+ })
+ }
+ })
+ }
effectsToRun.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
结果:
遍历数组
for...in
哪些操作会影响 for...in 循环对数组的遍历
- 添加新元素:arr[100] = 'bar'
- 修改数组长度:arr.length = 0
js
effect(() => {
for (const key in arr) {
console.log(key)
}
})
arr[1] = 'bar'
arr.length = 0
目前并不会触发
解决思路:
- for...in会触发ownKeys,之前object是添加了ITERATE_KEY的key,但是无论是为数组添加新元素,还是直接修改数组的长度,本质上都是因为修改了数组的 length 属性,所以只需要把length跟target关联起来
diff
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
ownKeys(target) {
- track(target, ITERATE_KEY)
+ track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
},
})
}
结果:
for...of
无论是使用 for...of 循环,还是调用 values 等方法,它们都会读取数组的 Symbol.iterator 属性。该属性是一个 symbol 值,为了避免发生意外的错误,以及性能上的考虑,我们不应该在副作用函数与 Symbol.iterator 这类 symbol 值之间建立响应联系
js
const arr = reactive(["foo"])
effect(() => {
for (const key of arr) {
key
}
})
arr[1] = 'bar'
arr.length = 0
解决思路:
在getter判断key不是symbol才执行track
数组的查找方法
根据索引查找数组中的对象
obj是个对象又作为arr数组的第一位,arr[0]
会触发getter得到是一个对象,对象又会递归reactive函数产生新的代理对象,includes方法内部也会通过 arr 访问数组元素又会触发getter又递归reactive函数产生新的代理对象,导致两个代理对象不一样为false
js
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0]))
解决思路:
- 之前得知会重复调用reactive函数并传入obj,但obj没有变化所以需要定义map来存储原始对象跟代理对象的映射,如果存在就直接返回,不存在就创建新的代理对象并以原始对象为key存到map中
diff
+// 定义一个 Map 实例,存储原始对象到代理对象的映射
+const reactiveMap = new Map()
function reactive(obj) {
+ // 优先通过原始对象 obj 寻找之前创建的代理对象,如果找到了,直接返回已有的代理对象
+ const existionProxy = reactiveMap.get(obj)
+ if (existionProxy) return existionProxy
+ // 否则,创建新的代理对象
+ const proxy = createReactive(obj)
+ // 存储到 Map 中,从而避免重复创建
+ reactiveMap.set(obj, proxy)
+ return proxy
}
结果:
根据原始对象查找
obj是原始数据而arr是代理对象,arr.includes会遍历arr中每个元素,得到的每个元素也是代理对象,所以用原始数据查找会得到false
js
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj))
解决思路:
- arr.includes会触发getter,在getter中判断操作的目标对象是否数组,并且 key 存在于arrayInstrumentations 上includes方法
diff
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
if (key === "raw") return target
+ // 如果操作的目标对象是数组,并且 key 存在于arrayInstrumentations 上, 那么返回定义在 arrayInstrumentations 上的值
+ if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
+ return Reflect.get(arrayInstrumentations, key, receiver)
+ }
// 非只读的时候才需要建立响应联系
if (!isReadonly && typeof key !== 'symbol') {
track(target, key)
};
// 得到原始值结果
const res = Reflect.get(target, key, receiver)
// 如果是浅响应,则直接返回原始值
if (isShallow) return res
// 调用 reactive 将结果包装成响应式数据并返回
if (typeof res === "object" && res !== null) {
// 如果数据为只读,则调用 readonly 对值进行包装
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
})
}
- 在自定义的includes函数中先调用数组原生的includes再改变this也就是arr代理对象来判断args也就是要判断的值是否在arr中,如果不在arr中再判断是否在原始对象通过this.raw来触发getter来得到target
js
const arrayInstrumentations = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function (...args) {
// this 是代理对象,先在代理对象中查找,将结果存储到 res 中
let res = originMethod.apply(this, args)
if (res === false || res === -1) {
// res 为 false 说明没找到,通过 this.raw 拿到原始数组,再去其中查找,并更新 res 值
res = originMethod.apply(this.raw, args)
}
// 返回最终结果
return res
}
})
修改数组长度方法
push既会读取数组的 length 属性值会触发getter,也会设置数组的 length属性值也会触发setter,在第一个effect中跟length关联,第二个effect调用push会触发setter的length于是把与 length 属性相关联的effect全部取出并执行,但是第二个effect还没执行完就要执行第一个才导致栈溢出
js
const arr = reactive([])
// 第一个副作用函数
effect(() => {
arr.push(1)
})
// 第二个副作用函数
effect(() => {
arr.push(1)
})
解决思路:
- 导致栈溢出的原因是两个effect相互执行,定义一个变量shouldTrack来判断是否执行完默认为true再重写push方法
js
// 一个标记变量,代表是否进行追踪。默认值为 true,即允许追踪
let shouldTrack = true
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
// 取得原始 push 方法
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function (...args) {
shouldTrack = false
// push 方法的默认行为
let res = originMethod.apply(this, args)
// 在调用原始方法之后,恢复原来的行为,即允许追踪
shouldTrack = true
return res
}
})
- 开始执行自定义push将shouldTrack设置为false,push会触发length读取触发track,在track中判断shouldTrack为false就退出不会让length跟effect建立关联,当一个push执行完再将shouldTrack重置为true让第二个push可以执行,第二个effect的push执行
type === "ADD" && Array.isArray(target)
会发现length关联的effect是为空
diff
function track(target, key) {
// 没有 activeEffect、禁止追踪时 则直接 return
+ if (!activeEffect || !shouldTrack) return;
// 根据target从桶中取得 depsMap,也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,就新建一个 Map 并和 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 根据 key 从 depsMap 中取得 deps, 也是一个 Set 类型
// 里面存储着所有与当前 key 相关联的副作用函数: effects
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
结果:
代理 Set 和 Map
size
因为代理对象上没有size属性所以报错了
js
const s = new Set([1, 2, 3])
const p1 = reactive(s)
console.log(p1.size)
解决思路:
- 使用Reflect.get把this指向原始对象
js
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
get(target,key, receiver) {
if (key === "size") {
return Reflect.get(target, key, target)
}
}
})
}
delete
因为delete是一个函数,而当访问 p.delete 时,delete 方法并没有执行,真正使其执行的语句是 p.delete(1) 这句函数调用
js
const s = new Set([1, 2, 3])
const p1 = reactive(s)
console.log(p1.delete(1))
解决思路:
把this绑定到原始对象并返回函数可以进行调用所以用bind
diff
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
get(target,key, receiver) {
if (key === "size") {
return Reflect.get(target, key, target)
}
+ // 将方法与原始数据对象 target 绑定后返回
+ return target[key].bind(target)
}
})
}
结果:
建立响应联系
add
在effect中访问了size,但是add会改变size未重新触发effect,是因为在访问size时没有让size跟effect关联
js
const p1 = reactive(new Set([1, 2, 3]))
effect(() => {
// 在副作用函数内访问 size 属性
console.log(p1.size)
})
// 添加值为 1 的元素,应该触发响应
p1.add(1)
解决思路:
- 当访问size时调用track跟ITERATE_KEY关联起来,是因为不管修改、新增都会影响size的状态
diff
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
get(target,key, receiver) {
+ if (key === 'raw') return target
if (key === "size") {
+ // 调用 track 函数建立响应联系
+ track(target, ITERATE_KEY)
return Reflect.get(target, key, target)
}
// 将方法与原始数据对象 target 绑定后返回
return mutableInstrumentations[key]
}
})
}
- 定义mutableInstrumentations对象来存放自定义实现的方法,当访问add时执行的是自定义add,通过this.raw来拿到target,再执行target的add并执行完返回得到结果,再触发trigger并传入ADD参数让执行
if (type === "ADD" || type === "DELETE")
拿到ITERATE_KEY相关联的effect
js
// 定义一个对象,将自定义的 add 方法定义到该对象下
const mutableInstrumentations = {
add(key) {
// this 仍然指向的是代理对象,通过 raw 属性获取原始数据对象
const target = this.raw
// 先判断值是否已经存在
const hadKey = target[key]
// 通过原始数据对象执行 add 方法添加具体的值,
// 注意,这里不再需要 .bind 了,因为是直接通过 target 调用并执行的
const res = target.add(key)
// 调用 trigger 函数触发响应,并指定操作类型为 ADD
if (!hadKey) {
trigger(target, key, 'ADD')
}
// 返回操作结果
return res
}
}
结果:
delete
delete方法只有在要删除的元素确实在集合中存在时,才需要触发响应
js
// 定义一个对象,将自定义的 add 方法定义到该对象下
const mutableInstrumentations = {
delete(key) {
const target = this.raw
const hadKey = target.has(key)
const res = target.delete(key)
// 当要删除的元素确实存在时,才触发响应
if (hadKey) {
trigger(target, key, 'DELETE')
}
return res
}
}
结果:
避免污染原始数据
当调用 get 方法读取数据时,需要调用 track 函数追踪依赖建立响应联系;当调用 set 方法设置数据时,需要调用 trigger 方法触发响应
get
重写get方法,让get跟effect关联起来,并让值转换成响应数据
js
const mutableInstrumentations = {
get(key) {
// 获取原始对象
const target = this.raw
// 判断读取的 key 是否存在
const had = target.has(key)
// 追踪依赖,建立响应联系
track(target, key)
// 如果存在,则返回结果。这里要注意的是,如果得到的结果 res 仍然是可代理的数据,
// 则要返回使用 reactive 包装后的响应式数据
if (had) {
const res = target.get(key)
return typeof res === 'object' ? reactive(res) : res
}
}
}
set
Map的set方法接受key跟新的value参数,判断key是否有对应的值区分新增还是修改,通过旧值跟新值比对触发对应effect
js
const mutableInstrumentations = {
set(key, value) {
const target = this.raw
const had = target.has(key)
// 获取旧值
const oldValue = target.get(key)
// 设置新值
target.set(key, value)
// 如果不存在,则说明是 ADD 类型的操作,意味着新增
if (!had) {
trigger(target, key, 'ADD')
} else if (oldValue !== value || (oldValue === oldValue &&
ue === value)) {
// 如果不存在,并且值变了,则是 SET 类型的操作,意味着修改
trigger(target, key, 'SET')
}
}
}
处理 forEach
代理对象的this上没有forEach所以会报错需要重写forEach
js
const m = reactive(new Map([
[{ key: 1 }, { value: 1 }]
]))
effect(() => {
m.forEach(function (value, key, m) {
console.log(value) // { value: 1 }
console.log(key) // { key: 1 }
})
})
m.set(0,1)
解决思路:
在mutableInstrumentations添加自定义forEach,因为forEach跟key、value的数量有关,所以forEach要跟ITERATE_KEY关联起来,只要新增删除就要重新触发forEach的effect
diff
const mutableInstrumentations = {
+ forEach(callback) {
+ // 取得原始数据对象
+ const target = this.raw
+ // 与 ITERATE_KEY 建立响应联系
+ track(target, ITERATE_KEY)
+ // 通过原始数据对象调用 forEach 方法,并把 callback 传递过去
+ target.forEach(callback)
+ }
}
结果:
forEach回调参数转换响应数据
forEach回调的value参数不是响应数据所以修改并不会触发effect
js
const key = { key: 1 }
const value = new Set([1, 2, 3])
const p1 = reactive(new Map([
[key, value]
]))
effect(() => {
p1.forEach(function (value, key) {
console.log(value.size) // 3
})
})
p1.get(key).delete(1)
解决思路:
使用target.forEach处理每个key、value,因为是map需要将key传入wrap转换成代理对象再传给callback
diff
const mutableInstrumentations = {
forEach(callback, thisArg) {
+ const wrap = (val) => typeof val === "object" ? reactive(val) : val
// 取得原始数据对象
const target = this.raw
// 与 ITERATE_KEY 建立响应联系
track(target, ITERATE_KEY)
// 通过原始数据对象调用 forEach 方法,并把 callback 传递过去
+ target.forEach((val, key) => {
+ callback.call(thisArg, wrap(val), wrap(key), this)
+ })
+ }
}
结果:
Map的set触发forEach
Map的set是修改value的,而且Map的forEach中的回调有返回value,所以在使用set需要查询触发forEach
js
const p1 = reactive(new Map([
['key', 1]
]))
effect(() => {
p1.forEach(function (value, key) {
// forEach 循环不仅关心集合的键,还关心集合的值
console.log(value) // 1
})
})
p1.set('key', 2) // 即使操作类型是 SET,也应该触发响应
解决思路:
增加判断type为set并且target是Map就触发ITERATE_KEY关联的effect
diff
function trigger(target, key, type, newVal) {
if (
type === "ADD" ||
type === "DELETE" ||
+ // 如果操作类型是 SET,并且目标对象是 Map 类型的数据,
+ // 也应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行
+ (type === "SET" && Object.prototype.toString.call(target) === '[object Map]')
) {
// 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
结果:
迭代器方法
for...of
forof一个代理对象会触发[Symbol.iterator]方法,但代理对象没有实现该方法
js
const p1 = reactive(new Map([
['key1', 'value1'],
['key2', 'value2']
]))
effect(() => {
for (const [key, value] of p1) {
console.log(key, value)
}
})
p1.set('key3', 'value3')
解决方法:
- 在mutableInstrumentations添加[Symbol.iterator]方法
- 因为forof会涉及新增删除value所以需要跟ITERATE_KEY关联
- 因为forof中拿到的value是响应数据,所以通过
itr.next()
拿到value、done,再把value传入wrap转换成代理对象
diff
const mutableInstrumentations = {
+ [Symbol.iterator]() {
+ // 获取原始数据对象 target
+ const target = this.raw
+ // 获取原始迭代器方法
+ const itr = target[Symbol.iterator]()
+ const wrap = (val) => typeof val === "object" && val !== null ? reactive(val) : val
+ // 调用 track 函数建立响应联系
+ track(target, ITERATE_KEY)
+ // 将其返回
+ return {
+ next() {
+ // 调用原始迭代器的 next 方法获取 value 和 done
+ const { value, done } = itr.next()
+ return {
+ // 如果 value 不是 undefined,则对其进行包裹,value[0]是key,value[1]是value
+ value: value ? [wrap(value[0]), wrap(value[1])] : value,
+ done
+ }
+ }
+ }
}
结果:
entries
指entries的返回值不是一个可迭代对象
js
const p1 = reactive(new Map([
['key1', 'value1'],
['key2', 'value2']
]))
effect(() => {
` for (const [key, value] of p1.entries()) {
console.log(key, value)
}
})
console.log("修改")
p1.set('key3', 'value3')
解决方法:
[Symbol.iterator]与entries相等所以抽离一个共用函数,再添加可迭代协议
diff
// 抽离为独立的函数,便于复用
function iterationMethod() {
const target = this.raw
const itr = target[Symbol.iterator]()
const wrap = (val) => typeof val === 'object' ? reactive(val) : val
track(target, ITERATE_KEY)
return {
next() {
const { value, done } = itr.next()
return {
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done
}
},
+ // 实现可迭代协议
+ [Symbol.iterator]() {
+ return this
+ }
}
}
const mutableInstrumentations = {
+ [Symbol.iterator]: iterationMethod,
+ entries: iterationMethod
}
结果:
values
代理对象上没有values
js
const p1 = reactive(new Map([
['key1', 'value1'],
['key2', 'value2']
]))
effect(() => {
for (const value of p1.values()) {
console.log(value)
}
})
console.log("修改")
p1.set('key3', 'value3')
解决思路
- 新增valuesIterationMethod,通过
target.values()
拿到原始迭代器方法 - 因为在forof中value会影响length,所以要跟ITERATE_KEY关联起来
- 把value传入wrap转换成想要对象
- 再实现可迭代协议
js
function valuesIterationMethod() {
// 获取原始数据对象 target
const target = this.raw
// 通过 target.values 获取原始迭代器方法
const itr = target.values()
const wrap = (val) => typeof val === 'object' ? reactive(val) : val
track(target, ITERATE_KEY)
// 将其返回
return {
next() {
const { value, done } = itr.next()
return {
// value 是值,而非键值对,所以只需要包裹 value 即可
value: wrap(value),
done
}
},
[Symbol.iterator]() {
return this
}
}
}
diff
const mutableInstrumentations = {
+ values: valuesIterationMethod
}
结果:
keys
代理对象上没有keys方法
js
const p1 = reactive(new Map([
['key1', 'value1'],
['key2', 'value2']
]))
effect(() => {
for (const key of p1.keys()) {
console.log(key)
}
})
console.log("修改")
p1.set('key3', 'value3')
解决思路:
- 新增keysIterationMethod,通过
target.values()
拿到原始迭代器方法 - 把value传入wrap转换成想要对象
- 再实现可迭代协议
js
const MAP_KEY_ITERATE_KEY = Symbol()
function keysIterationMethod() {
// 获取原始数据对象 target
const target = this.raw
// 获取原始迭代器方法
const itr = target.keys()
const wrap = (val) => typeof val === 'object' ? reactive(val) : val
// 调用 track 函数追踪依赖,在副作用函数与 MAP_KEY_ITERATE_KEY 之间建立响应联系
track(target, MAP_KEY_ITERATE_KEY)
// 将其返回
return {
next() {
const { value, done } = itr.next()
return {
value: wrap(value),
done
}
},
[Symbol.iterator]() {
return this
}
}
}
diff
const mutableInstrumentations = {
+ keys: keysIterationMethod
}
- 因为keys只有新增或者是删除才会影响到key的数量set不会影响,所以需要重新定义MAP_KEY_ITERATE_KEY,只有新增、删除、并且是 Map 类型的数据才触发所以需要重新定义MAP_KEY_ITERATE_KEY,set不触发只触发ITERATE_KEY
diff
function trigger(target, key, type, newVal) {
+ if (
+ // 操作类型为 ADD 或 DELETE
+ (type === 'ADD' || type === 'DELETE') &&
+ // 并且是 Map 类型的数据
+ Object.prototype.toString.call(target) === '[object Map]'
+ ) {
+ // 则取出那些与 MAP_KEY_ITERATE_KEY 相关联的副作用函数并执行
+ const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
+ iterateEffects && iterateEffects.forEach(effectFn => {
+ if (effectFn !== activeEffect) {
+ effectsToRun.add(effectFn)
+ }
+ })
+ }
}
结果:
新增、删除触发
修改不触发