6

In most cases, network requests are first-request-first-response. However, in some cases, due to some unknown problems, the requested api may be returned first. The easiest solution is to add a loading state so that the next request cannot be made until all requests are completed.

But not all businesses can take this approach. At this time, the developer needs to deal with it to avoid rendering wrong data.

Use "version number"

We can use version numbers to make decisions about business processing and data rendering:

 const invariant = (condition: boolean, errorMsg: string) => {
    if (condition) {
        throw new Error(errorMsg)
    }
}

let versionForXXXQuery = 0;

const checkVersionForXXXQuery = (currentVersion: number) => {
    // 版本不匹配,就抛出错误
    invariant(currentVersion !== versionForXXXQuery, 'The current version is wrong')
}

const XXXQuery = async () => {
    // 此处只能使用 ++versionForXXXQuery 而不能使用 versionForXXXQuery++
    // 否则版本永远不对应
    const queryVersion = ++versionForXXXQuery;

    // 业务请求
    checkVersion(queryVersion)
    // 业务处理
    // ?界面渲染


    // 业务请求
    checkVersion(queryVersion)
    // 业务处理
    // ?界面渲染
}

In this way, the execution of the API requested first will be aborted by an error, but only the latest version of the request is finally rendered on the interface. But the program is too intrusive to the business. Although we can use class and AOP to simplify code and logic. But it's still not friendly to development. At this time we can use AbortController.

Using AbortController

AbortController cancels previous request

Not much to say, first use AbortController to complete the same function above.

 let abortControllerForXXXQuery: AbortController | null = null

const XXXQuery = async () => {
    // 当前有中止控制器,直接把上一次取消
    if (abortControllerForXXXQuery) {
        abortControllerForXXXQuery.abort()
    }

    // 新建控制器
    abortControllerForXXXQuery = new AbortController();

    // 获取信号
    const { signal } = abortControllerForXXXQuery
    
    const resA = await fetch('xxxA', { signal });
    // 业务处理
    // ?界面渲染

    const resB = await fetch('xxxB', { signal });
    // 业务处理
    // ?界面渲染
}

We can see that: the code is very simple, and the performance is enhanced at the same time, the browser will stop getting data in advance (Note: the server will still process multiple requests, and the server pressure can only be reduced by loading).

AbortController remove binding event

Although the code is very simple, why do you need to add an AbortController class like this instead of directly adding an api to abort network requests? Doesn't this add complexity? I also thought so at the beginning. Only found out later. The AbortController class is more complex, but it is generic, so AbortController can be used by other web standards and JavaScript libraries.

 const controller = new AbortController()
const { signal } = controller

// 添加事件并传递 signal
window.addEventListener('click', () => {
    console.log('can abort')
}, { signal })

window.addEventListener('click', () => {
    console.log('click')
});

// 开始请求并且添加 signal
fetch('xxxA', { signal })

// 移除第一个 click 事件同时中止未完成的请求
controller.abort()

Generic AbortController

Since it's generic, isn't it possible to terminate the business method as well. The answer is yes. Let's take a look at why AbortController can be used universally?

AbortController provides a semaphore signal and abort abort method, through which the state and binding events can be obtained.

 const controller = new AbortController();

// 获取信号量
const { signal } = controller;

// 获取当前是否已经执行过 abort,目前返回 false
signal.aborted

// 添加事件
signal.addEventListener('abort', () => {
    console.log('触发 abort')
})

// 添加事件
signal.addEventListener('abort', () => {
    console.log('触发 abort2')
})


// 中止 (不可以解构直接执行 abort,有 this 指向问题)
// 控制台打印 触发 abort,触发 abort2
controller.abort()

// 当前是否已经执行过 abort,返回 ture
signal.aborted

// 控制台无反应
controller.abort();

Undoubtedly, the above event adds the listener of the abort event. In summary, the author simply encapsulated AbortController. The Helper class looks like this:

 class AbortControllerHelper {
    private readonly signal: AbortSignal 

    constructor(signal: AbortSignal) {
        this.signal = signal
        signal.addEventListener('abort', () => this.abort())
    }

    /**
     * 执行调用方法,只需要 signal 状态的话则无需在子类实现
     */
    abort = (): void => {} 

    /**
     * 检查当前是否可以执行
     * @param useBoolean 是否使用布尔值返回
     * @returns 
     */
    checkCanExecution = (useBoolean: boolean = false): boolean => {
        const { aborted } = this.signal
        // 如果使用布尔值,返回是否可以继续执行
        if (useBoolean) {
            return !aborted
        }
        // 直接抛出异常
        if (aborted) {
            throw new Error('abort has already triggered');
        }
        return true
    }
}

In this way, developers can add subclasses to inherit AbortControllerHelper and put in signal. Then abort multiple or even multiple different events through one AbortController.

encourage

If you think this article is good, I hope you can give me some encouragement and help to star it under my github blog.

blog address

References

AbortController MDN


jump__jump
2.5k 声望4k 粉丝

猎奇者...未来战士...very vegetable