《Vue.js 设计与实现》读书笔记 - 第5章、非原始值的响应式方案
第5章、非原始值的响应式方案
5.1 理解 Proxy 和 Reflect
Proxy
Proxy
只能代理对象,不能代理非对象原始值,比如字符串。Proxy
会拦截对对象的基本语义,并重新定义对象的基本操作。
const p = new Proxy(obj, {
get() {...}, // 拦截读取操作
set() {...}, // 拦截设置操作
})
const fn = (name) => {
console.log('i am ', name);
}
const p2 = new Proxy(fn, {
apply(target, thisArg, argArray) {
target.call(thisArg, ...argArray)
}
})
p2('hcy') // i am hcy
Proxy
构造函数接受两个参数,第一个参数是被代理的对象,第二个参数是一个对象,包含一组夹子(trap)。
但是 Proxy
只能拦截基本操作,不能拦截复合操作。比如调用对象下的方法,obj.fn()
,因为这里先通过 get
获取值再调用。
Reflect
Reflect 是一个全局对象,其下面有很多方法,而这些方法和 Proxy
拦截器都同名,作用是提供访问一个对象属性的默认行为。
下面这两种操作是等价的。
const obj = { foo: 1 };
console.log(obj.foo); // 直接读取
console.log(Reflect.get(obj, 'foo')); // Reflect读取
其中 Reflect.get
可以指定第三个参数,指定接受者。下面例子说明应用
点击查看代码
const obj = {
foo: 1,
get bar() {
return this.foo;
},
};
const p = new Proxy(obj, {
get(target, key) {
track(target, key);
// 这里直接读取target的值
return target[key];
},
set(target, key, newVal) {
// 同样直接设置target的属性值
target[key] = newVal;
trigger(target, key);
return true;
},
});
effect(() => {
console.log(p.bar);
});
p.foo++;
我们读取 p.bar
依赖了 foo
字段,但是修改 p.foo
的时候,却没有执行副作用函数,因为我们在读取 bar
方法中的 this.foo
读取到的是 obj
这个原始对象的属性。
通过 Reflect.get
的第三个参数指定 receiver
来解决这个问题,这里可以理解为函数的 this
。
const p = new Proxy(obj, {
get(target, key, receiver) {
track(target, key);
// 通过Reflect.get读取值,并指定receiver
return Reflect.get(target, key, receiver);
},
});
5.2 JavaScript 对象及 Proxy 的工作原理
根据 ECMAScript 规范,对象分为常规对象和异质对象。
我们通过 [[xxx]]
代表对象的内部方法。比如 [[Get]]
,常规对象就是一些指定的内部方法按照指定定义来实现的对象,其他都是异质对象。
我们创建代理对象时,如果指定了某些拦截函数,就是指定了这个代理对象的行为,而如果没有指定,它就会调用原始对象的内部方法。
5.3 如何代理 Object
对一个普通对象的所有可能的读取操作
- 访问属性
obj.foo
- 判断对象或原型上是否存在给定的 key
key in obj
- 使用
for...in
循环遍历对象for (const key in obj) {}
拦截 in
操作符:
const obj = {
foo: 1,
};
const p = new Proxy(obj, {
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
return true;
},
});
effect(() => {
'foo' in p;
console.log(1111);
});
p.foo = 1
拦截 for...in
循环,通过拦截 ownKeys
,因为遍历并不会操作某一个指定的属性,我们需要创建一个 key
用了记录相关依赖,然后在 ownKeys
收集依赖,在新增或删除属性时触发依赖。
const ITERATE_KEY = Symbol();
const p = new Proxy(obj, {
ownKeys(target) {
// 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
});
接下来要做的就是拦截新增和删除属性。就是在 trigger
函数中新增执行的遍历收集的依赖函数。直接放完整代码。
点击查看代码
let activeEffect;
const effectStack = [];
function effect(fn, options = {}) {
const effectFn = () => {
// 执行前先清除依赖
cleanup(effectFn);
// 执行前先压入栈中
activeEffect = effectFn;
effectStack.push(effectFn);
const res = fn();
// 执行后弹出
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
// 存储fn的计算结果并返回
return res;
};
// 把 options 挂在 effectFn 上
effectFn.options = options;
// 用来存储与该副作用函数相关联的依赖集合
effectFn.deps = [];
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn;
}
function cleanup(effectFn) {
// 很简单 就是在每个依赖集合中把该函数删除
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
const bucket = new WeakMap();
// 在 track 中记录 deps
function track(target, key) {
if (!activeEffect) 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);
}
const ITERATE_KEY = Symbol();
function trigger(target, key, type) {
let depsMap = bucket.get(target);
if (depsMap) {
const effects = depsMap.get(key);
const effectsToRun = new Set();
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
if (type === 'ADD' || type === 'DELETE') {
const iterateEffects = depsMap.get(ITERATE_KEY);
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器 就用调度器执行副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则就直接执行
effectFn();
}
});
}
}
const obj = {
foo: 1,
};
const p = new Proxy(obj, {
ownKeys(target) {
// 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
set(target, key, newVal, receiver) {
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD';
// res就是设置的结果
const res = Reflect.set(target, key, newVal, receiver);
trigger(target, key, type);
return res;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
// 存在属性且删除成功才触发
trigger(target, key, 'DELETE');
}
return res;
},
});
effect(() => {
console.log('====');
for (const key in p) {
console.log(key);
}
});
p.bar = 1;
delete p.foo
5.4 合理地触发响应
赋值时要判断新旧值不相等才需要出发依赖。注意要对 NaN
特殊判断。
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 && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
return res;
},
接下来考虑对 Proxy
做一层封装,用于对任意对象做响应式封装。
function reactive(obj) {
return new Proxy(obj, {...})
}
现在考虑这样一个场景
const obj = {};
const proto = { bar: 1 };
const child = reactive(obj);
const parent = reactive(proto);
Object.setPrototypeOf(child, parent);
effect(() => {
console.log(child.bar);
});
child.bar = 2;
我们创建了两个响应式对象,并把 child
的原型设置为 perent
,现在修改 child.bar
会发现触发了两次副作用的执行。
原因是我们的child
上并没有 bar
属性,这样我们会读取到 parent
的属性,所以在 parent
上也收集了副作用函数,而设置的时候,同样会在 parent
上进行设置,这样又触发了 parent
的依赖,所以在 parent
和 child
上分别执行了一次。
现在的问题是,我们不应该在 parent
上进行触发。而在 set
中有第三个参数 receiver
表示设置的代理对象。
set(target, key, value, receiver) {
// child 中
// target 是 obj, receiver 是 child
// parent 中
// target 是 proto, receiver 还是 child
}
所以只需要通过 receiver
比较一下就可以了。不过我们要先拦截 get
中的 raw
属性,以便我们能够获取原始值。
get(target, key, receiver) {
if (key === 'raw') {
return target;
}
track(target, key);
// 通过Reflect.get读取值,并指定receiver
return Reflect.get(target, key, receiver);
},
现在我们可以通过 proxy.raw
获取代理对象的原始数据。
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 (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
}
return res;
},
5.5 浅响应与深响应
我们上面实现的响应式是浅响应,如果是嵌套对象的话,修改内部的嵌套属性不会触发响应,而一般情况下我们需要实现深相应,这时我们就需要对对象递归调用 reactive
。
function createReactive(obj, isShallow = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === 'raw') {
return target;
}
const res = Reflect.get(target, key, receiver);
track(target, key);
if (isShallow) {
return res;
}
if (typeof res === 'object' && res !== null) {
return createReactive(res);
}
return res;
},
}
}
function reactive(obj) {
return createReactive(obj);
}
function shallowReactive(obj) {
return createReactive(obj, true);
}
通过参数 isShallow
可以实现浅响应和深响应的切换。
5.6 只读和浅只读
有些时候我们还需要实现数据只读,这个比较简单,就是在 set
的时候和 delete
的时候拦截一下就可以,同时如果数据是只读的,也就没有比较进行响应式处理了,所以在 get
也不需要收集依赖。
点击查看代码
let activeEffect;
const effectStack = [];
function effect(fn, options = {}) {
const effectFn = () => {
// 执行前先清除依赖
cleanup(effectFn);
// 执行前先压入栈中
activeEffect = effectFn;
effectStack.push(effectFn);
const res = fn();
// 执行后弹出
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
// 存储fn的计算结果并返回
return res;
};
// 把 options 挂在 effectFn 上
effectFn.options = options;
// 用来存储与该副作用函数相关联的依赖集合
effectFn.deps = [];
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn;
}
function cleanup(effectFn) {
// 很简单 就是在每个依赖集合中把该函数删除
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
const bucket = new WeakMap();
// 在 track 中记录 deps
function track(target, key) {
if (!activeEffect) 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);
}
const ITERATE_KEY = Symbol();
function trigger(target, key, type) {
let depsMap = bucket.get(target);
if (depsMap) {
const effects = depsMap.get(key);
const effectsToRun = new Set();
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
if (type === 'ADD' || type === 'DELETE') {
const iterateEffects = depsMap.get(ITERATE_KEY);
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器 就用调度器执行副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则就直接执行
effectFn();
}
});
}
}
function reactive(obj) {
return new Proxy(obj, {
ownKeys(target) {
// 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
get(target, key, receiver) {
if (key === 'raw') {
return target;
}
track(target, key);
// 通过Reflect.get读取值,并指定receiver
return Reflect.get(target, key, receiver);
},
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 (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
}
return res;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
// 存在属性且删除成功才触发
trigger(target, key, 'DELETE');
}
return res;
},
});
}
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
ownKeys(target) {
// 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
get(target, key, receiver) {
if (key === 'raw') {
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)
? 'SET'
: 'ADD';
const res = Reflect.set(target, key, newVal, receiver);
if (target === receiver.raw) {
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, 'DELETE');
}
return res;
},
});
}
function reactive(obj) {
return createReactive(obj);
}
function shallowReactive(obj) {
return createReactive(obj, true);
}
function readonly(obj) {
return createReactive(obj, false, true);
}
function shallowReadonly(obj) {
return createReactive(obj, true, true);
}
const obj = reactive({ foo: { bar: 1 } });
effect(() => {
console.log(obj.foo.bar);
});
obj.foo.bar = 2;
5.7 代理数组
数组是异质对象,所以有些操作需要特殊处理。
5.7.1 数组索引与 length
我们通过下标设置或获取元素值,比如arr[0]=1
都可以正常通过拦截。但是数组中还有个特殊的属性 length
,我们设置的下标如果大于 length
,length
会被更新。我们设置 length
如果小于之前的 length
,那么大于 length
的元素都会被删除,我们要把对应的副作用函数全部执行。
这里主要是调整 set
和 trigger
点击查看代码
function trigger(target, key, type, newVal) {
let depsMap = bucket.get(target);
if (depsMap) {
const effects = depsMap.get(key);
const effectsToRun = new Set();
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
if (type === 'ADD' || type === 'DELETE') {
const iterateEffects = depsMap.get(ITERATE_KEY);
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
if (type === 'ADD' && Array.isArray(target)) {
const lengthEffects = depsMap.get('length');
lengthEffects &&
lengthEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
if (Array.isArray(target) && key === 'length') {
depsMap.forEach((effects, key) => {
if (key >= newVal) {
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
});
}
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器 就用调度器执行副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则就直接执行
effectFn();
}
});
}
}
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 = 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.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type, newVal);
}
}
return res;
},
});
}
const arr = reactive([1, 2]);
effect(() => {
console.log(arr[0]);
console.log(arr[1]);
});
arr.length = 1; // 1 undefined
5.7.2 遍历数组
我们之前通过 for...in
遍历数组,自定义了一个 ITERATE_KEY
,但是对于数组来说,我们只需要通过 length
收集依赖就可以了。
ownKeys(target) {
// 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
track(target, Array(target) ? 'length' : ITERATE_KEY);
return Reflect.ownKeys(target);
},
对于 for...of
遍历我们不需要特殊处理,因为我们对元素和length
都做了处理。当然我们知道迭代器是通过 @@iterator(Symbol.iterator)
指定的,而为了避免错误和性能考虑,我们不应该和 symbol
值建立响应关系。
get(target, key, receiver) {
if (key === 'raw') {
return target;
}
if (!isReadonly && typeof key !== 'symbol') { // 新增
track(target, key);
}
// ...
},
5.7.3 数组的查找方法
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0]));
按照之前的代码,上面的情况会返回 false
因为我们每次 get
的时候,如果获取的是对象就会进行响应式操作,这样导致两次读取的时候,生成了两次 proxy
对象,所以不相等。现在我们需要维护一个映射,对于同一个原始对象应该返回相同的 proxy
对象。
// 存储原始对象到代理的映射
const reactiveMap = new Map();
function reactive(obj) {
const existionProxy = reactiveMap.get(obj);
if (existionProxy) return existionProxy;
const proxy = createReactive(obj);
reactiveMap.set(obj, proxy);
return proxy;
}
但是很显然还是有问题,我们把数组元素对象变成响应式的代理值了,那么我们再查原始值明显差不到了。
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj));
解决思路就是,把代理值匹配一遍,再把原始值匹配一遍。
const arrayInstrumentations = {};
['includes', 'indexOf', 'lastIndexOf'].forEach((method) => {
const originMethod = Array.prototype[method];
arrayInstrumentations[method] = function (...args) {
let res = originMethod.apply(this, args);
if (res === false) {
res = originMethod.apply(this.raw, args);
}
return res;
};
});
get(target, key, receiver) {
if (key === 'raw') {
return target;
}
if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// ...
}
5.7.4 隐式修改数组长度的原型方法
下面的代码会造成死循环,因为两个副作用函数都会监听 length
但是又都会修改 length
。
const arr = reactive([]);
effect(() => {
arr.push(1);
});
effect(() => {
arr.push(1);
});
修改思路就是屏蔽对 length
的监听。引因为 push
的语义是修改而不是读取,我们可以屏蔽原始操作的响应。
let shouldTrack = true;
const arrayInstrumentations = {};
['push', 'pop', 'shift', 'unshift', 'splice'].forEach((method) => {
const originMethod = Array.prototype[method];
arrayInstrumentations[method] = function (...args) {
shouldTrack = false;
let res = originMethod.apply(this, args);
shouldTrack = true;
return res;
};
});
function track(target, key) {
if (!activeEffect || !shouldTrack) return;
// ...
}
5.8 代理 Map 和 Set
这些数据结构和普通对象相比有很多特殊的属性和方法,所以需要特殊处理。
5.8.1 如何代理 Map 和 Set
const s = new Set([1, 2, 3]);
const p = new Proxy(s, {})
console.log(p.size);
如上程序会出现报错:"TypeError: Method get Set.prototype.size called on incompatible receiver #
实际上 Set.prototype.size
是一个属性访问器,它的 set
访问器为 undefined
,它的 get
访问器计算 set 的元素个数并返回。
在 get
访问器内部会调用内部方法,但是现在我们通过代理调用的时候,由于我们把 this
指定为 proxy
对象,所以报错了,所以我们需要改动之前的代码,如果是原始对象是 Set
且获取 size
的时候,我们把 receiver
指定为原始对象。
const s = new Set([1, 2, 3]);
const p = new Proxy(s, {
get(target, key, receiver) {
if (key === 'size') {
return Reflect.get(target, key, target)
}
return Reflect.get(target, key, receiver)
}
})
console.log(s.size);
同时 delete
也会出现类似问题,不过 delete
是函数而不是访问器,所以我们可以通过 bind
来指定 this
。最后需要把这部分集成到之前的代码。
get(target, key, receiver) {
if (key === 'size') {
return Reflect.get(target, key, target)
}
return target[key].bind(target)
}
其次就是对 size
进行响应式处理,在 add
或 delete
时触发,和前面对普通对象的处理一样,绑定到 ITERATE_KEY
键上,并在 add
和 delete
时触发。
5.8.3 避免污染原始数据
这里是说如果在 Map 中设置一个响应式数据的话,会导致用户通过原始值调用也会触发响应式操作导致混乱。所以我们设置值的时候,如果发现是响应式值,只保存它的原始值。
这里使用的 raw
可能与用户定义属性重名,可以使用 Symbol
避免,同时,其他集合类型,Set 和数组都需要做类似的处理。
点击查看代码
set(key, value) {
const target = this.raw;
const had = target.has(key);
const oldValue = target.get(key);
// 获取原始值并设置
const rawValue = value.raw || value;
target.set(key, rawValue)
if (!had) {
trigger(target, key, 'ADD');
} else if (
oldValue !== value &&
(oldValue === oldValue || value === value)
) {
trigger(target, key, 'SET');
}
},
5.8.4 处理 forEach
forEach 遍历 Map 时,我们要对 ITERATE_KEY
建立响应,同时不仅在 add
和 delete
时触发,在 set
时也要触发相应,因为遍历时要读取值。
同时,我们在获取值的时候,应该做响应式处理。
// trigger 函数中如果对象是 Map,那么 SET 也出要出发相应
if (
type === 'ADD' ||
type === 'DELETE' ||
(type === 'SET' &&
Object.prototype.toString.call(target) === '[object Map]')
)
然后 mutableInstrumentations
添加 forEach
函数
forEach(callback, thisArg) {
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val);
const target = this.raw;
track(target, ITERATE_KEY);
target.forEach((v, k) => {
callback.call(thisArg, wrap(v), wrap(k), this);
});
},
5.8.5 迭代器方法
集合类型有三个迭代器方法:entries
,keys
,values
。其中,
m[Symbol.iterator] === m.entries // true
我们实现这个方法,第一点注意一定要有 Symbol.iterator
属性,其次要把key/value
都做响应式处理,然后要和 ITERATE_KEY
绑定,
点击查看代码
const mutableInstrumentations = {
// ...
[Symbol.iterator]: iterationMethod,
enties: iterationMethod,
};
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;
},
};
}
5.8.6 values 和 keys 方法
方法和 entries
类似。不过要注意,对于 entries
和 values
即使是 SET 操作也需要触发执行,但是 keys
只有在新增和删除才会执行,所以给它单独绑定到 MAP_KET_ITERATE_KEY
。
代码懒得写了。。。。