浅显易懂的Axios源码解析并封装CancelToken类

1,360 阅读5分钟

前言

axios是我们用的最多的请求库,不仅可以在浏览器端用,甚至还能在服务端用。下面让我们来探究一下它的源码

基本使用

// axios.create
const instance = axios.create({
  baseURL: import.meta.env.VITE_URL
})

下面让我们看看axios.create内部到底做了什么事!

// lib/axios.js
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig); // 创建了axios上下文
  var instance = bind(Axios.prototype.request, context); // 创建实例

  // 复制axios的原型到实例中
  utils.extend(instance, Axios.prototype, context);

  // 复制上下文到实例中
  utils.extend(instance, context);
  
  // 返回实例
  return instance;
}

可以看到instanceConfig就是我们传入的配置,它在内部与默认配置混合,并传给createInstance

然后instance上就有这么些属性了

微信图片_20210618140611.png

拦截器

  • 使用方式
// 请求拦截器
instance.interceptors.request.use(config => {
  // 在这里可以对config进行操作
  return config
})

// 响应拦截器
instance.interceptors.response.use((resp) => {
  if(resp.data.status === 200) {
    // 接口请求成功
    return resp.data.data
  }
  return Promise.reject('')
})

以上就是拦截器的基本使用,相信大家应该都懂

下面让我们来看看他的原理

在use的时候会把所有拦截器收集起来,等需要的时候调用

// lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}
/**
 * 往堆栈里面加入拦截器
 * @param {Function} fulfilled promise为then下的处理函数
 * @param {Function} rejected promise为catch下的处理函数
 *
 * @return {Number} 返回的是拦截器的id,可以用于移除这个拦截器
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
};

请求

接下来我们就可以使用 axios 进行请求了

  instance.get || instance.post || instance.request

不管是哪种请求方式,本质上都是调用了request方法

我们要看的是在发送请求的时候,内部做了什么事情

// lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // 只保留核心逻辑代码

  // 分别把请求拦截器和响应拦截器,以从后往前的顺序放入对应的数组中
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 保留一开始的对象
  var newConfig = config;
  // 把上面保存的请求拦截器数组拿出来 依次执行,得到一个最终的config对象
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break;
    }
  }

  // 请求拦截器执行结束, 下面发起请求
  try {
    promise = dispatchRequest(newConfig); // 返回响应后的包装结果,
  } catch (error) {
    return Promise.reject(error);
  }

  // 下面开始执行响应拦截器
  while (responseInterceptorChain.length) {
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); // 把resolve和reject的响应拦截器放进来
  }
  // 返回最终的promise,.then的数据就是我们调用完请求的返回的那一个对象
  return promise;
}

取消请求之cancelToken

按照惯例我们来看一下基本使用

  instance.interceptors.request.use(config => {
    config.cancelToken = new axios.CancelToken((cancel) => {
      setTimeout(() => {
        cancel() // 取消请求
      }, 10)
    })
    return config
})

可以看到,调用cancel就可以取消掉本次请求了,是不是很神奇,想不想知道它是怎么做到的!!下面让我们来看看

// lib/cancel/CancelToken.js
/**
 * @class
 * @param {Function} executor 执行器,是一个函数
 */
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  // 核心代码,可以看到他把resolve交给了这个变量,也就是把resolve交给这个变量来控制
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // 这个cancel函数就是 new axios.CancelToken((cancel) => {}) 的那个cancel参数
  // 可以看到,我们调用cancel的时候它才会去resolve,完全把resolve的控制权限交给了我们
  const cancel = function cancel(message) {
    if (token.reason) {
      // 如果该次请求已经被取消了,返回
      return;
    }
    token.reason = message; 
    resolvePromise(token.reason);
  }
  executor(cancel);
}

但是,我们可以看到,这里并没有看到取消请求的任何操作,那么他到底在哪里取消请求的呢?待着这个疑问,我用cancelToken这个关键字搜索全局

终于找到了答案,在lib/adapters/xhr.js 312行 和 lib/adpaters/http.js 168行

xhr是axios在浏览器端请求的封装方法,http是在服务端的

if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
      if (req.aborted) return;
      req.abort();
      reject(cancel);
    });
  }

then是在这里处理的,也就是我们调用cencel的时候,就会执行这个微任务,把这个请求取消掉了。

至此,我们常用的axios流程源码已经看完了。当然,axios的源码可不只这么点,这只是皮毛,还有很多东西等待你们去发现!!

下面让我们来封装一个通过的CancelToken类,基于TS,当然JS也可以,把类型去掉就行

封装CancelToken


import axios, { AxiosRequestConfig, Canceler } from 'axios'

export default class CancelToken {
  // 声明一个 Map 用于存储每个请求的标识 和 取消函数
  private static pending: Map<string, Canceler> = new Map()
  // 白名单, 写入接口名称
  private static whiteRequest: string[] = []

  /**
   * 得到该格式的url
   * @param {AxiosRequestConfig} config 
   * @returns 
  */
  private static getUrl(config: AxiosRequestConfig) {
    return [config.method, config.url].join('&')
  }

  /**
   * 添加请求
   * @param {AxiosRequestConfig} config
  */
  public static addPending(config: AxiosRequestConfig) {
    const url = this.getUrl(config)
    config.cancelToken = new axios.CancelToken(cancel => {
      if (!this.pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
        this.pending.set(url, cancel)
      }
    })
  }

  /**
   * 移除请求
   * @param {AxiosRequestConfig} config
  */
  public static removePending(config: AxiosRequestConfig) {
    const url = this.getUrl(config)
    const method = url.split('&')[1]
    if (this.pending.has(url) && !this.whiteRequest.includes(method)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
      const cancel = this.pending.get(url)
      cancel!(url)
      this.pending.delete(url)
    }
  }
  /**
   * 清空 pending 中的请求(在路由跳转时调用)
  */
  public static clearPending() {
    for (const [url, cancel] of this.pending) {
      cancel(url)
    }
    this.pending.clear()
  }
}

下面是使用方式

instance.interceptors.request.use(config => {
  // 请求开始前,检查一下是否已经有该请求了,有则取消掉该请求
  CancelToken.removePending(config)
  // 把当前请求添加进去
  CancelToken.addPending(config)
  return config
})

instance.interceptors.response.use((resp: AxiosResponse<IResponse>) => {
  // 接口响应之后把这次请求清除
  CancelToken.removePending(resp.config)
  if(resp.data.status === 200) {
    // 接口请求成功
    return resp.data.data
  }
  return Promise.reject('')
})

// router.ts
router.beforeEach((to, from, next) => {
  // 路由跳转要清除之前所有的请求缓存
  CancelToken.clearPending()
  next()
})

结语

感谢你们看到这里,希望你们能从这篇文章学到东西。

有错误或疑问欢迎评论区交流,再见!