第五章、非原始值的响应式方案
代理对象 Object
Vue3 还可以对 for...in 循环进行拦截,还可以对集合类型 Map Set WeakMap WeakSet 进行代理。
可以代理 apply 操作来拦截函数的调用
const fn = (str) => {
console.log(str)
}
const pFn = new Proxy(fn, {
apply(target, thisArg, argArray) {
console.log('拦截函数调用')
target.call(thisArg, ...argArray)
},
})
// 输出:
// 函数调用
// 123
pFn('123')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Reflect 对象是一个全局对象,它提供了访问一个对象的一些默认行为,它下面的方法都可以在 Proxy 下找到相同的的拦截器。
const obj = {
foo: 1,
}
// 两者等价
console.log(obj.foo)
console.log(Reflect.get(obj, 'foo'))
2
3
4
5
6
7
Reflect 下的方法可以接收第三个参数 receive,表示指向某个默认行为时,改变其中的 this 指向。
const obj = {
bar: 1,
get foo() {
return this.bar
},
}
const obj2 = {
bar: 2,
}
console.log(obj.foo)
// Reflect.get执行时this指向了obj2,所以返回 2
console.log(Reflect.get(obj, 'foo', obj2))
2
3
4
5
6
7
8
9
10
11
12
13
14
原来的 get 代理中,直接使用了target[key]
进行返回,这会出现 this 指向问题
// 原始数据
const data = {
foo: 1,
get bar() {
return this.foo
},
}
const obj = new Proxy({
// 代理操作
})
effect(() => {
console.log(obj.bar)
})
// 不会触发副作用函数
obj.foo++
// 在代理对象get操作时,使用target[key]返回结果,target指向的是原始对象,访问bar的时候访问了this.foo
// 而此时的this指向的是原始对象target,所以target[key]就是data.foo,副作用函数就变成了
// effect(() => {
// console.log(data.foo)
// })
// data.foo不是响应式属性,不会触发依赖的收集,也就不会触发副作用函数的执行
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
使用 Reflect 全局对象,来实现对各种操作的代理。
const obj = new Proxy(data, {
// 拦截读取操作接受第三个参数receiver,表示实际访问属性的是哪个对象,可以理解为当前this的指向
get(target, key, receiver) {
track(target, key)
// 使用 Reflect返回属性值,传入第三个参数receiver改变this的指向
return Reflect.get(target, key, receiver)
},
set(target, key, newVal, receiver) {
// 使用 Reflect设置属性值
const res = Reflect.set(target, key, newVal, receiver)
trigger(target, key)
// 返回设置的结果
return res
},
})
effect(() => {
console.log(obj.bar)
})
// 会触发副作用函数
obj.foo++
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
对象在执行各种操作时,本质上是执行了内部的各种方法。例如读取属性,本质是执行了对象内部的[[Get]]方法,代理对象就可以通过重写 get 方法来覆盖内部[[Get]]方法的默认行为。
所以说代理对象本质上是通过各种自定义方法,来代理对象上面各种操作实际执行的内部方法。例如删除操作,本质上是代理对象内部的 deleteProperty 方法:
const obj = new Proxy(data, {
// 自定义 deleteProperty 方法,拦截delete操作
deleteProperty(target, key) {
console.log('删除属性')
// 通过Reflect上的deleteProperty方法执行删除操作
return Reflect.deleteProperty(target, key)
},
})
// 输出 删除属性
delete obj.foo
2
3
4
5
6
7
8
9
10
11
通过 has 方法拦截 in 操作符
const obj = new Proxy(data, {
has(target, key) {
// in 操作符触发时,收集依赖
track(target, key)
return Reflect.has(target, key)
},
})
effect(() => {
// in 操作符也属于 读取 操作,需要收集依赖
'foo' in obj
console.log('执行')
})
// 副作用函数会被触发
obj.foo++
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对于 for...in 操作,其实是在内部调用了Reflect.ownKeys(obj)
来获取只属于对象自身拥有的键,所以需要对Reflect.ownKeys
操作进行拦截。
// 因为 ownKeys 拦截没有对应的key,所以定一个Symbol值来收集ownKeys触发时对应的依赖
const ITERATE_KEY = Symbol()
// 对原始数据的代理
const obj = new Proxy(data, {
ownKeys(target) {
// 使用Symbol值来收集依赖
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
})
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// 触发依赖时,将通过ITERATE_KEY收集到的依赖也取出
const iterateEffect = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
// 执行ITERATE_KEY收集到的依赖
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
effect(() => {
// 执行for...in操作,通过ownKeys拦截
for (const key in obj) {
console.log(key)
}
})
// 新增属性,会导致for...in操作需要遍历的次数改变,所以副作用函数需要重新执行
obj.bar = 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
现在即使只是修改 obj 上原有的属性,不新增属性,理论上来说 for...in 操作遍历的次数不会改变,所以不需要重新执行。但是由于设置属性和新增属性,都是通过拦截 set 操作实现的。 所以需要在 set 操作内部对当前的操作作区分。判断当前到底是新增,该是修改。
// 定义常量来表示各种操作
const TriggerType = {
SET: 'SET',
ADD: 'ADD',
}
const obj = new Proxy(data, {
set(target, key, newVal, receiver) {
// 判断当前属性是否已经存在,如果存在的话表示是修改属性,如果不存在则表示是新增属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
const res = Reflect.set(target, key, newVal, receiver)
// 触发响应时将当前的操作类型传入
trigger(target, key, type)
return res
},
})
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
// 当前的操作为新增时,执行对应的响应
if (type === TriggerType.ADD) {
const iterateEffect = depsMap.get(ITERATE_KEY)
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
删除属性时,也要做这种判断。
const obj = new Proxy(data, {
deleteProperty(target, key) {
// 判断被删除的key是否存在
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 执行删除
const res = Reflect.deleteProperty(target, key)
// key存在且删除成功时触发响应,并传入操作类型
if (res && hadKey) {
trigger(target, key, TriggerType.DELETE)
}
return
},
})
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const iterateEffect = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
// DELETE表示执行删除操作,for...in遍历的次数会变少,也要重新执行
if (type === TriggerType.ADD || type === TriggerType.DELETE) {
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
effect(() => {
for (const key in obj) {
console.log(key)
}
})
// 删除属性,for...in遍历的次数会变少,重新执行
delete obj.foo
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
当修改的属性值与之前相等时,理论上属性值没有变化,不应该触发副作用函数的执行。所以修改值时需要对新旧值做对比。
const obj = new Proxy(data, {
set(target, key, newVal, receiver) {
// 取出旧值
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
const res = Reflect.set(target, key, newVal, receiver)
// 新旧值对比,只有不全等时才触发响应,还要注意对NaN做额外判断
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
return res
},
})
effect(() => {
console.log(obj.foo)
})
// foo本来就是1,修改为1不会触发响应
obj.foo = 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
当从原型上继承属性时,会造成不必要的更新。
// 封装成函数方便调用
function reactive(data) {
return new Proxy(data, {
// 代理操作
})
}
const obj1 = { foo: 1 }
const obj2 = { bar: 2 }
const child = reactive(obj1)
const parent = reactive(obj2)
// 修改child的原型为parent
Object.setPrototypeOf(child, parent)
effect(() => {
console.log(child.bar)
})
// 修改child原型上的属性,会发现副作用函数执行了两次,输出了两个3
child.bar = 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
修改 bar 属性,实际上会去调用原型上的属性做 set 操作,所以触发了 parent 的 set 拦截。 执行 parent 的 set 操作时,传入的 target 和 receiver 代表的对象不一致,target 指向了 parent 的原始对象 obj2,但是 receiver 指向的却是 obj1 的代理对象 child。所以可以在这里对其做区分。 增加一个属性来判断当前的代理对象代理的是否是当前的原始对象。即 receiver 代理的是否是 target。
const ISSELF = Symbol()
function reactive(data) {
return new Proxy(data, {
get(target, key, receiver) {
// 增加一个 ISSELF 属性,用来返回当前正在代理的原始对象
if (key === ISSELF) {
return target
}
track(target, key)
return Reflect.get(target, key, receiver)
},
//
set(target, key, newVal, receiver) {
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
const res = Reflect.set(target, key, newVal, receiver)
// 判断当前代理对象所代理的是不是当前的原始对象,只有代理对象代理的是当前的原始对象时,才需要触发响应
if (target === receiver[ISSELF]) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
目前的响应形式是浅响应,也就是说对深处的属性做修改的话无法触发响应。
const obj = reactive({ foo: { bar: 1 } })
effect(() => {
console.log(obj.foo.bar)
})
// 修改深层属性无法触发响应
obj.foo.bar = 2
2
3
4
5
6
7
需要对深层的属性做递归代理操作:
function reactive(data) {
return new Proxy(data, {
get(target, key, receiver) {
if (key === ISSELF) {
return target
}
track(target, key)
// 获取属性值,判断是否为对象,为对象则递归代理
const res = Reflect.get(target, key, receiver)
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
有的时候我们只需要浅响应就够了,所以需要一个传参来决定当前到底使用浅响应还是深响应。
// 封装统一的代理创建函数,新增参数用来判断是否创建浅响应
function createReactive(data, isShallow = false) {
return new Proxy(data, {
get(target, key, receiver) {
if (key === ISSELF) {
return target
}
track(target, key)
const res = Reflect.get(target, key, receiver)
// 如果只需要浅响应则直接返回结果
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
})
}
// 深响应
function reactive(data) {
return createReactive(data)
}
// 浅响应
function shallowReactive(data) {
return createReactive(data, true)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
有时我们希望创建只读的响应数据,修改只读的数据时报出警告。
// 新增参数用来判断是否创建只读属性
function createReactive(data, isShallow = false, isReadonly = false) {
return new Proxy(data, {
get(target, key, receiver) {
if (key === ISSELF) {
return target
}
// 只读属性不需要收集依赖
if (!isReadonly) {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
// 如果不是浅响应,表示深层的属性也要变为只读的,所以要递归调用只读
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
set(target, key, newVal, receiver) {
// 只读属性不允许修改,报出警告
if (isReadonly) {
console.warn(`属性${key}是只读的`)
return true
}
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
const res = Reflect.set(target, key, newVal, receiver)
if (target === receiver[ISSELF]) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 只读属性不允许删除,报出警告
if (isReadonly) {
console.warn(`属性${key}是只读的`)
return true
}
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
trigger(target, key, TriggerType.DELETE)
}
return res
},
})
}
// 深只读
function readonly(data) {
return createReactive(data, false, true)
}
// 浅只读
function shallowReadonly(data) {
return createReactive(data, true, true)
}
const obj = readonly({ foo: { bar: 1 } })
effect(() => {
console.log(obj.foo.bar)
})
// 修改深层属性报出警告
obj.foo.bar = 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
代理数组 Array
数组其实是一种特殊的对象,对对象的代理操作,在数组上面也可以使用。
const arr = reactive(['foo'])
effect(() => {
console.log(arr[0])
})
// 能够触发响应
arr[0] = 'bar'
2
3
4
5
6
7
8
修改数组的元素有时候会影响数组的 length 属性。而且存在两种情况,一种是修改数组已有的元素,这种情况是纯修改,一种是修改数组没有的元素,这种情况是新增。所以需要对 length 属性做单独处理。
function createReactive(data, isShallow = false) {
return new Proxy(data, {
set(target, key, newVal, receiver) {
if (isReadonly) {
console.warn(`属性${key}是只读的`)
return true
}
const oldVal = target[key]
// 判断当前需要代理的是否为数组,为数组则通过当前key和length的大小来判断是新增还是修改
const type = Array.isArray(target)
? Number(key) < target.length
? TriggerType.SET
: TriggerType.ADD
: Object.prototype.hasOwnProperty.call(target, key)
? TriggerType.SET
: TriggerType.ADD
const res = Reflect.set(target, key, newVal, receiver)
if (target === receiver[ISSELF]) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
})
}
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const iterateEffect = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
if (type === TriggerType.ADD || type === TriggerType.DELETE) {
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
// 当操作为新增且代理的是数组的时候,要把length属性有关的响应取出来执行
if (type === TriggerType.ADD && Array.isArray(target)) {
const lengthEffect = depsMap.get('length')
lengthEffect &&
lengthEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
修改数组的 length 属性时,也有可能造成数组元素的改变,因此也需要触发响应,但是只有索引值大于等于新 length 值的元素才会被改变,所以只有这些元素需要触发响应。
function createReactive(data, isShallow = false) {
return new Proxy(data, {
set(target, key, newVal, receiver) {
if (isReadonly) {
console.warn(`属性${key}是只读的`)
return true
}
const oldVal = target[key]
const type = Array.isArray(target)
? Number(key) < target.length
? TriggerType.SET
: TriggerType.ADD
: Object.prototype.hasOwnProperty.call(target, key)
? TriggerType.SET
: TriggerType.ADD
const res = Reflect.set(target, key, newVal, receiver)
if (target === receiver[ISSELF]) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 将新值传入
trigger(target, key, type, newVal)
}
}
return res
},
})
}
function trigger(target, key, type, newVal) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const iterateEffect = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
if (type === TriggerType.ADD || type === TriggerType.DELETE) {
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
if (type === TriggerType.ADD && Array.isArray(target)) {
const lengthEffect = depsMap.get('length')
lengthEffect &&
lengthEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
// 当代理的是数组,且触发响应的属性是length时,取出副作用函数执行
if (Array.isArray(target) && key === 'length') {
depsMap.forEach((fns, index) => {
// 只有索引值大于等于新 length 值的元素才会被改变,所以只有这些元素收集的依赖需要被响应
if (index >= newVal) {
fns.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
})
}
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
const arr = reactive(['foo'])
effect(() => {
console.log(arr[0])
})
// 修改length为0,会导致arr[0]被删除,需要触发响应
arr.length = 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
数组也是对象,也可以使用 for...in 进行遍历,但是数组的 length 属性改变后才会影响 for...in 的次数。
function createReactive(data, isShallow = false) {
return new Proxy(data, {
ownKeys(target) {
// 判断当前代理的是否为数组,是数组使用length为key进行收集依赖
track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
},
})
}
const arr = reactive(['foo', 'foo', 'foo'])
effect(() => {
for (const k in arr) {
console.log(k)
}
})
// 触发响应,for...in重新遍历
arr.length = 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for...of 用来遍历可迭代对象,一个对象或它的原型上如果实现了@@iterator 方法(通过[Symbol.iterator]调用),这个对象就是可迭代的。数组内部实现了这个方法,所以数组本身就是可迭代的,可以用 for...of 遍历。 for...of 通过[Symbol.iterator]为 key 访问内部迭代器,再访问了数组的 length 属性和数组的索引,也能对 for...of 进行拦截。 values()方法与 for...of 相同,也是访问了[Symbol.iterator] key
function createReactive(data, isShallow = false) {
return new Proxy(data, {
get(target, key, receiver) {
if (key === ISSELF) {
return target
}
// 不需要对symbol类型的key做依赖收集
if (!isReadonly && typeof key !== 'symbol') {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
})
}
const arr = reactive(['foo', 'foo', 'foo'])
effect(() => {
for (const k of arr) {
console.log(k)
}
// values()方法也能追踪,因为values()方法本质就是返回数组内建的迭代器
// Array.prototype.values === Array.prototype[Symbol.iterator] // true
// for (const k of arr.values()) {
// console.log(k)
// }
})
// 触发响应,for...of重新遍历
arr[0] = 1
arr.length = 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
数组的查找方法,类似includes
、indexOf
、lastIndexOf
。在执行时内部会访问数组的 length 属性和数组的索引。因此现在已经能够触发响应,但还是有些特殊情况。
const arr = reactive([1, 2, 3])
effect(() => {
// 返回true
console.log(arr.includes(1))
})
// 修改后返回false
arr[0] = 2
// ------------------------------
const data = {}
const arr = reactive([data])
// 数组肯定包含他的第一项,原本应该返回true,但是返回false了
console.log(arr.includes(arr[0]))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
根据 ES 规范中,includes
的实现可以得知,内部会通过数组的索引访问元素,访问到arr[0]
时,由于其也是一个对象,会被深层代理成一个新的代理对象。 而我们打印的时候也主动访问了arr[0]
,得到的结果因为是个对象,所以也会被深层代理成一个新的代理对象,两个代理对象不是同一个,所以返回了 false。
// 定义一个map来储存已经被代理过的对象
const reactiveMap = new Map()
function reactive(data) {
// 如果当前对象已经被代理过了,则从map中取出并返回
const existionProxy = reactiveMap.get(data)
if (existionProxy) return existionProxy
// 否则创建代理对象,并储存到map中
const proxy = createReactive(data)
reactiveMap.set(data, proxy)
return proxy
}
const data = {}
const arr = reactive([data])
// 返回true
console.log(arr.includes(arr[0]))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
但是现在还会出现一个问题,就是使用原始对象去查询的时候,因为 includes
内部会访问 this,当前的 this 指向的是代理对象 arr,所以原始对象肯定不会存在于代理对象数组内。
const data = {}
const arr = reactive([data])
// 使用原始对象查询的时候返回false了,不符合直觉。
console.log(arr.includes(data))
2
3
4
为此需要重写 includes
方法,包括indexOf
,lastIndexOf
。
function createReactive(data, isShallow = false, isReadonly = false) {
return new Proxy(data, {
get(target, key, receiver) {
if (key === ISSELF) {
return target
}
// 如果当前代理的是数组,而且访问的key是被我们改写过的方法,则返回我们改写过的方法
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
}
if (typeof res === 'object' && res !== null) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
})
}
// 定义需要被重写的方法集合
const arrayInstrumentations = {}
// 重写方法
;['includes', 'indexOf', 'lastIndexOf'].forEach((method) => {
// 获取原本的方法名
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function (...args) {
// 执行原本的方法,this指向的是当前的代理对象,也就是说去代理对象中查找元素
let res = originMethod.apply(this, args)
// 为false表示在代理对象中没有找到,就去原始对象中查找
if (res === false) {
// 如果我们传入的是原始数据,这一步可以在原始对象找到需要的数据
res = originMethod.apply(this[ISSELF], args)
}
return res
}
})
const data = {}
const arr = reactive([data])
// 返回true
console.log(arr.includes(data))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
有些方法会隐式的修改数组的 length 属性,例如 push、pop、shift、unshift、splice。这些方法在使用时会隐式的把数组的 length 改掉,因此需要重写这些方法。
const arr = reactive([])
// 这两个不相关的副作用函数会互相影响,因为push方法会隐式的访问和修改length属性,
// 第一个副作用函数修改length之后会触发第二个副作用函数的执行,第二个也会修改length属性触发第一个的执行。
// 如此循环往复,最终造成栈溢出
effect(() => {
arr.push(1)
})
effect(() => {
arr.push(1)
})
2
3
4
5
6
7
8
9
10
11
12
解决这种问题的方法是执行这些方法时,当它们访问 length 属性时,不做依赖收集。这样就不会让 length 属性和副作用函数建立依赖。
// 定义全局变量表示是否需要收集依赖
let shouldTrack = true
function track(target, key) {
// shouldTrack为false则直接不追踪
if (!activeEffect || !shouldTrack) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
// 改写这些会隐式改变数组length的方法
const arrayLengthFunctions = {}
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach((method) => {
const originMethod = Array.prototype[method]
arrayLengthFunctions[method] = function (...args) {
// 执行原始方法之前,将shouldTrack置为fasle,这样在这些原始方法中访问到length时就不会触发依赖收集
shouldTrack = false
let res = originMethod.apply(this, args)
// 执行完成之后置为true
shouldTrack = true
return res
}
})
function createReactive(data, isShallow = false, isReadonly = false) {
return new Proxy(data, {
get(target, key, receiver) {
if (key === ISSELF) {
return target
}
// 如果当前代理的是数组,而且访问的key是被我们改写过的方法,则返回我们改写过的方法
if (Array.isArray(target)) {
if (arrayInstrumentations.hasOwnProperty(key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (arrayLengthFunctions.hasOwnProperty(key)) {
return Reflect.get(arrayLengthFunctions, key, receiver)
}
}
if (!isReadonly && typeof key !== 'symbol') {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
})
}
const arr = reactive([])
// 正常执行
effect(() => {
arr.push(1)
})
effect(() => {
arr.push(1)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
代理 Set、Map
Set、Map 类型读取属性和设置属性的方法和普通对象不一样,但拦截的整体思路一致,即读取属性时 track 收集依赖,设置属性时 trigger 触发响应。
const data = new Set([1, 2, 3])
const obj = new Proxy(data, {})
// 对set类型代理,访问size属性会报错
console.log(obj.size)
2
3
4
5
Set.prototype.size
中会访问 this 属性,当前的 this 指向的是代理对象 obj,而代理对象不存在内部方法[[SetData]]
,所以报错。
const data = new Set([1, 2, 3])
const obj = new Proxy(data, {
get(target, key, receiver) {
// 访问size属性时,将当前的this指向原始对象
if (key === 'size') {
return Reflect.get(target, key, target)
}
return Reflect.get(target, key, receiver)
},
})
// 能正确输出3
console.log(obj.size)
2
3
4
5
6
7
8
9
10
11
12
13
14
删除属性的时候也会遇到类似的问题。
const data = new Set([1, 2, 3])
const obj = new Proxy(data, {
get(target, key, receiver) {
if (key === 'size') {
return Reflect.get(target, key, target)
}
return Reflect.get(target, key, receiver)
},
})
// 删除属性时报错
console.log(obj.delete(1))
2
3
4
5
6
7
8
9
10
11
12
13
删除属性时执行的是p.delete(1)
语句,这个语句中 this 会始终指向代理对象 p,所以只能修改 delete 方法本身的 this 绑定。
const data = new Set([1, 2, 3])
const obj = new Proxy(data, {
get(target, key, receiver) {
if (key === 'size') {
return Reflect.get(target, key, target)
}
// 直接将当前方法中的this绑定到target
return target[key].bind(target)
},
})
// 正常删除返回true
console.log(obj.delete(1))
2
3
4
5
6
7
8
9
10
11
12
13
14
Map 和 Set 类型中的各种方法需要重写,封装成统一的创建代理函数来实现
// 重写各种操作方法
const mutableInstrumentations = {
add(key) {
// 调用add方法时this指向代理对象,通过[ISSELF]属性获取原始对象
const target = this[ISSELF]
// 判断是否已经存在key
const hadKey = target.has(key)
// 在原始对象上执行add操作
const res = target.add(key)
// 原来不存在key时才触发响应,响应的type为add
if (!hadKey) {
trigger(target, key, TriggerType.ADD)
}
return res
},
// 根据add可以实现delete逻辑
delete(key) {
const target = this[ISSELF]
const hadKey = target.has(key)
const res = target.delete(key)
// 原来存在key则触发响应
if (hadKey) {
trigger(target, key, TriggerType.ADD)
}
return res
},
}
function createReactive(data, isShallow = false, isReadonly = false) {
return new Proxy(data, {
get(target, key, receiver) {
if (key === ISSELF) return target
if (key === 'size') {
// 收集依赖,新增和删除会影响size属性,我们用一个Symbol值来代理size属性
track(target, ITERATE_KEY)
return Reflect.get(target, key, target)
}
// 返回重写的方法
return mutableInstrumentations[key]
},
})
}
const obj = reactive(new Set([1, 2, 3]))
effect(() => {
console.log(obj.size)
})
// 正常触发副作用函数
obj.add(4)
obj.delete(4)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
get 和 set 方法的实现,调用 set 方法触发响应时,需要注意不要造成数据污染。
const mutableInstrumentations = {
get(key) {
const target = this[ISSELF]
const had = target.has(key)
track(target, key)
if (had) {
const res = target.get(key)
// 递归代理 可以实现深代理和浅代理,这是只实现了深代理
return typeof res === 'object' ? reactive(res) : res
}
},
set(key, value) {
const target = this[ISSELF]
const had = target.has(key)
// 获取旧值
const oldVal = target.get(key)
// value可能是响应式对象,直接给原始对象设置一个响应式对象会造成原始数据的污染
target.set(key, value[ISSELF] || value)
if (!had) {
trigger(target, key, TriggerType.ADD)
} else if (oldVal !== value || (oldVal === oldVal && value === value)) {
trigger(target, key, TriggerType.SET)
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
处理 forEach 遍历。
const mutableInstrumentations = {
forEach(callback, thisArg) {
// 定义一个包裹函数,如果参数是对象的话,则将其设置为响应式对象
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
const target = this[ISSELF]
track(target, ITERATE_KEY)
// 调用原始对象遍历,将参数都设置为响应式对象
target.forEach((v, k) => {
callback.call(thisArg, wrap(v), wrap(k), this)
})
},
}
2
3
4
5
6
7
8
9
10
11
12
forEach 遍历和 for...in 遍历存在本质不同,forEach 除了关心 key 之外,还关心 value 的变化,也就是说即使是 set 操作改变了某个 key 对应的 value,没有改变遍历的次数,也需要重新遍历,所以当我们触发响应的时候,要对 map 类型做特殊处理。
function trigger(target, key, type, newVal) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const iterateEffect = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
// 即使是set类型,如果代理的是Map类型,也需要触发响应
if (
type === TriggerType.ADD ||
type === TriggerType.DELETE ||
(type === TriggerType.SET && Object.prototype.toString.call(target) === '[object Map]')
) {
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
if (type === TriggerType.ADD && Array.isArray(target)) {
const lengthEffect = depsMap.get('length')
lengthEffect &&
lengthEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
if (Array.isArray(target) && key === 'length') {
depsMap.forEach((fns, key) => {
if (key >= newVal) {
fns.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
})
}
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
const obj = reactive(new Map([['key', 1]]))
effect(() => {
obj.forEach(function (value, key) {
console.log(value)
})
})
// set修改值,副作用函数也会重新执行
obj.set('key', 2)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
对于 entries、keys、values 等迭代器方法,内部都会调用 [Symble.iterator] 迭代器,Map 和 Set 内部已经部署了迭代器,所以可以直接 for...of 迭代。 但是我们的响应式对象上没有迭代器,所以 for...of 迭代时会报错,我们要自己实现它。
const mutableInstrumentations = {
[Symbol.iterator]: iterationMethod,
entries: iterationMethod,
}
function iterationMethod() {
const target = this[ISSELF]
const itr = target[Symbol.iterator]()
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
track(target, ITERATE_KEY)
return {
// 迭代器协议,表示使用next方法进行迭代操作
next() {
const { value, done } = itr.next()
return {
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done,
}
},
// 可迭代协议,实现了这个方法表示这个对象是可迭代的
[Symbol.iterator]() {
return this
},
}
}
const obj = reactive(
new Map([
['key1', 1],
['key2', 2],
])
)
effect(() => {
for (const [key, value] of obj.entries()) {
console.log(key, value)
}
})
// 触发for...of重新迭代
obj.set('key2', 3)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
values 和 keys 方法的实现与 entries 大致相同。
const mutableInstrumentations = {
values: valuesIterationMethod,
keys: keysIterationMethod,
}
// 之前设置了Map类型在set操作的时候也会执行副作用函数。
// 但是Map类型在使用keys方法迭代时,就算修改了某些值,keys也可能是不变的,所以跟keys迭代相关的副作用函数就不需要执行。
// 使用一个新的Symbol值来表示这种情况进行依赖收集。
const MAP_KEY_ITERATE_KEY = Symbol()
function valuesIterationMethod() {
const target = this[ISSELF]
// 通过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 {
// values迭代只有value值
value: wrap(value),
done,
}
},
[Symbol.iterator]() {
return this
},
}
}
function keysIterationMethod() {
const target = this[ISSELF]
// 通过keys方法拿到原始迭代器
const itr = target.keys()
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
// keys方法使用新的Symbol类型key来储存副作用函数
track(target, MAP_KEY_ITERATE_KEY)
return {
next() {
const { value, done } = itr.next()
return {
// keys迭代只有value值
value: wrap(value),
done,
}
},
[Symbol.iterator]() {
return this
},
}
}
function trigger(target, key, type, newVal) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
if (
type === TriggerType.ADD ||
type === TriggerType.DELETE ||
(type === TriggerType.SET && Object.prototype.toString.call(target) === '[object Map]')
) {
const iterateEffect = depsMap.get(ITERATE_KEY)
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
// 当Map类型新增和删除属性时,取出MAP_KEY_ITERATE_KEY为key所对应的副作用函数执行
if ((type === TriggerType.ADD || type === TriggerType.DELETE) && Object.prototype.toString.call(target) === '[object Map]') {
const iterateEffect = depsMap.get(MAP_KEY_ITERATE_KEY)
iterateEffect &&
iterateEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
if (type === TriggerType.ADD && Array.isArray(target)) {
const lengthEffect = depsMap.get('length')
lengthEffect &&
lengthEffect.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
if (Array.isArray(target) && key === 'length') {
depsMap.forEach((fns, key) => {
if (key >= newVal) {
fns.forEach((fn) => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
}
})
}
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
const obj = reactive(
new Map([
['key1', 1],
['key2', 2],
])
)
effect(() => {
for (const value of obj.values()) {
console.log(value)
}
})
effect(() => {
for (const value of obj.keys()) {
console.log(value)
}
})
// 修改属性值,只有values相关的副作用函数会重新执行
obj.set('key2', 3)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141