第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 的依赖,所以在 parentchild 上分别执行了一次。

现在的问题是,我们不应该在 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,我们设置的下标如果大于 lengthlength 会被更新。我们设置 length 如果小于之前的 length,那么大于 length 的元素都会被删除,我们要把对应的副作用函数全部执行。

这里主要是调整 settrigger

点击查看代码
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 进行响应式处理,在 adddelete 时触发,和前面对普通对象的处理一样,绑定到 ITERATE_KEY 键上,并在 adddelete 时触发。

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 建立响应,同时不仅在 adddelete 时触发,在 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 类似。不过要注意,对于 entriesvalues 即使是 SET 操作也需要触发执行,但是 keys 只有在新增和删除才会执行,所以给它单独绑定到 MAP_KET_ITERATE_KEY

代码懒得写了。。。。

《Vue.js 设计与实现》读书笔记 - 第5章、非原始值的响应式方案的更多相关文章

  1. 【vue.js权威指南】读书笔记(第一章)

    最近在读新书<vue.js权威指南>,一边读,一边把笔记整理下来,方便自己以后温故知新,也希望能把自己的读书心得分享给大家. [第1章:遇见vue.js] vue.js是什么? vue.j ...

  2. 【vue.js权威指南】读书笔记(第二章)

    [第2章:数据绑定] 何为数据绑定?答曰:数据绑定就是将数据和视图相关联,当数据发生变化的时候,可以自动的来更新视图. 数据绑定的语法主要分为以下几个部分: 文本插值:文本插值可以说是最基本的形式了. ...

  3. Linux内核设计与实现 读书笔记 转

    Linux内核设计与实现  读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...

  4. 【2018.08.13 C与C++基础】C++语言的设计与演化读书笔记

    先占坑 老实说看这本书的时候,有很多地方都很迷糊,但却说不清楚问题到底在哪里,只能和Effective C++联系起来,更深层次的东西就想不到了. 链接: https://blog.csdn.net/ ...

  5. 《Linux内核设计与实现》第八周读书笔记——第四章 进程调度

    <Linux内核设计与实现>第八周读书笔记——第四章 进程调度 第4章 进程调度35 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配 ...

  6. 《Linux内核设计与分析》第六周读书笔记——第三章

    <Linux内核设计与实现>第六周读书笔记——第三章 20135301张忻估算学习时间:共2.5小时读书:2.0代码:0作业:0博客:0.5实际学习时间:共3.0小时读书:2.0代码:0作 ...

  7. 《LINUX内核设计与实现》第三周读书笔记——第一二章

    <Linux内核设计与实现>读书笔记--第一二章 20135301张忻 估算学习时间:共2小时 读书:1.5 代码:0 作业:0 博客:0.5 实际学习时间:共2.5小时 读书:2.0 代 ...

  8. 《Linux内核设计与实现》第四周读书笔记——第五章

    <Linux内核设计与实现>第四周读书笔记--第五章 20135301张忻 估算学习时间:共1.5小时 读书:1.0 代码:0 作业:0 博客:0.5 实际学习时间:共2.0小时 读书:1 ...

  9. 《Linux内核设计与实现》第五周读书笔记——第十一章

    <Linux内核设计与实现>第五周读书笔记——第十一章 20135301张忻 估算学习时间:共2.5小时 读书:2.0 代码:0 作业:0 博客:0.5 实际学习时间:共3.0小时 读书: ...

  10. 《Linux内核设计与实现》读书笔记——第五章

    <Linux内核设计与实现>读书笔记--第五章 标签(空格分隔): 20135321余佳源 第五章 系统调用 操作系统中,内核提供了用户进程与内核进行交互的一组接口.这些接口让应用程序受限 ...

随机推荐

  1. 题解:CF1984B Large Addition

    题解:CF1984B Large Addition 题意 判断 \(n\) 是否是两个位数相同的 \(large\) 数的和. 思路 有以下三种证明方法: 最高位为 \(1\),因为两个 \(larg ...

  2. 安装jieba中文分词库

    插入一条: 有个更快安装下载jieba的方法,用镜像下载,非常快,2秒就行 pip install jieba -i https://pypi.douban.com/simple/ 1.打开官方网站: ...

  3. PixiJS源码分析系列:第三章 使用 canvas 作为渲染器

    使用 canvasRenderer 渲染 上一章分析了一下 Sprite 在默认 webgl 渲染器上的渲染,这章让我们把目光聚集到 canvasRenderer 上 使用 canvas 渲染器渲染图 ...

  4. Jmeter参数化5-JSON提取器

    后置处理器[JSON提取器] ,一般放于请求接口下面,用于获取接口返回数据里面的json参数值 1.以下json为例,接口返回的json结果有多组数据.我们要取出purOrderNo值 2.在jmet ...

  5. 【RabbitMQ】03 订阅模式

    Pub / Sub 订阅模式 特点是 一条消息可以给多个消费者接收了 首先创建订阅模式生产者发生一些代码变动: package cn.dzz.pubSub; import com.rabbitmq.c ...

  6. 人形机器人|星动纪元开源端到端强化学习训练框架“Humanoid-Gym”,实现「sim-to-real」 功能

    相关: https://www.leiphone.com/category/robot/cJo6GYgVkx8iQ9T7.html 开源的 Humanoid-Gym 框架,主要实现的技术有: 通过精心 ...

  7. Ubuntu Server无桌面无显示器情况下虚拟屏幕xvfb的安装及设置—ubuntu18.04server服务器系统下为python安装虚拟显示器 (使用jupyter notebook在web端播放openai的gym下保存的运行视频——需安装ipython)

    1.  安装xvfb sudo apt-get install xvfb Xvfb是流行的虚拟现实库,可以使很多需要图形界面的程序虚拟运行. 2. 安装pyvirtualdisplay pyvirtu ...

  8. windows系统下安装最新版gym的安装方法(此时最新版的gym为0.24.0,gym==0.24.0)

    当前gym的最新版本为0.24.0,本篇介绍对gym[atari]==0.24.0进行安装. 使用pip安装: pip install gym[atari] 可以看到此时安装的是ale_py而不是at ...

  9. Apache DolphinScheduler 1.3.4升级至3.1.2版本过程中的踩坑记录

    因为在工作中需要推动Apache DolphinScheduler的升级,经过预研,从1.3.4到3.1.2有的体验了很大的提升,在性能和功能性有了很多的改善,推荐升级. 查看官方的升级文档,可知有提 ...

  10. Ruoyi-Cloud 启动失败的坑,关于 selectConfigList

    刚才编辑了一堆,不知道为啥加了个英文单词,当前页面刷新自动搜索了单词,之前的内容总的就是现在都要会SpringCloud,高并发,几个真正懂高并发的,问题一般项目也没有啥高并发.自己之前的项目遇到过高 ...