Vue.js设计与实现(第六章)— Reflect与深层和浅层reactive

318 阅读12分钟

大家好,本章给的内容是是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的实现过程 image.png 其关键点在于使用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();
  }
}