14
头图

Earlier we talked about the that may time out, and mentioned the "interrupt" processing fetch() This time we will talk about "interrupting" asynchronous operations.

Due to the single-threaded nature of JavaScript, there are not many asynchronous scenarios that can be performed in JavaScript. Some of them are probably as follows:

  1. setTimeout() / setInterval()
  2. event
  3. Ajax
  4. Partial call to Native method
  5. ……

Interrupt Ajax operation

Basically, Ajax processing can also be classified as "call to native method", because it is basically implemented by XMLHttpRequest or fetch() So Axios, fetch() and jQuery.ajax(), etc., all provide an abort interface. Interrupt fetch() has already been Asynchronous operation with possible timeout", here is another example of jQuery and Axios.

jQuery's jqXHR provides .abort()

// url 是在 beeceptor.com 上做的一个要 3 秒后响应的 GET 接口
const fetching = $.ajax(url, { type: "get" })
    .done(() => console.log("看不到这句话"))
    .fail(() => console.log("但是能看到这句"));

setTimeout(() => fetching.abort(), 1000);   // 1 秒后中断请求

You can also write await

(async () => {
    try {
        const fetching = $.ajax(url, { type: "get" });
        setTimeout(() => fetching.abort(), 1000);
        await fetching;
        console.log("看不到这句话");
    } catch (err) {
        console.log("但是能看到这句");
    }
})();

Interrupt Axios request

Axios provides CancelToken to implement interrupts. This mode is the same as the AbortController and AbortSignal fetch() in the previous article.

// Node 中需要 import;浏览器中直接引用的 axios.js 会有全局 axios 对象
import axios from "Axios";

(async () => {
    const { CancelToken } = axios;
    const source = CancelToken.source();    // 创建一个中断源

    try {
        setTimeout(() => source.cancel("1 秒中断"), 1000);
        const data = await axios.get(
            url,    // beeceptor.com 上做的一个要 3 秒后响应的 GET 接口
            {
                cancelToken: source.token   // 把 token 传进去
            }
        );
        console.log("因为超时中断,看不到这句话");
    } catch (err) {
        if (axios.isCancel(err)) {
            console.log("超时中断了 Axios 请求", err);
            // 超时中断了 Axios 请求 Cancel { message: '1 秒中断' }
        } else {
            console.log("发生其他错误");
        }
    }
})();

Interrupt timers and events

The interruption of setTiemout() / setInteraval() can be said to be relatively simple, and it can be done by clearTimeout() / clearInterval()

And the interrupt event-just log off the event handler directly. However, it should be noted that some event frameworks need to provide the registered event processing function when canceling the event. For example, removeEventListener() needs to provide the original processing function; while jQuery only needs to provide the name and namespace when canceling the event processing function .off() (If there is one).

However, when these processes are encapsulated in Promises, remember to reject them during the "deregistration" process (of course, a special value for resolve can be agreed upon). Take setTimeout as an example:

async function delayToDo() {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            resolve("延迟后拿到这段文本");
        }, 5000);
        setTimeout(() => {
            clearTimeout(timer);
            reject("超时了");
        }, 1000);
    });
}

It can be said that this code is rather useless -who would be okay to set a shorter timeout operation immediately after setting a delay task?

Bring out an abort() function

If we need to set up a delayed task and interrupt it in a certain situation later, the correct way is to bring timer to the outside of the delayed task function so that it can be used elsewhere. A better way is to bring out a abort() function to make the semantics more accurate.

function delayToDo(ms) {
    let timer;
    const promise = new Promise(resolve => {
        timer = setTimeout(() => {
            resolve("延迟后拿到这段文本");
        }, ms);
    });
    promise.abort = () => clearTimeout(timer);
    return promise;
}

const promise = delayToDo(5000);

// 在其他业务逻辑中通过 promise.abort() 来中断延时任务
setTimeout(() => promise.abort(), 1000);

Use the transfer box object to transport abort() out

Note that delayToDo() is not a function of async If we use async modify, we cannot get return of promise . async really needs to be modified, I have to work around it and bring out abort()

function delayToDo(ms, transferBox) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            resolve("延迟后拿到这段文本");
        }, ms);

        // 如果有转运箱,就把 abort 函数给运出去
        if (transferBox) transferBox.abort = (message) => {
            clearTimeout(timer);
            reject({ abort: true, message });
        };
    });
}

// 定义一个转运箱对象,注意作用域(所以定义在 IIFE 外面)
const box = {};

(async () => {
    try {
        const s = await delayToDo(5000, box);
        console.log("不会输出这句", s);
    } catch (err) {
        console.log("出错", err);
    }
})();

// 1 秒后通过转运出来的 abort 中断延时操作
setTimeout(() => box.abort("超时中断"), 1000);

// 1 秒后会输出下面这行
// 出错 { abort: true, message: '超时中断' }

Use AbortController & AbortSignal

The operation of using the transfer box looks very similar to CancelToken It's just that CancelToken brings the signal to the asynchronous operation, and the transfer box brings the interrupt function outside. AbortController and CanelToken are similar. Modern environments (Chrome 66+, Nodejs 15+) have AbortController , so try to use this professional tool.

function delayToDo(ms, signal) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => resolve("延迟后拿到这段文本"), ms);

        if (signal) {
            // 如果 AbortController 发出了中断信号,会触发 onabort 事件
            signal.onabort = () => {
                clearTimeout(timer);
                reject({ abort: true, message: "timeout" });
            };
        }
    });
}

const abortController = new AbortController();
(async () => {
    try {
        const s = await delayToDo(5000, abortController.signal);
        console.log("不会输出这句", s);
    } catch (err) {
        console.log("出错", err);
    }
})();

setTimeout(() => abortController.abort(), 1000);

There is not much difference between this code and the above paragraph, but the semantics are more clear after using AbortController. After all, it is specifically used to "interrupt" this matter. Unfortunately AbortController of abort() method without any parameters, can not interrupt message (reason) to go with.

Implement a MyAbort

AbortController still in the experimental stage and is not very mature, so it is normal to have some unsatisfactory. But this principle is actually not difficult to say, you might as well implement one yourself.

This JavaScript code uses ESM syntax, Private field, Field declarations, Symbol, etc. If you don’t understand, please check MDN .
const ABORT = Symbol("abort");

export class MyAbortSingal {
    #onabort;
    aborted;
    reson;

    // 使用模块内未导出的 ABORT Symbol 来定义,目的有两个
    // 1) 避免被用户调用
    // 2) 给 MyAbort 调用(如果做成 private field,MyAbort 就不能访问)
    [ABORT](reson) {
        this.reson = reson;
        this.aborted = true;
        if (this.#onabort) {
            this.#onabort(reson);
        }
    }

    // 允许设置 onabort,但不允许获取(也不需要获取)
    set onabort(fn) {
        if (typeof fn === "function") {
            this.#onabort = fn;
        }
    }
}

export class MyAbort {
    #signal;

    constructor() {
        this.#signal = new MyAbortSingal();
    }

    // 允许获取 signal,但不允许设置
    get signal() { return this.#signal; }

    abort(reson) {
        this.#signal[ABORT](reson);
    }
}

With MyAbort may alternatively fall directly preceding sample code AbortController . And you can also pass in the reason when .abort()

import { MyAbort } from "./my-abort.js";

function delayToDo(ms, signal) {
    return new Promise((resolve, reject) => {
        ...
        reject({ abort: true, message: signal.reson });
        ...    
    });
}

const abortController = new MyAbort();
...

setTimeout(() => abortController.abort("一秒超时"), 1000);

More detailed interruption

For timers and events, the "logout" method is mainly used to interrupt. But in fact this granularity may be a bit coarse.

Interrupt the endless loop

If there is one thing, you need to keep trying until it succeeds. This kind of thing is usually written as an endless loop, and will not break out of the loop until the goal is reached. If it is not JavaScript, such as Java or C#, a new thread is usually opened to do it, and then every time the loop is looped, it checks whether there is an abort signal, and if there is, it will be interrupted.

JavaScript is single-threaded, so if you want to write an endless loop, it's really dead. But there is a workaround-use setInterval() to periodically process it, just like a loop, and continue to process it every other time until clearInterval() (like exiting the loop). In the process of periodic processing, it is possible to judge the abort signal, like this:

function loop(signal) {
    const timer = setInterval(
        () => {
            if (signal.aborted) {
                clearInterval(timer);
                return;
            }
            // TODO 业务处理
        },
        200
    );
    signal.onabort = () => clearInterval(timer);
}

const ac = new AbortController();
loop(ac.signal);

You see, the infinite loop is not really dead, you still have to leave the interrupt interface.

Interrupt complex multi-step asynchronous operations

In addition to loops, some asynchronous operations are also time-consuming. For example, processing a certain business requires multiple interactions with the backend:

  1. Authenticate through the information entered by the user
  2. After getting the authentication, get the user's basic information
  3. Get the department information from the department number of the user information
  4. Obtain the relevant data of the department according to the department information

The business operation illustrated here has so many steps, which can actually be simplified through negotiation with the backend, but it is not within the scope of our discussion today. In actual business, there are indeed many situations that require multiple steps to complete. What we are going to discuss now is how to interrupt. First look at the sample code of this business process:

async function longBusiness() {
    const auth = await remoteAuth();
    const userInfo = await fetchUserInfo(auth.token);
    const department = await fetchDepartment(userInfo.departmentId);
    const data = await fetchData(department);
    dealWithData();
}

There are not many sentences, but it is time-consuming. If an interaction takes 1 second, the operation takes at least 4 seconds to complete. What if the user wants to interrupt at the second second?

In fact, it is the setInterval() , it is good to insert the check of the abort signal appropriately:

async function sleep(ms) {
    return new Promise(resolve => setTimeout(() => {
        console.log(`完成 ${ms} 任务`);
        resolve();
    }, ms));
}

// 模拟异步函数
const remoteAuth = () => sleep(1000);
const fetchUserInfo = () => sleep(2000);
const fetchDepartment = () => sleep(3000);
const fetchData = () => sleep(4000);

async function longBusiness(signal) {
    try {
        const auth = await remoteAuth();
        checkAbort();
        const userInfo = await fetchUserInfo(auth?.token);
        checkAbort();
        const department = await fetchDepartment(userInfo?.departmentId);
        checkAbort();
        const data = await fetchData(department);
        checkAbort();
        // TODO 处理数据
    } catch (err) {
        if (err === signal) {
            console.log("中断退出");
            return;
        }
        // 其他情况是业务错误,应该进行容错处理,或者抛出去给外层逻辑处理
        throw err;
    }

    function checkAbort() {
        if (signal.aborted) {
            // 抛出的功能在 catch 中检查出来就行,最好定义一个 AbortError
            throw signal;
        }
    }
}

const ac = new AbortController();
longBusiness(ac.signal);

setTimeout(() => {
    ac.abort();
}, 2000);

longBusiness() performs an abort signal check every time a time-consuming operation is performed. In the example, if the abort information is checked, an exception will be thrown to interrupt the program. It is more convenient to use the method of throwing an exception to interrupt the program. If you don't like it, you can also use the if branch to handle it, such as if (signal.aborted) { return; } .

This sample program will complete two time-consuming tasks, because when the interrupt is requested, the second time-consuming task is in progress, and the next abort information check will only occur after it ends.

summary

In general, interruption is not difficult. But when we write programs, we often forget to perform interrupt processing that may be required for time-consuming programs. Necessary interrupt processing can save computing resources and improve user experience. Try it if you have a suitable business scenario!


边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!