阅读视图

发现新文章,点击刷新页面。

Vue3 响应式中的 Reactive

请说说你对 Vue3 响应式的理解

Vue 的数据响应式,是当数据变化时,自动执行依赖于它的副作用函数

深入实现一个 Reactive 响应式

通过 effect 函数来监控数据变化,并在数据变化时调用 render 函数自动更新视图。

  1. 首先需要一个依赖追踪系统,用于依赖追踪,并在数据变化时通知相关的副作用函数重新执行。

    class Dep {
      constructor() {
        // 用 Set 来存储订阅的副作用函数,避免重复添加
        this.subscribers = new Set();
      }
      // 添加依赖
      depend() {
        // activeEffect 是当前正在执行的副作用函数
        if (activeEffect) {
          this.subscribers.add(activeEffect);
        }
      }
      // 通知所有依赖更新
      notify() {
        this.subscribers.forEach((effect) => effect.update());
      }
    }
    

    activeEffect 是一个全局变量,用于存储被注册的副作用函数,在下文的执行 effect 函数执行前赋值。

  2. Watcher 实现 用于管理副作用函数的执行,并在数据变化时重新执行这些函数

    // activeEffect 用于存储当前正在执行的副作用函数
    let activeEffect = null;
    class Watcher {
      constructor(effect) { 
        this.effect = effect;
        this.run(); // 初始化时执行一次副作用函数
      }
      // 执行副作用函数
      run() {
        activeEffect = this;
        this.effect();
        activeEffect = null;
      }
      // 数据变化时调用,重新执行副作用函数
      update() {
        this.run();
      }
    }
    
    // 定义一个 effect 函数,用于注册副作用函数
    function effect(fn) {
      new Watcher(fn);
    }
    

    effect 注册副作用函数,通过实例化 Watcher ,将改函数赋值给全局变量 activeEffecteffect 函数的定义,可以不在通过硬编码的格式定义副作用函数,即使是一个匿名函数,也能通过全局变量 activeEffect 来找到并执行。

  3. 响应式数据实现 使用 Proxy 来拦截对对象属性的访问(get)和修改(set),从而实现响应式数据。

    // 实现一个reactive函数,将一个普通对象转换为响应式对象
    // 使用 WeakMap 来存储对象及其属性对应的 Dep
    const targetMap = new WeakMap();
    
    // 获取对象属性对应的 Dep
    function getDep(target, key) {
      let depsMap = targetMap.get(target);
      // 如果没有对应的依赖 Map,则创建一个新的 Map
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }
      let dep = depsMap.get(key);
      // 如果没有对应的 Dep,则创建一个新的 Dep
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
      }
      return dep;
    }
    
    // 创建响应式对象
    function reactive(target) {
      const handler = {
        get(target, key, receiver) {
          // 拿到对应的 Dep
          const dep = getDep(target, key);
          dep.depend(); // 追踪依赖
          return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
          const result = Reflect.set(target, key, value, receiver);
          const dep = getDep(target, key);
          dep.notify(); // 通知依赖更新
          return result;
        },
      };
      return new Proxy(target, handler);
    }
    

    从上述代码可以看出构造数据结构的方式,涉及到了三种数据结构:WeakMapMapSet

    • WeakMaptarget --> Map 构成;
    • Mapkey --> Set 构成;

    其中 WeakMap 的键是原始对象 targetWeakMap 的值是一个 Map 实例,而 Map 的键是原始对象 target 中的 keyMap 的值是一个由副作用函数组成的 Set

    为什么要使用 WeakMap 来创建

    WeakMapkey 是弱引用,它不影响垃圾回收器的工作,一旦它的 key 被垃圾回收器回收了,那么对应的键和值就访问不到了

    为什么要使用 Reflect

    实际上,Reflectgetset 方法还接受第三个参数 Receiver(指定接收者),可以理解为函数调用中的 this,因此在触发 getset 时,能准确的将代理对象作为 this 传递给 getset 方法

    this 由原始对象 target 变成了代理对象,很显然,这会在副作用函数与响应式数据之间建立响应联系,从而达到依赖收集的效果

  4. 更新 dom 视图,需要一个 render函数来更新视图,当响应式数据发生变化时,自动调用 render 函数更新视图

    <div id="app">
      <div id="count-display"></div>
      <button id="increment-btn">Increment</button>
    </div>
    
    function render() {
        document.getElementById(
          "count-display"
        ).innerText = `Count: ${state.count}`;
    }
    
  5. 综合应用

    const state = reactive({
        count: 0,
    });
    // 默认先触发一次,初始化渲染
    effect(() => {
        render();
    });
    // 绑定点击事件,点击按钮时,触发了render方法,页面的数据自动变化
    document.getElementById("increment-btn").addEventListener("click", () => {
        state.count++;
    });
    

PromiseA+规范

promisesaplus.com/

简单的 promise 是什么?(thenable/PromiseLike

  • 对象
  • 有一个 then 方法

但是一个真正的 promise 除去这两点还有一些细化的东西,具体是一个怎么样的对象,then 方法内部是什么处理

function isPromiseLike(value) {
  return (
    value !== null &&
    (typeof value === "object" || typeof value === "function") &&
    typeof value.then === "function"
  );
}

实现 PromiseA+ 规范

  1. 定义工具方法,用于 class 中的内部使用

    • 定义一个检测是否函数的方法
    function isFunction(x) {
      return typeof x === "function";
    }
    
    • 定义一个判断是否 thenablePromiseLike)的方法
    function isThenable(x) {
      // 判断 x 是不是一个对象或者函数,并且 x.then 是一个函数
      return (
        ((typeof x === "object" && x !== null) || isFunction(x)) &&
        isFunction(x.then)
      );
    }
    
  2. 初始化一个 promise 构造函数,包含属性的定义,一个 constructor 构造方法以及 then 方法声明 定义内部变量,包含 statepromise 的状态),data(完成状态下的数据),reason(拒绝状态下的数据),settledhandlers(完成状态下的回调函数)

    class MyPromise {
      // Promise有三种状态
      _state = "pending"; // 初始状态'pending' | 'fulfilled' | 'rejected'
      _data = undefined; // promise 完成状态下的相关数据
      _reason = undefined; // promise 拒绝状态下的相关数据
      _settledhandlers = []; // 当前 promise 完成状态下的回调函数,为什么要定义成一个数组,是因为可能会有多个 then 方法
    
      constructor(executor) {}
    
      then(onFulfilled, onRejected) {}
    }
    

    为什么这些属性没有设置私有

    Promise/A+ 规范只要求状态不可变没要求可见性; ES6 官方 Promise 把 [[PromiseState]] 做成引擎级私有槽,外部无法访问; 自己实现时,用 #stateWeakMap 把它设为私有,是最佳实践而非强制。

  1. 完善构造函数的实现

    // 构造函数接受一个执行器函数 executor
      constructor(executor) {
        // executor 必须是一个函数
        if (!isFunction(executor)) {
          throw new TypeError(`Promise resolver ${executor} is not a function`);
        }
        // 定义实例属性 resolve 和 reject
        const resolve = (data) => {
          resolvePromise(this, data);
        };
        // 这个简单
        const reject = (err) => {
          rejectPromise(this, err);
        };
        try {
          // 执行过程中有错误,会直接抛出错误
          executor(resolve, reject);
        } catch (e) {
          return reject(e);
        }
      }
    

    具体的 resolvereject 需要定义额外的方法去实现

    • 其中,reject 方法考虑的内容较少,实现较为简单(只关注两件事,改变状态和更新 reason

      function rejectPromise(prom, reason) {
        // 当一个 promise 的状态确定后,状态和相关数据就不会再改变了,历史不可倒退
        if (prom._state !== "pending") return; // 状态只能从 pending -> fulfilled/rejected
        // 直接拒绝,修改promise的状态
        prom._state = "rejected";
        prom._reason = reason;
      }
      
    • resolve 方法的实现,判断传入的值是不是一个 promise,要去吸收状态,否则直接改变 promise 的状态

      function resolvePromise(prom, x) {
        // 当一个 promise 的状态确定后,状态和相关数据就不会再改变了,历史不可倒退
        if (prom._state !== "pending") return; // 状态只能从 pending -> fulfilled/rejected
        //   判断 x 是不是一个 promise(有可能是一个 thenable 对象)
        if (isThenable(x)) {
          // 判断 x 和 prom 是不是同一个对象
          if (x === prom) {
            return rejectPromise(
              prom,
              new TypeError("Chaining cycle detected for promise #<Promise>")
            );
          }
          // 不是同一个对象,要去吸收 x 的状态,还要增加一个 microtask 队列
          queueMicrotask(() => {
            x.then(
              (data) => {
                // x 成功了,用 data 去 resolve prom
                resolvePromise(prom, data); // 递归解析
              },
              (err) => {
                // x 失败了,用 err 去 reject prom
                rejectPromise(prom, err);
              }
            );
          });
        } else {
          prom._state = "fulfilled";
          prom._data = x;
        }
      }
      
  2. 实现 then 方法,就处理两个问题,回调什么时候调用,以及 then 方法的返回新的 promise 状态是什么

    then(onFulfilled, onRejected) {
        // then 方法返回一个新的 promise
        const prom = new MyPromise((resolve, reject) => {});
        // 为什么要定义成一个数组,是因为可能会有多个 then 方法
        // 这里的回调函数要等到当前 promise 状态确定后才能调用
        this._settledhandlers.push({
          onFulfilled,
          onRejected,
          prom, // 当前 then 方法返回的 promise
        });
        // 数组里面push的内容什么时候执行呢?当状态确定后执行  通过一个flushHandles函数
        flushHandlers(this);
        return prom;
      }
    

    flushHandlers 方法是依次执行 promise 完成状态下的回调函数,通过 then 方法添加

    // 将curpromise的_settledhandlers数组中的回调函数依次执行  所有回调依次处理
    function flushHandlers(curPromise) {
      if (curPromise._state === "pending") return; // 状态还是pending 直接返回
      // 状态已经确定了 可能是fulfilled 也可能是rejected
      // 让这些回调函数依次执行
      const settledhandlers = curPromise._settledhandlers; //取出所有的回调
      // 将后续的处理放入微任务队列中执行  是将整个放入,不是循环一个一个放入
      queueMicrotask(() => {
        while (settledhandlers.length) {
          // 这个prom 和传入的curPromise不一样,是then方法中new的那个promise
          // 取出第一个回调,需要清空原数组
          const { onFulfilled, onRejected, prom } = settledhandlers.shift();
          // 具体处理回调和prom的状态
          // 判断,要是 onfocusfilled/onRejected 不是函数,就直接用当前promise的值去resolve/reject
          if (!isFunction(onFulfilled) && curPromise._state === "fulfilled") {
            // 状态穿透
            resolvePromise(prom, curPromise._data);
            continue; // 继续下一个
          }
          // 同样的,当前状态为失败,但是不是函数
          if (!isFunction(onRejected) && curPromise._state === "rejected") {
            // 状态穿透
            rejectPromise(prom, curPromise._reason);
            continue; // 继续下一个
          }
          // 走到这里,说明是函数,那就运行其中一个回调,状态结果取决于回调的结果是否报错,因此要try catch
          let result;
          try {
            // 不报错就resolve;
            result =
              curPromise._state === "fulfilled"
                ? onFulfilled(curPromise._data)
                : onRejected(curPromise._reason);
          } catch (error) {
            // 报错就reject
            rejectPromise(prom, error);
            continue; // 继续下一个
          }
          // 走到这里,说明没有报错,用result去resolve
          // 只要返回了一个结果,不管是成功还是失败,都要用这个结果去resolve
          resolvePromise(prom, result);
        }
      });
    }
    

    该方法为处理后续的回调,在状态确定后或者调用 then 方法时执行

    因此需要额外在状态确定时调用一次,涉及到状态调用的地方有 rejectPromiseresolvePromise 两个方法

    function rejectPromise(prom, reason) {
      // 当一个 promise 的状态确定后,状态和相关数据就不会再改变了,历史不可倒退
      if (prom._state !== "pending") return; // 状态只能从 pending -> fulfilled/rejected
      // 直接拒绝,修改promise的状态
      prom._state = "rejected";
      prom._reason = reason;
      // 状态确定后,去执行回调函数
      flushHandlers(prom);
    }
    function resolvePromise(prom, x) {
      // 当一个 promise 的状态确定后,状态和相关数据就不会再改变了,历史不可倒退
      if (prom._state !== "pending") return; // 状态只能从 pending -> fulfilled/rejected
      //   判断 x 是不是一个 promise(有可能是一个 thenable 对象)
      if (isThenable(x)) {
        xxxx.....
      } else {
        prom._state = "fulfilled";
        prom._data = x;
        // 状态确定后,去执行回调函数
        flushHandlers(prom);
      }
    }
    
  3. 调用测试,和 Promise 方法进行对比

    process.on("unhandledRejection", () => {
      console.log("process unhandledRejection");
    });
    const p = new Promise((resolve, reject) => {
      resolve(1);
    });
    const p_my = new MyPromise((resolve, reject) => {
      resolve(1);
    });
    console.log("start", p._state);
    const p1 = p.then(123).then(() => {
      return "p1 fulfilled";
    });
    const p2 = p_my.then(123).then(() => {
      return "p2 fulfilled";
    });
    setTimeout(() => {
      console.log("p=>", p);
      console.log("p_my=>", p_my);
      console.log("p1=>", p1);
      console.log("p2=>", p2);
    }, 0);
    
    // p=> Promise { 1 }
    // p_my=> MyPromise {
    //   _state: 'fulfilled',
    //   _data: 1,
    //   _reason: undefined,
    //   _settledhandlers: []
    // }
    // p1=> Promise { 'p1 fulfilled' }
    // p2=> MyPromise {
    //   _state: 'fulfilled',
    //   _data: 'p2 fulfilled',
    //   _reason: undefined,
    //   _settledhandlers: []
    // }
    

完整写法

// 贴合es6的写法
function isFunction(x) {
  return typeof x === "function";
}
function isThenable(x) {
  // 判断 x 是不是一个对象或者函数,并且 x.then 是一个函数
  return (
    ((typeof x === "object" && x !== null) || isFunction(x)) &&
    isFunction(x.then)
  );
}

// 处理后续的回调  在状态确定后 或者 调用 then 方法执行
// 将curpromise的_settledhandlers数组中的回调函数依次执行  所有回调依次处理
function flushHandlers(curPromise) {
  if (curPromise._state === "pending") return; // 状态还是pending 直接返回
  // 状态已经确定了 可能是fulfilled 也可能是rejected
  // 让这些回调函数依次执行
  const settledhandlers = curPromise._settledhandlers; //取出所有的回调
  // 将后续的处理放入微任务队列中执行  是将整个放入,不是循环一个一个放入
  queueMicrotask(() => {
    while (settledhandlers.length) {
      // 这个prom 和传入的curPromise不一样,是then方法中new的那个promise
      // 取出第一个回调,需要清空原数组
      const { onFulfilled, onRejected, prom } = settledhandlers.shift();
      // 具体处理回调和prom的状态
      // 判断,要是 onfocusfilled/onRejected 不是函数,就直接用当前promise的值去resolve/reject
      if (!isFunction(onFulfilled) && curPromise._state === "fulfilled") {
        // 状态穿透
        resolvePromise(prom, curPromise._data);
        continue; // 继续下一个
      }
      // 同样的,当前状态为失败,但是不是函数
      if (!isFunction(onRejected) && curPromise._state === "rejected") {
        // 状态穿透
        rejectPromise(prom, curPromise._reason);
        continue; // 继续下一个
      }
      // 走到这里,说明是函数,那就运行其中一个回调,状态结果取决于回调的结果是否报错,因此要try catch
      let result;
      try {
        // 不报错就resolve;
        result =
          curPromise._state === "fulfilled"
            ? onFulfilled(curPromise._data)
            : onRejected(curPromise._reason);
      } catch (error) {
        // 报错就reject
        rejectPromise(prom, error);
        continue; // 继续下一个
      }
      // 走到这里,说明没有报错,用result去resolve
      // 只要返回了一个结果,不管是成功还是失败,都要用这个结果去resolve
      resolvePromise(prom, result);
    }
  });
}

function rejectPromise(prom, reason) {
  // 当一个 promise 的状态确定后,状态和相关数据就不会再改变了,历史不可倒退
  if (prom._state !== "pending") return; // 状态只能从 pending -> fulfilled/rejected
  // 直接拒绝,修改promise的状态
  prom._state = "rejected";
  prom._reason = reason;
  // 状态确定后,去执行回调函数
  flushHandlers(prom);
}
function resolvePromise(prom, x) {
  // 当一个 promise 的状态确定后,状态和相关数据就不会再改变了,历史不可倒退
  if (prom._state !== "pending") return; // 状态只能从 pending -> fulfilled/rejected
  //   判断 x 是不是一个 promise(有可能是一个 thenable 对象)
  if (isThenable(x)) {
    // 判断 x 和 prom 是不是同一个对象
    if (x === prom) {
      return rejectPromise(
        prom,
        new TypeError("Chaining cycle detected for promise #<Promise>")
      );
    }
    // 不是同一个对象,要去吸收 x 的状态,还要增加一个 microtask 队列
    queueMicrotask(() => {
      x.then(
        (data) => {
          // x 成功了,用 data 去 resolve prom
          resolvePromise(prom, data); // 递归解析
        },
        (err) => {
          // x 失败了,用 err 去 reject prom
          rejectPromise(prom, err);
        }
      );
    });
  } else {
    prom._state = "fulfilled";
    prom._data = x;
    // 状态确定后,去执行回调函数
    flushHandlers(prom);
  }
}

class MyPromise {
  // Promise有三种状态
  _state = "pending"; // 初始状态'pending' | 'fulfilled' | 'rejected'
  _data = undefined; // promise 完成状态下的相关数据
  _reason = undefined; // promise 拒绝状态下的相关数据
  _settledhandlers = []; // 当前 promise 完成状态下的回调函数

  // 构造函数接受一个执行器函数 executor
  constructor(executor) {
    // executor 必须是一个函数
    if (!isFunction(executor)) {
      throw new TypeError(`Promise resolver ${executor} is not a function`);
    }
    // 定义实例属性
    const resolve = (data) => {
      resolvePromise(this, data);
    };
    // 这个简单
    const reject = (err) => {
      rejectPromise(this, err);
    };
    try {
      // 执行过程中有错误,会直接抛出错误
      executor(resolve, reject);
    } catch (e) {
      return reject(e);
    }
  }

  // 就处理两个问题,回调什么时候调用,以及 then 方法的返回新的 promise 状态是什么
  then(onFulfilled, onRejected) {
    // 这里的回调函数要等到当前 promise 状态确定后才能调用
    const prom = new MyPromise((resolve, reject) => {});
    // 为什么要定义成一个数组,是因为可能会有多个 then 方法
    // 这里的回调函数要等到当前 promise 状态确定后才能调用
    this._settledhandlers.push({
      onFulfilled,
      onRejected,
      prom, // 当前 then 方法返回的 promise
    });
    // 数组里面push的内容什么时候执行呢?当状态确定后执行  通过一个flushHandles函数
    flushHandlers(this);
    return prom;
  }
}
❌