axios的cancelToken

2021-04-18 15:01:17

axios的config中提供了一個cancelToken的屬性來取消請求。

用法

用法一

利用CancelToken.source方法,它會返回帶有CancelToken範例和cancel方法的物件。將CancelToken範例賦值給config.cancelToken。在需要取消請求時呼叫cancel方法就會取消帶有與這個cancel相對應的token的所有請求。

var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {//get請求在第二個引數
    cancelToken: source.token
}).catch(function(thrown) {
});
axios.post('/user/12345', {//post請求在第三個引數
    name: 'new name'
}, {
    cancelToken: source.token
});
source.cancel('不想請求了');

用法二

通過範例化一個CancelToken物件,並傳入一個回撥函數,賦值cancel方法。

var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
    cancelToken: new CancelToken((c)=>cancel=c)//範例化一個CancelToken物件,賦值cancel方法
}).catch(function(thrown) {
});
cancel()

第一種用法只是在第二種用法上多包了一層,本質差不多

原理分析

// axios/lib/cancel/CancelToken.js

'use strict';

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

function CancelToken(executor) {
    if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
    }
    /**
    * 定義一個將來能執行取消請求的promise物件,當這個promise的狀態為完成時(fullfilled),
    * 就會觸發取消請求的操作(執行then函數)。而執行resolve就能將promise的狀態置為完成狀態。
    * 這裡把resolve賦值給resolvePromise,就是為了在這個promise外能執行resolve而改變這個promise的狀態
    * 注意這個promise物件被賦值給CancelToken範例的屬性promise,將來定義then函數就是通過這個屬性得到promise
    */
    var resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve;
    });
    /**
    * 將CancelToken範例賦值給token
    * 執行executor函數,將cancel方法傳入executor,
    * cancel方法可呼叫resolvePromise方法,即觸發取消請求的操作
    */
    var token = this;
    executor(function cancel(message) {
        if (token.reason) {
            // 取消已響應 返回
            return;
        }
        token.reason = new Cancel(message);
        // 這裡執行的就是promise的resolve方法,改變狀態
        resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
    if (this.reason) {
        throw this.reason;
    }
};

// 這裡可以看清楚source函數的真面目
CancelToken.source = function source() {
    var cancel;
    var token = new CancelToken(function executor(c) {
        // c 就是CancelToken中給executor傳入的cancel方法
        cancel = c;
    });
    return {
        token: token,
        cancel: cancel
    };
};

module.exports = CancelToken;

CancelToken

CancelToken是一個建構函式,通過new CancelToken()得到的是一個範例物件,建立時它只有一個屬性promise, 它的值是一個能觸發取消請求的Promise物件。

token = new CancelToken(executor function) ===> { promise: Promise物件 }

執行CancelToken函數做了兩件事:

建立一個Promise物件,且將這個物件賦值給promise屬性,其resolve引數被暴露出來以備外部參照。
執行executor函數,將內部定義的cancel函數作為引數傳遞給executor

var token = this;
var cancel = function (message) {
    if (token.reason) {
        // 取消已響應 返回
        return;
    }
    token.reason = new Cancel(message);
    // 這裡執行的就是promise的resolve方法,改變狀態
    resolvePromise(token.reason);
}
executor(cancel);

當執行

let cancel
token = new CancelToken(function executor(c) {
    cancel = c;
});

token值為{promise: Promise物件}
cancel就是CanelToken內部的那個cancel函數。

CancelToken.source

CancelToken.source是一個函數,通過原始碼可以看到,執行const source = CancelToken.source(),得到的是一個物件:

return {
    token: token,
    cancel: cancel
};

包含一個token物件,即CancelToken範例物件,和一個cancel函數。因此CancelToken.source()函數的作用是生成token物件和取得cancel函數。token物件是用於設定給axios請求中的cancelToken屬性,cancel函數是將來觸發取消請求的函數。

config.cancelToken.promise

在cancelToken.promise的回撥中呼叫abort中止請求

if (config.cancelToken) {
    // Handle cancellation
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }
        request.abort();//xmlHttpRequest物件的abort中止請求
        reject(cancel);
        // Clean up request
        request = null;
    });
}

Cancel

Cancel用來儲存cancel方法傳入的取消原因

executor(function cancel(message) {//cancel傳入原因
        if (token.reason) {
            // 取消已響應 返回
            return;
        }
        token.reason = new Cancel(message);token.reason儲存原因
        // 這裡執行的就是promise的resolve方法,改變狀態
        resolvePromise(token.reason);
  });
//Cancel.js
'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;

axios.isCancel()

axios.isCancel來判斷該請求是不是通過cancel手動取消的

'use strict';

module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};

cancelToken用法實戰

問題

一個聊天專案遇到這樣的一個問題,客戶在使用者列表中切換使用者,每次切換都會去獲取使用者的基本資訊和之前的聊天記錄並渲染到頁面上。如果頻繁切換,可能導致頁面要響應很多請求,並且如果前一次切換的請求在最後一次切換請求後面返回還會導致頁面上渲染的資料不對。

思考

雖然可以通過防抖的方式解決,但是每次切換延遲一段時間使用者體驗又不好,所以想到了使用cancelToken的方式來解決。

基本思路

1、每次請求生成一個cancelToken,也可以自己傳入cancelToken。然後根據url和method,將對應的cancel方法存起來
2、當有同樣的請求過來後,通過url和method拿出cancel方法並呼叫,這樣就可以取消前一次還沒返回的相同請求,頁面也不會響應。保證頁面一段時間內只會響應最後一次重複請求。

相關程式碼

//cancelToken.js
// 宣告一個 Map 用於儲存每個請求的標識 和 取消函數
const pending = new Map()
/**
 * 新增請求
 * @param {Object} config 
 */
export const addPending = (config) => {
  const url = [
    config.method,
    config.url
  ].join('&')
  config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
    if (!pending.has(url)) { // 如果 pending 中不存在當前請求,則新增進去
      pending.set(url, cancel)
    }
  })
}
/**
 * 移除請求
 * @param {Object} config 
 */
export const removePending = (config) => {
  const url = [
    config.method,
    config.url
  ].join('&')
  if (pending.has(url)) { // 如果在 pending 中存在當前請求標識,需要取消當前請求,並且移除
    const cancel = pending.get(url)
    cancel(url)
    pending.delete(url)
  }
}
/**
 * 清空 pending 中的請求(在路由跳轉時呼叫)
 */
export const clearPending = () => {
  for (const [url, cancel] of pending) {
    cancel(url)
  }
  pending.clear()
}
import {addPending,removePending} from "./cancelToken"//引入cancelToken
// 建立axios範例
const service = axios.create({
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'X-Requested-With': 'XMLHttpRequest'
  }
})

// request攔截器
service.interceptors.request.use(config => {
  removePending(config) // 在請求開始前,對之前的請求做檢查取消操作
  addPending(config) // 將當前請求新增到 pending 中
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone攔截器
service.interceptors.response.use(
  response => {
    removePending(response) // 在請求結束後,移除本次請求
    Promise.relove(response)
  },
  error => {
    if (axios.isCancel(error)) {//處理手動cancel
      console.log('這是手動cancel的')
    }
    return Promise.reject(error)
  }
)