JesseZhao1990 / blog

learing summary
MIT License
62 stars 7 forks source link

关于取消请求的探讨 #163

Open JesseZhao1990 opened 5 years ago

JesseZhao1990 commented 5 years ago

背景

在公司的一个项目中,最近频繁出现一个bug,点餐的过程中,不同桌台之间的数据老是串数据,最后发现是由于服务员在不同桌台之间快速切换导致的。如果网络环境不好,前几个桌台的订单数据还没回来,后几个桌台的数据已经发出,并且在前端,订单数据是封装成了一个OrderService进行管理的。也就是说前端只保存一份订单的数据。解决这个问题的方案有很多。但是改动最小又最友好的方式,应该是属于“取消请求”的方案,从A桌台切到B桌台的时候,把A桌台的请求取消。axios自带取消请求的功能,但是由于此项目使用的是angularjs,请求的service是自己封装的。所以,只能自己动手改造一下请求的service了。在改动之前,扒拉了一下axios的源码,看看axios是怎么实现的"取消请求"的。

axios如何使用取消请求

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

首先通过执行source函数创建一个source, 然后把source.token 通过参数的配置传给axios。最后执行source上的cancel方法。

axios相关源码分析

相关的源码在这里:https://github.com/axios/axios/tree/503418718f669fcc674719fd862b355605d7b41f/lib/cancel

image

既然首先是通过CancelToken上的source函数创建一个source。那我们首先就分析一下CancelToken是什么鬼?

源码如下:

'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;

可以看到CancelToken是一个构造函数,这个构造函数上挂载一个静态方法source。我们一个个的分析。 首先分析CancelToken这个构造函数,删繁就简,省略了不太重要的判断之后,这个函数如下,可以看到这个构造函数。

  1. 在this上绑定一个promise,并且把这个promise触发状态完成的resolve方法给了变量resolvePromise。
  2. 这个构造函数接收一个函数executor, 在实例化的时候执行executor。
  3. executor也接受一个函数,这个函数叫cancel,cancel这个函数的作用就是执行resolvePromise,让内部的这个promise变成完成态。
function CancelToken(executor) {
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(cancel);

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

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

然后我们再看一下source方法。 source方法通过new CancelToken(executor) 创建一个实例 token。并返回一个对象。这个对象上挂账两个属性,一个是token,一个是cancel。 这个cancel是token内部的那个cancel,可以触发token内部的那个promise变成完成态。

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken( executor);

function executor(c) {
    cancel = c;
  }

  return {
    token: token,
    cancel: cancel
  };
};

因此,到这里我们也基本上能猜到axios是怎么取消一个请求的。通过source工厂函数,返回一个token和token内部的cancel,调用cancel的时候能触发token内部的promise变成完成态。当token内部的promise是完成态的时候,此时触发请求的完成,不管结果回来还是没回来。结果如我们预期的一样。看下面的xhr相关的代码

https://github.com/axios/axios/blob/503418718f669fcc674719fd862b355605d7b41f/lib/adapters/xhr.js

image

此时当参数配置中传入config.cancelToken的话,cancelToken也就是我们生成的token,我们利用cancelToken上的promise,调用cancelToken上的promise的then方法,给此promise放入一个注册函数。此函数执行的时候会调用reject触发这个请求变成失败的完成态。

总结

利用一个工厂函数创建一个对象,对象上挂载着promise,和让这个promise变成完成态的cancel方法。由于这里面有很多promise,为了不混淆,我们暂且叫这个promise为p1。 p1将来会通过参数配置的方式传入到另一个promise内部。这个promise我们称为p2. p2 是封装的ajax的promise。如果p2成fullfilled状态或者是rejected状态。这个请求就会结束。所以,我们从外部执行cancel方法。导致p1变成完成态,进而触发p2变成rejected状态。来结束这个请求。即取消请求。

ok。that‘s all。其实,这么一分析还是非常简单的。我们可以利用这个思路,去给自己封装的请求函数提供取消请求的功能。