常规对象和异质对象
-
根据 ECMAScript 规范,对象可分为两种对象,常规对象(ordinary object)和异质对象(exotic object), 任何不属于常规对象的对象都是异质对象
-
常规对象的内部方法槽
| 内部方法槽 | 调用的方法 |
|---|---|
| [[GetPrototypeOf]] | getPrototypeOf |
| [[SetPrototypeOf]] | setPrototypeOf |
| [[IsExtensible]] | isExtensible |
| [[PreventExtensions]] | preventExtensions |
| [[GetOwnProperty]] | getOwnPropertyDescriptor |
| [[DefineOwnProperty]] | defineProperty |
| [[HasProperty]] | has |
| [[Get]] | get |
| [[Set]] | set |
| [[Delete]] | deleteProperty |
| [[OwnPropertyKeys]] | ownKeys |
- 函数特有的方法槽, 看这个对象是否是函数时,可以为此作为判断
| 内部方法槽 | 调用的方法 |
|---|---|
| [[Call]] | apply |
| [[Construct]] | construct |
obj.foo;
- 引擎内部会调用[[Get]]内部方法读取属性值。当然其他操作比如 修改、删除都会触发相应的内部方法。
- proxy 的内部方法槽
| 内部方法槽 | 调用的方法 |
|---|---|
| [[GetPrototypeOf]] | getPrototypeOf |
| [[SetPrototypeOf]] | setPrototypeOf |
| [[IsExtensible]] | isExtensible |
| [[PreventExtensions]] | preventExtensions |
| [[GetOwnProperty]] | getOwnPropertyDescriptor |
| [[DefineOwnProperty]] | defineProperty |
| [[HasProperty]] | has |
| [[Get]] | get |
| [[Set]] | set |
| [[Delete]] | deleteProperty |
| [[OwnPropertyKeys]] | ownKeys |
| [[Call]] | apply |
| [[Construct]] | construct |
const obj = new Proxy({ foo: 5 });
obj.foo;
-
实际上,引擎会调用部署在对象 obj 上的内部部方法 [[Get]]。到这一步,其实代理对象和普通对象没有太大区别。它们的区别在于对于内部方方法[[Get]]的实现,这里就体现了内部方法的多态性,即不同的对象部署相同的内部方法,但它我们的行为可能不同。具体的不同体现在,如果在创建代理对象时没有指定对应的拦截函数,例如和没有指定 get()拦截函数,那么当我们通过代理对象访问属性值时,代理对象的内部方法[[Get[]] 会调用原始对象的内部方法 [[Get]] 来获取属性值,这其实就是代理透明性质。
-
数组也是异质对象
reactive 的实现
// 存储副作用函数的桶
const bucket = new WeakMap();
const ITERATE_KEY = Symbol();
function reactive(obj) {
return createReactive(obj);
}
// 浅响应
function shallowReactive(obj) {
return createReactive(obj, true);
}
// 只读reactive
function readonly(obj) {
return createReactive(obj, false, true);
}
// 浅响应只读
function shallowReadonly(obj) {
return createReactive(obj, true, true);
}
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
// 访问原始值
if (key === "raw") {
return target;
}
// 非只读的时候才需要建立响应联系
if (!isReadonly) {
track(target, key);
}
// return target[key] 是原始值,不会触发响应的
// 通过receiver, 也就是代理对象来读取对应的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) {
console.log("set: ", key);
// 只读响应数据,不能设置
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);
}
}
return res;
},
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
ownKeys(target) {
// 判断对象是否有某个属性时, 此操作并不是绑定给某个属性的,可能是任意属性, 所以用ITERATE_KEY 来标识
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
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;
},
});
}
- 上面的 proxy 代理响应数据的操作, 其中 get(), has(), ownKeys() 属获取操作,需要 track ; 其中 set(), deleteProperty()属设置操作,需 trigger
- 判断对象是否有某个属性时, 此操作并不是绑定给某个属性的,可能是任意属性, 所以 用 ITERATE_KEY 来标识
// 收集依赖
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);
}
// 触发响应,执行副作用函数
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((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
// 添加和删除都会影响对象key的变化, 会影响for-in遍历的结果
if (type === "ADD" || type === "DELETE") {
const iterateEffects = depsMap.get(ITERATE_KEY);
iterateEffects && iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
// 数组的添加,删除 会改变length操作,当前数组添加时,获取length对应的副作用函数,进行执行
if (type === "ADD" && Array.isArray(target)) {
const lengthEffects = depsMap.get("length");
lengthEffects && lengthEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
// 交给调度器执行
effectsToRun.forEach((effectFn) => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect;
// effect 栈
const effectStack = [];
function effect(fn, options = {}) {
const effectFn = () => {
// 先清除当前副作用函数所有的依赖项
cleanup(effectFn);
// 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn;
// 在调用副作用函数之前将当前副作用函数压栈
effectStack.push(effectFn);
const res = fn();
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
};
// 将 options 挂在到 effectFn 上
effectFn.options = options;
// activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
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;
}