這次終於弄懂Axios是如何中斷請求了

Axios 文檔案例

先看下 Axios 文檔給的例子 https://github.com/axios/axios#cancellation

  1. 通過 CancelToken.source 工廠函數進行取消
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.');

  1. 通過 CancelToken 構造函數進行取消
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

  1. 通過 AbortController 中斷請求,這是 fetch 的 api,本文就不再詳細介紹了,具體使用可以參考 https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController/AbortController
const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// cancel the request
controller.abort()

源碼分析

首先需要從 GitHub 下載 Axios 源碼。如果不想下載,也可以打開 https://github1s.com/axios/axios/ 進行查看。

工廠函數 CancelToken.source

通過前面兩個例子,可以知道取消請求和 CancelToken 這個類息息相關,CancelToken.source()工廠函數只不過是在我們看不見的內部幫助我們去實例化一個 CancelToken 的實例出來。

那我們先來看下工廠函數的實現。

// 文件路徑 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
  };
};

module.exports = CancelToken;

可以看到 CancelToken.source 這個工廠函數就是幫助我們實例化了一個 CancelToken 的實例,然後返回給我們需要使用的 實例(token)取消請求的函數(cancel)

接下來我們繼續深入 CancelToken 內部,看看爲什麼執行了 cancel 函數後,請求就中斷了。

CancelToken 類

// 文件路徑 Axios/lib/cancel/CancelToken.js

// ...

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

  // eslint-disable-next-line func-names
  this.promise.then(function(cancel) {
    if (!token._listeners) return;

    var i;
    var l = token._listeners.length;

    for (i = 0; i < l; i++) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
  });

  // eslint-disable-next-line func-names
  this.promise.then = function(onfulfilled) { // ...
  };

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

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

// ...

通過文檔中的例子可以看出,在發起請求的時候傳入了 cancelToken,也就是 CancelToken 的一個實例。

實例化的過程中會調用我們傳入的 executor函數,將 cancel 函數傳遞給我們外部。

另外這個實例上有一個 promise 的屬性,當我們調用 cancel 函數,promise 則會從 pending 的狀態變成 fulfilled。從而觸發 promise.then,執行所有的 token._listeners

token._listeners 又從何而來?

答案還是在當前的文件中

// 文件路徑 Axios/lib/cancel/CancelToken.js

// ...

/**
 * Subscribe to the cancel signal
 */

CancelToken.prototype.subscribe = function subscribe(listener) {
  // reason 值不爲 undefined 說明該請求已取消,可直接調用 listener
  if (this.reason) {
    listener(this.reason);
    return;
  }

  if (this._listeners) {
    this._listeners.push(listener);
  } else {
    this._listeners = [listener];
  }
};

// ...

CancelToken 的原型對象上添加了 subscribe 方法,用於訂閱取消請求的事件。如果該請求已被取消,則會立即調用 listener,否則會將 listener 保存在 _listeners 數組中。

當我們調用 cancel 也就是取消請求的時候,_listeners 中保存的 listener 則會被調用(見上文)。

這時候並沒有看到中斷請求的操作,具體的邏輯是在 listener 內部,這樣寫的原因就是可以進行解耦,提高代碼的複用性。

另外還有一個 unsubscribe 取消訂閱就不再展開了。

這就是典型的訂閱發佈模式。

取消請求

最快速的方法就是搜索 config.cancelToken.subscribe,這樣就可以快速定位到取消請求的具體實現。

只搜索 lib 文件夾即可,可以看到有兩處地方,一個是 lib/adapters/http.js,另一個是 lib/adapters/xhr.js

因爲 Axios 是一個支持 node.js 和瀏覽器的 http 客戶端。這裏應用了適配器的模式來兼容這兩個平臺。本文研究的是取消請求,就不去深究這部分了,我們看其中之一就好了。

// Axios/lib/adapters/xhr.js

// ...
    if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function(cancel) {
        if (!request) {
          return;
        }
        reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }
    
// ...

從 187 行這裏開始,我們可以看到 config.cancelToken.subscribe(onCanceled)cancelToken 註冊了一箇中斷請求的回調。request.abort(); 這裏的 requestXMLHttpRequest 的一個實例。

另外還有一個函數 done,即請求成功或者失敗之後會將上面註冊的 onCanceled 進行取消註冊。

至此整個取消請求的邏輯就跑通了。我簡單畫了個圖(畫了幾個小時),希望能方便大家理解。

結合 Vue 實現離開頁面取消未完成的請求

思路就是利用一個對象來管理所有的 CancelToken 實例。發起請求之前,把新創建的 cancelToken 保存到對象中,請求結束後(包括成功、失敗)把對應的實例清除。

再結合 vue-router 的路由守衛,這樣就可以在離開頁面的時候取消所有未完成的請求。

有些全局的接口需要做特殊處理,比如請求用戶信息之類的,這些全局通用的接口就不能再離開頁面的時候中斷請求。

具體代碼這裏就不展示了。我寫了一個 demo,有需要的小夥伴可以自行查看。

https://github.com/AD-feiben/demos/tree/main/abort-req

最後再重申一點,學習源碼是爲了學習源碼中優秀的設計,我們需要思考如何將這個設計應用到我們的項目中,這纔是最重要的一點。

希望文章的內容能爲你提供一絲絲幫助,如果錯誤,還望指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章