第五章 非原始值的响应式方案

84 阅读4分钟

常规对象和异质对象

  1. 根据 ECMAScript 规范,对象可分为两种对象,常规对象(ordinary object)和异质对象(exotic object), 任何不属于常规对象的对象都是异质对象

  2. 常规对象的内部方法槽

内部方法槽调用的方法
[[GetPrototypeOf]]getPrototypeOf
[[SetPrototypeOf]]setPrototypeOf
[[IsExtensible]]isExtensible
[[PreventExtensions]]preventExtensions
[[GetOwnProperty]]getOwnPropertyDescriptor
[[DefineOwnProperty]]defineProperty
[[HasProperty]]has
[[Get]]get
[[Set]]set
[[Delete]]deleteProperty
[[OwnPropertyKeys]]ownKeys
  1. 函数特有的方法槽, 看这个对象是否是函数时,可以为此作为判断
内部方法槽调用的方法
[[Call]]apply
[[Construct]]construct
obj.foo;
  • 引擎内部会调用[[Get]]内部方法读取属性值。当然其他操作比如 修改、删除都会触发相应的内部方法。
  1. 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;
    },
  });
}
  1. 上面的 proxy 代理响应数据的操作, 其中 get(), has(), ownKeys() 属获取操作,需要 track ; 其中 set(), deleteProperty()属设置操作,需 trigger
  2. 判断对象是否有某个属性时, 此操作并不是绑定给某个属性的,可能是任意属性, 所以 用 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;
}