JavaScript——中断请求的几种方案

本文探讨了在JavaScript中如何模拟中断网络请求,通过Promise和RxJS等方式处理请求的中断。介绍了中断调用链的概念,以及如何设置超时并中断请求。还封装了一个灵活的方法,允许用户手动控制请求的中断,确保即使请求被中断,也不会继续关心其结果。最后,对比了axios的CancelToken使用方法,展示了与包装方法的相似思想。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 Promise

Promise有一个缺点是一旦创建无法取消,所以本质上Promise是无法被终止的.

但是我们可以通过中断调用链中断Promise来模拟请求的中断.

中断调用链

中断调用链就是在某一个then/catch执行之后,后续的链式调用(包括then,catch,finally)不再继续执行.

方法是在then/catch返回一个新的Promise实例,并保持pending状态:

new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('result');
    });
}).then(res => {
    // 达到某种条件,return一个pending状态的Promise实例,以中断调用链
    if (res === 'result') {
        return new Promise(() => {});
    }
    console.log(res); // 不打印
}).then(() => {
    console.log('then不执行'); // 不打印
}).catch(() => {
    console.log('catch不执行'); // 不打印
}).finally(() => {
    console.log('finally不执行'); // 不打印
});

中断Promise

中断Promise不等同于中止Promise,因为Promise是无法被终止的.

这里的中断指的是,在合适的时机,把pending状态的promisereject掉.例如一个常见的应用场景就是给网络请求设置超时时间,一旦超时就中断.

老规矩,用setTimeout来模拟网络请求.阀值设置为Math.random() * 3000表示随机3秒之内返回结果.

const request = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('收到服务端数据')
  }, Math.random() * 3000)
})

假设超过2秒就是网络超时,我们可以封装一个超时处理函数.

由于网络请求所需的事件是随机的,因此可以利用Promise.race方法,达到超时reject的目的.

const timeoutReject = (p1, timeout = 2000) => {
    const p2 = new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('网络超时');
        }, timeout);
    });
    return Promise.race([p1, p2]);
};

timeoutReject(request).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
});

包装abort方法——仿照AxiosCancelToken

上面实现的方式并不灵活,因为中断Promise的方式有很多,不单单是网络超时.

我们可以仿照AxiosCancelToken的核心源码,简单包装一个abort方法,供使用者随时调用.

function abortWrapper(p1) {
    let abort;
    const p2 = new Promise((resolve, reject) => {
        abort = reject;
    });
    // 如果没有resolve或reject,p2的状态永远是pending
    const p = Promise.race([p1, p2]);
    p.abort = abort;
    return p;
}

const req = abortWrapper(request);
req.then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
});

setTimeout(() => {
    // 手动调用req.abort,将p2的状态改变为rejected
    req.abort('手动中断请求');
}, 2000);

如此封装的主要目的就是为了能够在Promise外部控制其resolvereject,让使用者可以随时手动调用resolve(触发.then)reject(触发.catch).

需要注意的是,虽然Promise请求被中断了,但是promise并没有终止,网络请求依然可能返回,只不过那时我们已经不关心请求结果了.

2 RXJSunsubscribe方法

rxjs本身提供了取消订阅的方法,即unsubscribe.

let stream1$ = new Observable(observer => {
    let timeout = setTimeout(() => {
        observer.next('observable timeout');
    }, 2000);

    return () => {
        clearTimeout(timeout);
    }
});
let disposable = stream1$.subscribe(value => console.log(value));
setTimeout(() => {
    disposable.unsubscribe();
}, 1000);

3 AxiosCancelToken

AxiosCancelToken有两种使用方法:

  • 方法一

    import axios from '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
      } 
    });
    
    source.cancel('Operation canceled by the user.');
    
  • 方法二

    import axios from 'axios';
    const CancelToken = axios.CancelToken;
    
    // 创建一个变量如 cancel 用于存储这个中断某个请求的方法
    let cancel;
    
    axios.get('/user/12345', {
      cancelToken: new CancelToken(function executor(c) {
        cancel = c; // 将参数 c 赋值给 cancel
      })
    });
    
    // 判断 cancel 是否为函数,确保 axios 已实例化一个CancelToken
    if (typeof cancel === 'function') {
        cancel();
        cancel = null;
    }
    

CancelToken的核心源码:(axios/lib/cancel/CancelToken.js)

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

可以看到,在Axios底层,CancelToken的核心源码所体现的思想,与上面中断Promise包装abort方法的思想一致.

只不过Axios在外部手动调用resolve(用户触发cancel方法),而resolve一旦调用,就会触发promisethen方法,来看这个promise.then的源码:(axios/lib/adapters/xhr.js)

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

可以看到then方法中会执行abort方法取消请求,同时调用reject让外层的promise失败.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值