28
头图

Since the release of ECMAScript's Promise ES2015 and async/await ES2017 features, asynchronous has become a particularly common operation in the front-end industry. Asynchronous code and synchronous code have some differences in the order of processing problems. Writing asynchronous code requires a different "awareness" from writing synchronous code. For this reason, I also wrote a special article " Asynchronous Programming Needs "Awareness" ", However, not many people watch it, and it may be “boring” indeed.

The question to be discussed in this article may still be "boring", but it is very realistic-what happens if a piece of code cannot be executed for a long time?

If this is synchronous code, we will see a phenomenon called "no response", or in layman's terms-"dead"; but what if it is a piece of asynchronous code? Maybe we can't wait for the result, but other code is still going on, as if this thing didn't happen.

Of course, things didn't really happen, but different phenomena will occur under different circumstances. For example, a page with loading animation seems to be loading all the time; another example is a page that should be updated with data, but no data changes can be seen; another example is a dialog box, which can't be closed... These phenomena are collectively referred to as BUG. But there are also times when an asynchronous operation process does not "echo", it just silently dies there. No one knows that after the page is refreshed, even a trace will not be left behind.

Of course, this is not a novel, we have to talk about "business."

Axios comes with timeout handling

Using Axios to make Web Api calls is a common asynchronous operation process. Usually our code will be written like this:

try {
    const res = await axios.get(url, options);
    // TODO 正常进行后续业务
} catch(err) {
    // TODO 进行容错处理,或者报错
}

This code executes well under normal circumstances, until one day the user complained: Why didn't it respond after waiting for a long time?

Then the developer realized that due to increased server pressure, this request has been difficult to respond instantaneously. Considering the user's feelings, a loading animation is added:

try {
    showLoading();
    const res = await axios.get(url, options);
    // TODO 正常业务
} catch (err) {
    // TODO 容错处理
} finally {
    hideLoading();
}

However, one day, a user said: "I have been waiting for half an hour, and I have been going around in circles!" So the developer realized that for some reason, the request was stuck. In this case, the request should be reissued. Or directly report to the user - well, a timeout check must be added.

Fortunately Axios really can handle a timeout, just in options add in a timeout: 3000 will solve the problem. If it times out, it can be detected and processed catch

try {...}
catch (err) {
    if (err.isAxiosError && !err.response && err.request
        && err.message.startsWith("timeout")) {
        // 如果是 Axios 的 request 错误,并且消息是延时消息
        // TODO 处理超时
    }
}
finally {...}

Axios is no problem, what about fetch()

Processing fetch() timeout

fetch() itself does not have the ability to handle timeouts. We need to judge the timeout to trigger the "cancel" request operation AbortController

If you need to interrupt a fetch() operation, you only need to get signal AbortController object, and pass this signal object as an option of fetch() It's probably like this:

const ac = new AbortController();
const { signal } = ac;
fetch(url, { signal }).then(res => {
    // TODO 处理业务
});

// 1 秒后取消 fetch 操作
setTimeout(() => ac.abort(), 1000);

ac.abort() will send a signal signal abort event, and set its .aborted attribute to true . fetch() internal processing will use this information to abort the request.

The above example demonstrates how to implement the timeout handling of the fetch() If you are using await form to deal with, we need to setTimeout(...) on fetch(...) before:

const ac = new AbortController();
const { signal } = ac;
setTimeout(() => ac.abort(), 1000);
const res = await fetch(url, { signal }).catch(() => undefined); 

In order to avoid the use of try ... catch ... to process the request fails, here in fetch() added after a .catch(...) in ignoring the error. If an error occurs, res will be assigned the value undefined . Actual business processing may require more reasonable catch() processing to make res contain identifiable error information.

This is the end of the fetch() here, but it would be very cumbersome to write such a long piece of code for each 06192ed8ae737e call, so it is better to encapsulate it:

async function fetchWithTimeout(timeout, resoure, init = {}) {
    const ac = new AbortController();
    const signal = ac.signal;
    setTimeout(() => ac.abort(), timeout);
    return fetch(resoure, { ...init, signal });
}

Is it okay? No, there is a problem.

If we output a message setTimeout(...)

setTimeout(() => {
    console.log("It's timeout");
    ac.abort();
}, timeout);

And give enough time in the call:

fetchWithTimeout(5000, url).then(res => console.log("success"));

We will see the output success and after 5 seconds we see the output It's timeout .

By the way, although we handled the timeout fetch(...) , we did not kill timer fetch(...) succeeded. As a careful-thinking programmer, how can you make such a mistake? Kill him!

async function fetchWithTimeout(timeout, resoure, init = {}) {
    const ac = new AbortController();
    const signal = ac.signal;
    
    const timer = setTimeout(() => {
        console.log("It's timeout");
        return ac.abort();
    }, timeout);
    
    try {
        return await fetch(resoure, { ...init, signal });
    } finally {
        clearTimeout(timer);
    }
}

Perfect! But the problem is not over yet.

Everything can time out

Both Axios and fetch provide a way to interrupt asynchronous operations, but for an abort , what should we do?

For such a Promise, I can only say, let him go, and he can do it until he is old--I can't stop it anyway. But life has to go on, I can’t wait forever!

In this case, we can setTimeout() into a Promise, and then use Promise.race() to achieve "outdated":

Race means racing, so is Promise.race() easy to understand?
function waitWithTimeout(promise, timeout, timeoutMessage = "timeout") {
    let timer;
    const timeoutPromise = new Promise((_, reject) => {
        timer = setTimeout(() => reject(timeoutMessage), timeout);
    });

    return Promise.race([timeoutPromise, promise])
        .finally(() => clearTimeout(timer));    // 别忘了清 timer
}

You can write a Timeout to simulate the effect:

(async () => {
    const business = new Promise(resolve => setTimeout(resolve, 1000 * 10));

    try {
        await waitWithTimeout(business, 1000);
        console.log("[Success]");
    } catch (err) {
        console.log("[Error]", err);    // [Error] timeout
    }
})();

As for how to write asynchronous operations that can be aborted, I'll talk about it next time.


边城
59.8k 声望29.6k 粉丝

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