JS axios cancelToken 是如何實現取消請求?稍有啰嗦但超有耐心的 axios 源碼分析


壹 ❀ 引

axios,一個基於promise且對ajax進行了二次封裝的http庫,在提供了與promise類似的API便捷寫法同時,它還有一大特點,便是支持取消http請求。當然取消請求並不是axios獨有特性,它也只是對於XMLHttpRequest.abort()進行了內部封裝。

我在如何做好一個基礎的搜索功能?記一個因客戶大數據量而導致的后發先至Bug一文中,對於文章問題給出的解決方案就有涉及到取消請求。雖然原生做法也能達到目的,但實際開發中我們不會用那么麻煩的寫法,自然會借助三方http庫,比如axios,而在這個庫中,我們若需要取消請求,就得使用它的cancelToken,那么這個cancelToken到底是怎么做的呢?為什么一個簡單的source.cancel調用請求就不會發起了?出於好奇,我簡單閱讀了axios對於cancelToken實現的相關源碼,那么在這篇文章做個簡單記錄。

貳 ❀ 一個取消請求的例子

了解事情原貌最簡單的做法就是寫一個請求取消的例子,然后斷點到axios中去閱讀源碼,因此我在本地項目准備了一個這樣的例子,而本地項目請求接口容易跨域,為了解決這個問題,才有了React axios 使用 http-proxy-middleware 解決跨域問題小記這篇文章。所以這里我們就不多贅述,直接結合axios實現請求取消:

// 引用CancelToken
const CancelToken = axios.CancelToken;
// 調用CancelToken.source得到一個source實例,此實例包含token和cancel兩個屬性
const source = CancelToken.source();
// 請求接口時附帶cancelToken:source.token,get與post有所區別,具體查看官方文檔
axios.get('api/request', { cancelToken: source.token })
  .catch(function (thrown) {
    if (axios.isCancel(thrown)) {
      alert(`Request canceled.${thrown.message}`);
    }
  });
// 通過source.cancel取消請求
source.cancel('Operation canceled by the user.');

上述代碼中我們實現了一個簡單接口取消請求,運行項目並打開控制台,發現並沒發起request請求,且alert正常彈出。

而實現取消接口請求也比較簡單,通過CancelToken.source()創建一個resoure實例:

// 單純創建一個實例,不用於請求,讓我們查看它
const source = CancelToken.source();
console.dir(source)

如圖,此實例包含一個名為cancel的方法,接受一個message字段,也就你要取消請求時需要傳遞的理由,當然也可以不傳。另外是一個token對象,它包含一個狀態為pendingPromise對象(這個東西作用超級大,下文會解釋這個Promise從哪來有什么用)。

取消請求的目的雖然達到了,可這幾個方法像個黑盒,它里面到底發生了什么?沒關系,讓我們順着上面請求取消的例子來一探究竟!

叄 ❀ cancelToken源碼淺析

取消接口第一步創建resouce實例,它的源碼在node_modules/axios/lib/cancel/CancelToken.js中可查看,代碼如下:

/**
 * 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
  };
};

當我們執行CancelToken.source()觸發的就是上述代碼,首先,我們可以看到此方法確實返回一個包含canceltoken兩個屬性的對象,讓我們來看看執行過程。

首先,此方法創建了一個calcel變量,緊接着,又創建了一個token,而token的賦值結果是調用構造函數CancelToken得到,注意,此時我們在調用構造函數時,傳遞了一個function如下:

function executor(c) {
    cancel = c;
}

你可以先不用思考它有什么用,就把這個函數理解成調用構造函數傳遞了一個實參,緊接着返回了一個包含上述兩個屬性的對象,對於source方法,它做的事情就這么簡單,讓我們緊接着看看構造函數CancelToken,代碼如下:

/**
 * 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.');
  }
	// 其實就外層保存resolve方法
  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);
    // 在這里調用resolve方法,用於改變Promise狀態
    resolvePromise(token.reason);
  });
}

OK,對於構造函數CancelToken,它接受一個參數executor,而這個參數其實就是上面提到的executor方法。接着,函數內聲明了一個名為resolvePromise的變量。然后就是構造器屬性this.promise,既然是構造器屬性,那么這里可以預先知道,source方法中得到的token上一定有這個promise屬性(上面截圖已經展示了token中有個Promise),而這個屬性又由一個Promise構造器創建。

new Promise方法做的事情很簡單,在內部將resolve賦值給外部變量resolvePromise,我們都知道Promise接受一個callback,而callback內部可以使用resolvereject兩個方法,而上述所做的事僅僅是將Promise內部的resolve方法暴露出去,以達到在外層改變Promise狀態的目的,比如:

// 外層定義一個變量用於保存promise內部的方法
let resolvePromise;
const p = new Promise((resolve, reject) => {
  resolvePromise = resolve;
})
p.then((res) => {
  console.log(res);// 我在外層改變promise狀態
});
setTimeout(() => {
  // 外層調用promise的resolve方法以達到改變promise狀態目的
  resolvePromise('我在外層改變promise狀態')
}, 3000)

所以簡單來說,通過這種做法我們將promise內部方法暴露出來,想在哪用就在哪用,大概如此。

讓我們繼續回到CancelToken方法,接着我們將this賦予給變量token,不要詫異,我們知道當new一個構造函數時,其實就是在隱式的給this賦值,然后返回這個this,而為了讓這種綁定與返回更為可視化,常常有如下的寫法:

function Fn(name){
  const that = this;
  that.name = name;
  return that;
}

所以上面算是一種可視化做法,畢竟new CancelToken本身就是返回一個token並賦值給token,也相當於更好理解。

接着,我們執行了executor方法,它的參數又是一個函數,如下:

function cancel(message) {
  if (token.reason) {
    // Cancellation has already been requested
    return;
  }
  token.reason = new Cancel(message);
  // 在這里調用resolve方法,用於改變Promise狀態
  resolvePromise(token.reason);
}

同學們,還記得這個executor從哪來嗎?它不就是new CancelToken時傳遞的函數嗎:

function executor(c) {
    cancel = c;
}

所以這個c其實就是上面傳遞給executorcancel方法,而cancel = c這一句,目的跟上面resolve賦值給外層一樣,也是將定義在CancelToken內部的cancel暴露出去。

你可能有點混亂了,其實這里一共做了三次方法暴露,第一次我們將resolve暴露出去,目的是讓cancel中可以通過resolvePromise來改變Promise狀態;而這個cancel也不是在定義的地方使用,這里又做了第二次暴露,將cancel拋出去賦予給了source方法,緊接着source做了第三次方法暴露,當我們開發者調用source將得到canceltoken兩個屬性。大致流程如下圖:

那么到這里我們解釋了source方法是如何產生的這兩個屬性,以及這兩個屬性分別有什么用。

所以當我們調用接口時,傳遞了一個source.token其實就是給這次請求綁定一個狀態是pendingPromise,它更像是一個開發,一旦我們調用source.cancel就會啟動這個開發,告知axios要取消這個請求,怎么取消的呢?

讓我們找到node_modules/axios/lib/adapters/xhr.js這個文件,文章開頭就說了axios是對於ajax的封裝,而ajax又是對於XMLHttpRequest那一套的封裝,所以回歸本質,在xhr.js中我們可以看到發起請求所有的准備代碼都在這里,這里大致貼一點代碼並補了注釋,我刪掉了部分對於本身意義不大的代碼:

return new Promise(function dispatchXhrRequest(resolve, reject) {
  // 准備請求附帶數據
  var requestData = config.data;
  // 准備請求頭
  var requestHeaders = config.headers;
  // 創建xhr對象
  var request = new XMLHttpRequest();

  // 授權相關
  if (config.auth) {
    var username = config.auth.username || '';
    var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
    requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
  }
  // 獲取請求地址
  var fullPath = buildFullPath(config.baseURL, config.url);
  // 調用open,發起請求
  request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

  // 設置超時時間,你期望多久沒反應就提示超時那就設置多少
  request.timeout = config.timeout;

  // OK,監聽state狀態,
  request.onreadystatechange = function handleLoad() {
    if (!request || request.readyState !== 4) {
      return;
    }
    // 准備response數據體
    var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
    var response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config: config,
      request: request
    };

    settle(resolve, reject, response);
    // 一旦請求結束,清空請求
    request = null;
  };

  // 監聽abort的處理
  request.onabort = function handleAbort() {
  };

  // 監聽error的處理
  request.onerror = function handleError() {
  };

  // 監聽超時后的處理
  request.ontimeout = function handleTimeout() {
  };

  // 這里就是看我們有沒有給請求附帶cancelToken,如果帶了就會走這里
  if (config.cancelToken) {
    // 還記得這里的promise知道是哪創建的嗎?
    config.cancelToken.promise.then(function onCanceled(cancel) {
      // 兩種情況,要么此時沒請求,要么請求結束被清空為null,這里就不做處理,直接返回
      if (!request) {
        return;
      }
      // 這里調用了XMLHttpRequest.abort()
      request.abort();
      // 這里修改的是請求體自身promise的狀態,而不是我們上文提到的那個promise
      reject(cancel);
      // 清空request,請求對象都不要了
      request = null;
    });
  }
});

大家可以大致看看上述代碼,這里我們抽離出最重要的一段:

// 這里就是看我們有沒有給請求附帶cancelToken,如果帶了就會走這里
if (config.cancelToken) {
  // 還記得這里的promise知道是哪創建的嗎?
  config.cancelToken.promise.then(function onCanceled(cancel) {
    // 兩種情況,要么此時沒請求,要么請求結束被清空為null,這里就不做處理,直接返回
    if (!request) {
      return;
    }
    // 這里調用了XMLHttpRequest.abort()
    request.abort();
    // 這里修改的是請求體自身promise的狀態,而不是我們上文提到的那個promise
    reject(cancel);
    // 清空request,請求對象都不要了
    request = null;
  });
}

config不用多說,其實就是我們使用axios發起請求時的配置,上文的例子也展示了,你要想取消請求,你就得給請求配置中加一個{ cancelToken: source.token },而這個token中其實就是一個狀態為pendingPromise,目的是什么我們也很清楚了。這個Promise就為了是跟當前請求進行綁定,它就像一顆遙控炸彈,而炸彈的開關就是 source.cancel,當我們調用cancel時,就會執行下面這個方法:

function cancel(message) {
  if (token.reason) {
    // Cancellation has already been requested
    return;
  }
  token.reason = new Cancel(message);
  // 啟動炸彈
  resolvePromise(token.reason);
}

方法里就干一件事,將跟請求綁定在一起的Promise的狀態給改成resolve,一旦修改完成,那是不是就得跑下面這段代碼:

config.cancelToken.promise.then(function onCanceled(cancel) {
  // 兩種情況,要么此時沒請求,要么請求結束被清空為null,這里就不做處理,直接返回
  if (!request) {
    return;
  }
  // 這里調用了XMLHttpRequest.abort()
  request.abort();
  // 這里修改的是請求體自身promise的狀態,而不是我們上文提到的那個promise
  reject(cancel);
  // 清空request,請求對象都不要了
  request = null;
});

畢竟你Promise狀態變了,那我then就得執行,執行了干什么?request.abort()也就是XMLHttpRequest.abort()取消請求,成功把這個請求給引爆炸掉了!!!

abort干了什么呢?很遺憾,這個就徹徹底底是個黑盒了,是瀏覽器在幫我們處理,斷點跟不進去,但通過MDN我們可以的值,當調用此方法時,如果請求已發出,那么該方法將中止請求。當一個請求被中止時,它 readyState被更改為 XMLHttpRequest.UNSENT(0) 並且請求的 status代碼被設置為 0。

肆 ❀ 總

那么到這里,我們完整跟着請求代碼把axios執行過程給講了一遍,你會發現axios在內部其實就是創建一個名為token 的 Promise與一個改變Promise狀態的方法cancel,當你需要取消一個請求時,那就把這個Promise與請求綁在一起,然后便可通過cancel改變請求上的Promise狀態,從而達到取消請求的目的,不得不說這個三方庫的作者那是真的牛媽媽給牛寶寶開門,牛到家了!!

另外,abort原生做法我測試發現若請求發出,是會取消請求,所以可以看到chrome控制台有紅色的被取消的請求,而axios直接沒請求,猜測應該是axios內部做了特殊處理,或者是我請求被取消的太快,導致根本沒發出,至於這點我就沒再去研究了。

var xhr = new XMLHttpRequest(),
    method = "GET",
    url = "https://developer.mozilla.org/";
xhr.open(method, url, true);

xhr.send();

if (true) {
  xhr.abort();
}

結合前面文章,我們就可以在簡歷中寫通過axios.cancelToken解決XXX問題,若面試問你這個取消是怎么實現的。那真是不好意思,你算是撞到咱們的槍口上了,又可以愉快的跟面試官battle一番。OK,那么到這里,本文正式結束!!2點了!!睡覺!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM