大家好,本章给的内容是是Reflect与深层和浅层reactive,Reflect与Vue3中的proxy响应式有什么关联,并由此引申出 for...in、delete的响应式触发,还介绍了如何进行合理的触发响应式,以及介绍了reactive和readonly浅响应和深层次响应的实现过程。
Reflect 是一个全局内置独享,其下有许多方法例如:
- Reflect.get(target目标函数, propertyKey需要获取的值的键值 receiver 如果target对象中指定了getter,receiver则为getter调用时的this值。)
- Reflect.set()
- Reflect.apply()
- Reflect下的方法与Proxy的拦截器方法相同,任何能够在Proxy找到的同名方法都可以在Reflect中找到
那么它可以解决什么问题呢? 例如这里有一个对象,我希望可以修改this的指向,从而读取指定对象中的foo,而不是obj中的foo,就可以通过给receiver添加新的this,所以这里就会输出10 而不是5.
const obj = {
foo: 5,
get foo() {
return this.foo;
},
};
console.log(Reflect.get(obj, "foo", { foo: 10 })); // 10
所以这一点就可以运用到现有的响应式系统中,首先查看目前的效果,第一次会读取,但是自增却无法进行实现。
const data = {
foo: 1,
get bar() {
console.log(this, "@this");
return this.foo;
},
};
const obj = new Proxy(data, {...});
effect(() => {
console.log(obj.bar); // 输出1
});
obj.foo++; // 自增无反应
那么借助Reflect就可以实现
const obj = new Proxy(data, {
get(target, key, receiver) {
track(target, key);
// return target[key];
return Reflect.get(target, key, receiver);
},
set(target, key, newVal, receiver) {
const res = Reflect.set(target, key, newVal, receiver);
trigger(target, key);
return res;
},
});
for...in 建立响应式关系
在规范的14.7.5.9中 提供了for..in的实现过程
其关键点在于使用Reflect.ownKeys来获取只属于对象自身拥有的键,所以拦截for..in拦截就可以使用它来实现
const data = {
foo: 1,
bar: 2,
};
const ITERATE_KEY = Symbol();
const p = new Proxy(data, {
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联起来
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
})
这里将ITERATE_KEY唯一值最为Key的原因是,在set/get中,可以得到具体操作的key,但是在ownKeysz中,我们只能拿到目标对象target。因为在读写属性值时,总是能够明确的知道当前操作哪一个属性,所以只需要在该属性与副作用函数之间建联系即可。而ownKeys用来获取一个对象所以属性自己的键值,这个明显不与任何键绑定,因此只能构建唯一key作为表示。 当然在setter中也需要对于的触发才行,将 ITERATE_KEY 与哪些副作用函数相关联并取出来执行
function trigger(target, key, type) {
const depsMap = bucket.get(target);
if (!depsMap) return;
// 取得与key相关联的副作用函数
const effects = depsMap.get(key);
// 新增:取得与 ITERATE_KET 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY);
const effectsToRun = new Set();
// 将与key相关联的副作用函数添加到effectsToRun中
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
// 新增:将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRun
if (type === TriggerType.ADD) {
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
// 调度器
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
console.log(effectFn.options);
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
delete
这里可以通过deleteProperty来进行代理删除
const data = {
foo: 1,
bar: 2,
};
const ITERATE_KEY = Symbol();
const p = new Proxy(data, {
deleteProperty(target, key) {
const hasKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
// 当只有被删除的属性是对象自己并且删除成功时,才触发更新
if (res && hasKey) {
trigger(target, key, TriggerType.DELETE);
}
return res;
},
})
首先检查被删除的属性是否属于对象自身,然 后调用 Reflect.deleteProperty 函数完成属性的删除工作,只有 当这两步的结果都满足条件时,才调用 trigger 函数触发副作用函数 重新执行。需要注意的是,在调用 trigger 函数时,我们传递了新的 操作类型 'DELETE'。由于删除操作会使得对象的键变少,它会影响 for...in 循环的次数,因此当操作类型为 'DELETE' 时,我们也应 该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行
const TriggerType = {
SET: "SET",
ADD: "ADD",
};
function trigger(target, key, type) {
const depsMap = bucket.get(target);
if (!depsMap) return;
// 取得与key相关联的副作用函数
const effects = depsMap.get(key);
// 取得与 ITERATE_KET 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY);
const effectsToRun = new Set();
// 将与key相关联的副作用函数添加到effectsToRun中
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
// 将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRun
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
// 调度器
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
console.log(effectFn.options);
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
优化读取 - for..in
与添加新属性不同,修改属性不会对 for...in 循环产生影响。 因为无论怎么修改一个属性的值,对于 for...in 循环来说都只会循 环一次。所以在这种情况下,我们不需要触发副作用函数重新执行, 否则会造成不必要的性能开销。然而无论是添加新属性,还是修改已 有的属性值。
const data = { foo: 1 }
const obj = new Proxy(data,...)
effect(() => {
for (const key in obj) {
console.log(key);
}
});
// 当修改obj.foo时 会循环两次forin
obj.foo = 2;
在 trigger 函数内就可以通过类型 type 来区分当前的操作类 型,并且只有当操作类型 type 为 'ADD' 时,才会触发与 ITERATE_KEY 相关联的副作用函数重新执行,这样就避免了不必要的 性能损耗:
const data = {
foo: 1,
bar: 2,
};
const p = 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);
// target[key] = newVal;
// 把副作用函数从桶里取出并执行
// 将type 作为第三个参数传递给trigger函数
trigger(target, key, type);
return res;
},
})
function trigger(target, key, type) {
const depsMap = bucket.get(target);
if (!depsMap) return;
// 取得与key相关联的副作用函数
const effects = depsMap.get(key);
// 取得与 ITERATE_KET 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY);
const effectsToRun = new Set();
// 将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRun
if (type === TriggerType.ADD || type === TriggerType.DELETE) {
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
... 其他
}
优化读取-值相等
我们来看要面临的第一个问题,即当值没有发生变化时,应该不需要触发响应才对,例如下方的代码输出了两次相同的值,那么针对这一点 拦截 就可以进行优化
const data = { foo:1 }
const obj = new Proxy(data,....)
effect(() => {
console.log(obj.foo); // 输出两次1
});
obj.foo = 1
在set中比较新智与旧值,只要当不全等的时候才触发响应 这里需要注意NaN的情况,因为 NaN === NaN 为true
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);
// 把副作用函数从桶里取出并执行
// 将type 作为第三个参数传递给trigger函数
// 比较新值与旧值,只要当不全等的时候才触发trigger
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
return res;
},
})
合理的触发响应式-浅响应与深响应
下面这段代码,值可以读取,但是修改后,并没有触发响应式,更新修改后的值。
const obj = reactive({ foo: { bar: 1 } });
effect(() => {
console.log(obj.foo.bar, "@@");
});
obj.foo.bar = 10;
原因是因为这里有多层对象,而目前代理的只是对第一层对象进行了响应式代理,也就是目前的效果是 shallowReactive, 但是还需要实现深层,所以需要给内部的对象也需要进行代理, 为了方便管理,可以创建一个 createReactive 方法 给proxy进行一层包裹
const 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;
}
// 修改:如果是一个对象并且不为空,那么就调用深层reactive
if (typeof res === "object" && res !== null) {
return reactive(res);
}
return res;
},
.. 其他拦截
});
};
// 深层响应式
export const reactive = (obj) => {
return createReactive(obj);
};
// 浅层响应式
export const shallowReactive = (obj) => {
return createReactive(obj, true);
};
合理的触发响应式-浅只读与深只读
针对特殊数据需要进行只读,也是需要进行类似的操作
const createReactive = (obj, isShallow = false, isReadonly = false) => {
return new Proxy(obj, {
// 拦截
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
// 如果是浅响应,则直接返回原始值
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:${key} is readonly`);
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.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
}
return res;
},
});
};
// 只读响应式
export const readonly = (obj) => {
return createReactive(obj, false, true);
};
// 浅层只读响应式
export const shallowReadonly = (obj) => {
return createReactive(obj, true, true);
};
完整代码
export const data = {
foo: 1,
bar: 2,
};
let activeEffect;
const ITERATE_KEY = Symbol();
const effectStack = [];
const bucket = new WeakMap();
const TriggerType = {
SET: "SET",
ADD: "ADD",
DELETE: "DELETE",
};
/**
* 创建浅层或深层响应式
*/
const createReactive = (obj, isShallow = false, isReadonly = 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 isReadonly ? readonly(res) : reactive(res);
}
return res;
},
// 拦截设置操作
set(target, key, newVal, receiver) {
if (isReadonly) {
console.warn(`key:${key} is readonly`);
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);
// target[key] = newVal;
// 把副作用函数从桶里取出并执行
// 将type 作为第三个参数传递给trigger函数
// 比较心智与旧值,只要当不全等的时候才触发响应
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
}
return res;
},
// 拦截for...in操作
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联起来
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
// 删除操作
deleteProperty(target, key) {
if (isReadonly) {
console.warn(`key:${key} is readonly`);
return true;
}
const hasKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hasKey) {
trigger(target, key, TriggerType.DELETE);
}
return res;
},
});
};
// 深层响应式
export const reactive = (obj) => {
return createReactive(obj);
};
// 浅层响应式
export const shallowReactive = (obj) => {
return createReactive(obj, true);
};
// 只读响应式
export const readonly = (obj) => {
return createReactive(obj, false, true);
};
// 浅层只读响应式
export const shallowReadonly = (obj) => {
return createReactive(obj, true, true);
};
/**
* 通过新增代码可以看到,传递给effect函数的fn才是真正的副作用函数,
* 而effectFn是我们包装后的副作用函数,在effectFn中我们将fn的执行结果存储到res中
*/
export function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
// 将fn的执行结果存储到res中 新增
const res = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
//将res作为effectFn的返回值 新增
return res;
};
effectFn.deps = [];
// 将 options 挂载到 effectFn 上
effectFn.options = options;
// 只有非lazy的时候,才执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn; //新增
}
export const obj = new Proxy(data, {
get(target, key, receiver) {
track(target, key);
// return 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);
// target[key] = newVal;
// 把副作用函数从桶里取出并执行
// 将type 作为第三个参数传递给trigger函数
// 比较心智与旧值,只要当不全等的时候才触发响应
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type);
}
return res;
},
// 拦截for...in操作
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联起来
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
// 删除操作
deleteProperty(target, key) {
const hasKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hasKey) {
trigger(target, key, TriggerType.DELETE);
}
return res;
},
// // 拦截has操作
// has(target, key) {
// // 将副作用函数与 ITERATE_KEY 关联起来
// track(target, ITERATE_KEY);
// return Reflect.has(target, key);
// },
});
function track(target, key) {
if (!activeEffect) return target[key];
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);
}
function trigger(target, key, type) {
const depsMap = bucket.get(target);
if (!depsMap) return;
// 取得与key相关联的副作用函数
const effects = depsMap.get(key);
// 取得与 ITERATE_KET 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY);
const effectsToRun = new Set();
// 将与key相关联的副作用函数添加到effectsToRun中
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
// 将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRun
if (type === TriggerType.ADD || type === TriggerType.DELETE) {
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
// 调度器
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
console.log(effectFn.options);
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
function cleanup(fn) {
fn && fn.deps.forEach((dep) => dep.delete(fn));
fn.deps.length = 0;
}
/**
* 新增 scheduler 调度器
* 通过set 将任务添加到调度器任务队列中自动去重
*/
// 调度器任务队列
export const jobQueue = new Set();
const p = Promise.resolve();
let isFlushing = false;
export function flushJob() {
if (isFlushing) return;
isFlushing = true;
p.then(() => {
jobQueue.forEach((job) => job());
}).finally(() => {
isFlushing = false;
// jobQueue.length = 0;
});
}
/**
* 实现计算属性
*/
export function computed(getter) {
// 用来缓存上一次计算的值
let value;
// 用来表示是否需要重新计算值,true代表需要重新计算
let dirty = true;
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中奖dirty重置为true
scheduler() {
dirty = true;
// 当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应式
trigger(obj, "value");
},
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
// 计算完之后,将dirty设置为false,下次访问直接使用缓存到value中的值
dirty = false;
}
// 当读取 value 时,手动调用 track 函数进行追踪
track(obj, "value");
return value;
},
};
return obj;
}
/**
* 增加响应式数据读取的通用性
*/
export function traverse(value, seen = new Set()) {
// 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== "object" || value === null || seen.has(value)) return;
// 将数据添加到seen 中 代表遍历的读取过了, 避免循环引用引起的死循环
seen.add(value);
// 暂时不考虑数据等其他结构
// 假设value就是一个对象,使用for..in 读取对象的每一个值,并递归的调用 traverse
for (const key in value) {
traverse(value[key], seen);
}
return value;
}
export function watch(source, cb, options = {}) {
// 定义getter
let getter;
// 如果source时函数,说明用户传递的是getter,所以执行吧source赋值getter
if (typeof source === "function") {
if (typeof source() === "object") {
// 如果是对象,则使用traverse进行递归
getter = () => traverse(source());
} else {
getter = source;
}
} else {
// 否则按照原来的实现调用 traverse 递归的读取
getter = () => traverse(source);
}
// 定义新旧值
let oldValue, newValue;
// 提取 scheduler 调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行辅佐同函数,得到的是新值
newValue = effectFn();
// 将旧值和新值作为回调函数的参数
cb(newValue, oldValue);
// 更新旧值,不然下次会得到错误的旧值
oldValue = newValue;
};
console.log(oldValue, "@oldValue");
// 使用effect注册副作用函数时,开启lazy选项,并吧返回值存储到effectFn中 以便后续手动调用
// 执行响应式
const effectFn = effect(
// 执行 getter
() => getter(),
{
lazy: true,
// 执行调度器触发 trigger
scheduler: () => {
if (options.flush === "post") {
const p = Promise.resolve();
p.then(job);
} else {
job();
}
},
}
);
if (options.immediate) {
// 如果是立即执行,则手动调用一次job函数
job();
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn();
}
}