axios如何撤销请求?

675 阅读6分钟

axios是一款优秀的HTTP请求库,它基于Promise。我们知道Promise一旦开始便不能结束,状态只能从pending -> fulfilled或pending -> rejected,那它是如何做到撤销请求呢??下面为使用axios撤销请求的两种方式。

  1. 可以通过CancelToken.source工厂方法创建cancel token;
var CancelToken = axios.CancelToken;
var source = CancelToken.source();

axios.get('/user/12345', {
    cancelToken: source.token
}).catch((err) => {
    if(axios.isCancel(err)) {
        console.log('Request canceled', err.message);
    } else {
        // 处理错误
    }
});

// 取消请求(message参数是可选的)
source.cancel('Operation canceled by the user.');
  1. 可以通过传递一个executor函数到CancelToken的构造函数来常见cancel token;
var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/1234', {
    cancelToken: new CancelToken((c) => {
        // executor函数接受一个cancel函数作为参数
        cancel = c;
    });
});

上述两种方式的本质是一样的,无非是CancelToken提供的source方法将第二种方式给封装了。 下图为axios源码的结构图,其中用红线标出来的是axios做到撤销请求的核心代码。让我们一步一步地来揭开神秘的背后。

  1. Cancel.js Cancel.js的代码非常简单,定义了一个构造函数,在原型上添加了一个方法toString,用于打印一些消息,以及在原型上添加了一个属性__CANCEL__,并将它的值设置为true。
'use strict';

/**
 * A `Cancel` is an object that is thrown when an operation is canceled.
 *
 * @class
 * @param {string=} message The message.
 */
function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

module.exports = Cancel;
  1. isCancel.js 用于判断该请求是否是已撤销的请求。
'use strict';

module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};
  1. CancelToken.js CancelToken是一个构造函数,它接受一个参数executor,且该参数得是function类型,否则抛出异常。接下来,给cancelToken构造出来的对象添加promise属性(注意:这是撤销请求的关键桥梁)。最后,执行executor函数,cancel函数首先判断请求是否是早就已撤销,然后给token添加一个reason属性,且它的值为Cancel的实例,最后,调用resolvePromise,将this.promise的状态从pending -> fulfilled。 throwIfRequested为在CancelToken原型上添加的方法,判断当前的对象上是否有reason属性,有则抛出异常。 source为在CancelToken函数上添加的方法(函数也是一个对象),它返回一个对象,具有token和cancel属性。其中,token为new CancelToken的示例,cancel为executor函数的参数c。这个参数c有什么魔力呢?
'use strict';

var Cancel = require('./Cancel');

/**
 * A `CancelToken` is an object that can be used to request cancellation of an operation.
 *
 * @class
 * @param {Function} executor The executor function.
 */
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

/**
 * Throws a `Cancel` if cancellation has been requested.
 */
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

代码大部分已经贴出来,下面将结合一个例子来将整个流程给梳理出来。作者使用create-react-app构建一个简单的react项目,有两个button,分别为发送请求和撤销请求。点击发送请求后,后端的需要处理5s才能给出响应;点击撤销请求,请求被撤销。

首先,我们点击发送请求按钮,执行如下代码。

const sendRequest = () => {
    axios.get('/api', {
      cancelToken: new CancelToken(function(c) {
        cancel = c;
      })
    }).then((res) => {
      console.log('res: ', res);
    })
 };

源码首先使用默认创建的实例,调用get方法。get方法其实调用的是request方法,在request方法体中,通过调用dispatchRequest方法来发送请求。

// axios.js
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

module.exports = axios;

// Axios.js
utils.forEach(['delete', 'get', 'head', 'options'], 
    function forEachMethodNoData(method) {
      /*eslint func-names:0*/
      Axios.prototype[method] = function(url, config) {
        // this.request调用Axios.prototype.request方法
        return this.request(mergeConfig(config || {}, {
          method: method,
          url: url,
          data: (config || {}).data
        }));
     };
});

var dispatchRequest = require('./dispatchRequest');
/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
    // 省略
    
    // Hook up interceptors middleware
    var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);
    
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
};

看下dispatchRequest.js的源码,看看它做了些什么事情。变量adapter为defaults.js下的默认adapter,defaults.js使用getDefaultAdapter来获取默认的adapter,getDefaultAdapter根据不同的环境拿到不同的对象来发送请求。(在本例中为XMLHttpRequest)

// dispatchRequest.js
'use strict';

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');


/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
module.exports = function dispatchRequest(config) {
  // 省略
  
  var adapter = config.adapter || defaults.adapter;
  
  // 拿到当前环境下的请求对象
  return adapter(config).then(function onAdapterResolution(response) {

    // 省略
  }, function onAdapterRejection(reason) {
    
    // 省略
  });
};

// defaults.js
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' 
      && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

接下来看下./adapters/xhr.js文件中的源码。它里面的大部分源码挺中规中矩的,首先new XMLHttpRequest对象,然后在事件上绑定回调函数。但是有一段关于cancelToken的代码,我们在这看到了request.abort(),这个方法用于撤销请求。

if (config.cancelToken) {
   // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

撤销请求的源码已经全部列出来了。刚刚说到了点击发送请求的按钮,服务端得处理5s才能给出响应,在5s内,点击撤销请求的按钮。

const abortRequest = () => {
    cancel();
};

cancel方法本质上调用的是CancelToken.js下的executor方法的参数cancel。它调用resolvePromise,这样一来,this.promise的状态从pending -> fulfilled。

// CancelToken.js

var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
});
  
executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
 });

既然,this.promise的状态发生了改变,相应某块的地方的then也该执行。在xhr.js中,执行request.abort方法,触发request.onabort的回调,调用reject(createError('Request aborted', config, 'ECONNABORTED', request))。

// xhr.js

// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
  if (!request) {
    return;
  }
  
  console.log('aborted...');
  reject(createError('Request aborted', config, 'ECONNABORTED', request));

  // Clean up request
  request = null;
};

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    // 由于在onabort回调中调用了reject方法,所以不会再改变状态,下面的reject可忽略
    reject(cancel);
    // Clean up request
    request = null;
  });
}

reject方法调用后,某个promise示例发生了状态变化,相应的回调函数也该执行。执行如下代码then函数的第二个参数。isCancel判断reason是否为Cancel的实例,在上面分析中可以发现reason为createError创建出来的error,所以!isCancel为true,调用throwIfCancellationRequested方法。它调用cancelToken的throwIfRequested方法,抛出异常。

// dispatchRequest.js

'use strict';

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');

/**
 * Throws a `Cancel` if cancellation has been requested.
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  
  // 省略一些代码...

  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {

    // 省略
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }
    
    return Promise.reject(reason);
  });
};
  1. 流程图

image.png