Skip to content

Axios 本身是一个返回值为 promise 的函数

而在这种场景下,中止 promise 的方式有两种:

  1. cancelToken,利用的是 CancelToken,更详细可以说是利用promise状态机
  2. signal,利用的是 AbortController,更详细可以说是利用abort事件监听

虽然在当前较新版本的 axios 中, CancelToken 已经不被推荐使用。

但我们本节依然要重点梳理 CancelToken 的实现机制,以更好的理解 promise 状态机。

关于后者 signal 的相关介绍,实际上是借鉴了 fetch 的中止请求方式,可以参考 AbortController

TIP

关于 promise 的中止,有两个不同的概念:

  1. 封装一个支持中止内部 promise 的函数
  2. 封装一个支持可中止的 promise

笔者尝试了一下,前者即是 axios 中止的相关实现,而后者较难实现,或者无法实现(我们更多的是直接在外部作用域 reject,从而强制改变 promise 状态)。

js
class CancelToken {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new TypeError('executor must be a function.');
    }

    let resolvePromise;
    /*
      通过两次闭包的形式,将resolve的控制权抛给了外部使用者。当用户调用了cancel()方法之后,resolve方法被触发,此时then函数回调触发。
      这里之所以要额外使用Promise链,而不是直接将执行listeners的方法抛出,主要是因为promise.then微任务的执行机制。
    */
    this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
    });
    const token = this;
    this.promise.then(cancel => {
      if (!token._listeners) return;
      let i = token._listeners.length;
      while (i-- > 0) {
        token._listeners[i](cancel);
      }
      token._listeners = null;
    });

    /*
      这一段代码实际是对 config.cancelToken 的版本向下兼容。
      因为原始使用方式是这样的config.cancelToken.promise.then()。
      在现有Axios的源码逻辑,并不会触发此处。会触发上面的then方法
    */
    this.promise.then = onfulfilled => {
      let _resolve;
      const promise = new Promise(resolve => {
        /*
          这里的订阅,笔者认为并没有作用。因为当进行到这步时,外层promise已经是resolved状态了。
          因此,即使在此订阅后,再手动触发 `token.cancel`,也不会执行所有的listeners了。
        */
        token.subscribe(resolve);
        _resolve = resolve;
      }).then(onfulfilled);
      promise.cancel = function reject() {
        token.unsubscribe(_resolve);
      };
      return promise;
    };

    executor(function cancel(message, config, request) {
      if (token.reason) {
        return;
      }
      // 当取消时,会给实例token添加reason属性,也就是说,我们可以通过token.reason是否存在,来判断取消操作是否已经触发。
      token.reason = new CanceledError(message, config, request);
      resolvePromise(token.reason);
    });
  }

  // 如果已经取消了,则直接抛出错误
  throwIfRequested() {
    if (this.reason) {
      throw this.reason;
    }
  }

  // 订阅
  subscribe(listener) {
    if (this.reason) {
      listener(this.reason);
      return;
    }
    if (this._listeners) {
      this._listeners.push(listener);
    } else {
      this._listeners = [listener];
    }
  }

  // 取消订阅
  unsubscribe(listener) {
    if (!this._listeners) {
      return;
    }
    const index = this._listeners.indexOf(listener);
    if (index !== -1) {
      this._listeners.splice(index, 1);
    }
  }

  // 静态方法
  static source() {
    let cancel;
    const token = new CancelToken(function executor(c) {
      cancel = c;
    });
    // 返回token和cancel
    return {
      token,
      cancel
    };
  }
}

从上述代码中,可以看出 CancelToken 利用了订阅/发布设计模式。

但值得一提的是,截止到目前为止,CancelToken 的改动一共涉及了两版:

  1. Adding Cancel and CancelToken classes
  2. Release/v0.22.0

在版本 2 中才修改为了订阅/发布设计模式。在发布记录中,可以看到一部分目的是为了修复内存泄漏,但这个笔者不太确定。

TIP

这部分的订阅/发布模式,是基于 promise.then 的。并不能是同步调用

之所以这样设计,是因为微任务的执行时机和执行机制

譬如 CancelToken 是在 adapter 中调用的,而 adapater 以及 interceptor 都是在 promise.then 链中执行的。