Ajax 怎麼取消?要不要取消?

ssshooter發表於2022-06-30

Ajax cancel

假如你熟悉 xhr,會知道 Ajax 其實可以前端主動取消,使用的是 XMLHttpRequest.abort()。當然現在也不是刀耕火種的時代,除了面試,可能基本不會手寫 xhr,在無人不知的 axios 中有兩種取消方法:

首先是老式 cancelToken:

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.')

然後是新玩意(其實也不新)AbortController:

const controller = new AbortController()

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

cancelToken 和 signal 傳到 axios 之後,都會以某種機制呼叫 XMLHttpRequest.abort()

onCanceled = (cancel) => {
  if (!request) {
    return
  }
  reject(
    !cancel || cancel.type ? new CanceledError(null, config, request) : cancel
  )
  request.abort()
  request = null
}

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

cancelToken 是利用釋出訂閱模式通知 axios 取消請求,雖然這部分是 axios 自己實現的,但是源自於一個 tc39 提案 cancelable promises proposal,不過這個提案被廢棄了。

而 AbortController 是已經可以在瀏覽器使用的介面,顧名思義,這就是一個專門用於中止行為的控制器。mdn 的舉例用的也是 Ajax 請求,不過是至潮至 in 的 fetch,從中可見 axios 跟 fetch 的實踐是一致的:

function fetchVideo() {
  controller = new AbortController() // 新建一個 controller
  const signal = controller.signal
  fetch(url, { signal }) // 在 fetch 方法傳入 signal
    .then(function (response) {
      console.log('Download complete', response)
    })
    .catch(function (e) {
      console.log('Download error: ' + e.message)
    })
}

abortBtn.addEventListener('click', function () {
  if (controller) controller.abort() // 呼叫 controller.abort 取消 fetch
  console.log('Download aborted')
})

AbortController 的其他用途

當然 AbortController 不只有中止 Ajax 一個功能,通過檢視 dom 規範文件還能看到兩個使用示例:

一個比較實用的例子是用 AbortController 取消事件監聽:

dictionary AddEventListenerOptions : EventListenerOptions {
  boolean passive = false;
  boolean once = false;
  AbortSignal signal;
};

通過向 AddEventListener 傳入 signal,執行 abort() 即可取消事件監聽,這個方法對匿名回撥函式尤其有用。

另一個例子是用於中止 promise。這是一個比較簡潔且自文件的方法……不過其實實現這個功能也不是非要 AbortController 才能做到,只要想辦法拿到 promise 的 reject 就好了。我覺得這個例子的重點偏向於學會使用 signal 的 onabort:

const controller = new AbortController();
const signal = controller.signal;

startSpinner();

doAmazingness({ ..., signal })
  .then(result => ...)
  .catch(err => {
    if (err.name == 'AbortError') return;
    showUserErrorMessage();
  })
  .then(() => stopSpinner());

// …

controller.abort();

function doAmazingness({signal}) {
  return new Promise((resolve, reject) => {
    signal.throwIfAborted();

    // Begin doing amazingness, and call resolve(result) when done.
    // But also, watch for signals:
    signal.addEventListener('abort', () => {
      // Stop doing amazingness, and:
      reject(signal.reason);
    });
  });
}

總之,signal 就是個簡易發信器,而且功能偏向於取消某操作。如果在某種情況下不想自己實現一個 pubsub 物件的話,用這個就完事了。

AbortController 的介紹就到此為止吧,不知道大家有沒有逐漸忘記標題……最後是想討論一下,取消 Ajax 到底有沒有用?

取消還是不取消,這是個問題

事實上,這個 Ajax 取消只是前端自說自話,後端並不知道要中止,發過去的請求還是要執行的,後端沒有特殊處理的話 10s 的請求你取消了後端也仍然在費勁地跑。

那麼在一些文章中看到的“優化”,所謂“取消請求,只保留最後一個”是否真的有意義呢?

分情況討論,對於 POST 等修改資料的請求,每次傳送即使返回慢,伺服器也已經在處理了,取消上一個 POST 再重複發一個無疑是弱智行為。

對於 GET,且僅針對某些極限操作,或許有一點效果,例如:獲取一個超長 table,結果沒拿到,然後使用者就用搜尋快速返回少量資料並且渲染了,等到超長 table 真正返回就會覆蓋掉搜尋的資料,這個情況 cancel 是真的有效的。另外還有下載上傳的取消,不過估計也很少會用到。

最後再說一個有道理但是事實上也是沒什麼用的好處:cancel 之後能省一個請求位置,畢竟瀏覽器一個域名的同時請求數量是有限制的,更多情況下,比 cancel 更常見的 timeout 更實用。嗯……除非同時排著五六個超慢請求,否則輪轉還是比較快的……

個人建議是,說到底這個所謂“取消”都是極特殊情況的特殊處理,知道這回事就好了,沒有必要沒事就在攔截器裡整個取消操作。

參考

相關文章