第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. 2023/4/16 SCRUM个人博客

    1.我昨天的任务 大体学习并了解初始化pyqt5的一些可视化问题 2.遇到了什么困难 对于py的字典使用 3.我今天的任务 学习了easydict库的基本操作

  2. app备案证明需要提供md5值和公钥的解决方案

    现在app上架华为市场.小米市场.苹果市场等大型的应用商店,都需要提供国内的app备案证明.无论是安卓还是ios,都需要备案了. 但是问题是备案的时候需要填写app的bundle ID.公钥和MD5值 ...

  3. Mysql函数1-IFNULL

    IFNULL函数用于判断参数值是null时则返回指定内容. 原本 select goods_base_name,goods_id from goods where goods_id in (6,7,8 ...

  4. 对比python学julia(第三章:游戏编程)--(第一节)初识游戏库(3)

    1.1.    键盘和鼠标控制 在游戏应用程序中,通常使用键盘和鼠标作为游戏的操作设备.游戏的窗口都能接收来自键盘和鼠标设备的输人.当用户在键盘上按下按建或释放按键时,会产生相应的键盘事件:当用户移动 ...

  5. 【微信小程序】 自定义组件

    创建微信小程序组件 在小程序中创建组件: 1.项目根目录中创建[components]目录,存放自定义组件 2.进入components目录,给组件创建一个组件目录 3.右键组件目录,选择[创建Com ...

  6. Jax框架的Traced object特性与TensorFlow的placeholder的一致性

    前文: Jax框架的static与Traced Operations -- Static vs Traced Operations 前文讨论分析了Jax的static特性和Traced特性,这些谈下个 ...

  7. 支持AMD GPU —— 如何运行docker环境下的Jax环境

    相关: 支持NVIDIA GPU -- 如何运行docker环境下的Jax环境 官方给出的安装主页: https://hub.docker.com/r/rocm/jax 安装命令: docker pu ...

  8. 神奇的发现——所有的aarch64架构的CPU平台下的深度学习框架均不原生支持CUDA

    一个记录: 神奇的发型--所有的aarch64架构的CPU平台下的深度学习框架均不原生支持CUDA 不论是mindspore.pytorch.TensorFlow框架只要是aarch64架构的CPU下 ...

  9. SeaTunnel 发布成为 Apache 顶级项目后首个版本 2.3.2,进一步提高 Zeta 引擎稳定性和易用性

    近日,Apache SeaTunnel 正式发布 2.3.2 版本.此时距离上一版本 2.3.1 发布已有两个多月,期间我们收集并根据用户和开发者的反馈,在 2.3.2 版本中对 SeaTunnel ...

  10. EF Core 索引器属性(Indexer property)场景及应用

    EF Core 索引器属性(Indexer property)场景及应用 简介 EF Core 中的索引器属性(Indexer Property)是指通过一个特殊的属性来访问实体类中的数据,而不必明确 ...