# 使用AbortController中止请求

const abortController = new AbortController();
const { signal } = abortController;

const getNumber = async () => {
    const number = await useNumber(signal);
    setNumber(number);
};
useEffect(() => {
    getNumber();
    return () => abortController.abort()
}, []);
1
2
3
4
5
6
7
8
9
10
11
export const useNumber = async (signal?: AbortSignal) => {
    const response = await fetch("http://127.0.0.1:3000/test/getNumber", {
        signal,
    });
    if (!response.ok) {
        throw new Error("Network response was not ok");
    }
    const data = await response.json();
    return data.number
};
1
2
3
4
5
6
7
8
9
10

# 封装AbortController

import { useEffect } from "react";

export const useAbortController = () => {
    const abortController = new AbortController();
    const { signal } = abortController;

    useEffect(() => () => abortController.abort(), []);
    return {
        signal,
    };
};
1
2
3
4
5
6
7
8
9
10
11

在组件中使用,只需将signal传递给对应接口即可

const { signal } = useAbortController();
1

进一步优化

# useAbortRequest

export const useAbortRequest = <T>(asyncFunction: (signal: AbortSignal) => Promise<T>) => {
    const [data, setData] = useState<T | null>(null)
    const [error, setError] = useState<string | null>(null)
    const [loading, setLoading] = useState<boolean>(true)

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

        setLoading(true)

        asyncFunction(signal).then((res) => {
            setData(res)
            setError(null)
        }).catch((err) => {
            if (err.name !== 'AbortError') {
                setError(err.message)
            }
        }).finally(() => {
            setLoading(false)
        })
        return () => controller.abort() // 返回一个中止函数
    }
    return { data, error, loading, run }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

在组件中使用

const About1: React.FC = () => {
    const { data, error, loading, run } = useAbortRequest(useNumber);
    console.log("data: ", data, loading, error);

    useEffect(() => {
        const abort = run();
        return () => {
            abort();
        };
    }, []);
		// ...
};
1
2
3
4
5
6
7
8
9
10
11
12

使用useCallback优化

export const useAbortRequest = <T>(asyncFunction: (signal: AbortSignal) => Promise<T>) => {
    const [data, setData] = useState<T | null>(null)
    const [error, setError] = useState<string | null>(null)
    const [loading, setLoading] = useState<boolean>(true)


    const run = useCallback(() => {
        const controller = new AbortController()
        const { signal } = controller

        setLoading(true)

        asyncFunction(signal).then((res) => {
            setData(res)
            setError(null)
        }).catch((err) => {
            if (err.name !== 'AbortError') {
                setError(err.message)
            }
        }).finally(() => {
            setLoading(false)
        })
        return () => controller.abort() // 返回一个中止函数
    }, [asyncFunction])
    return { data, error, loading, run }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 使用AbortController 避免内存泄漏

会导致内存泄漏的操作有哪些:

  • 闭包
  • 无效请求

# return () => controller.abort() 和 return () => { return controller.abort() }

这两个不能等同理解

return () => controller.abort():

  • 这是箭头函数的简写形式,等效于返回一个函数,在这个函数体内直接调用了 controller.abort(),并立即执行 controller.abort() 的操作。
  • 它不是返回 controller.abort() 的引用,而是调用 abort() 函数并执行其副作用。

return () => { return controller.abort(); }

  • 写法:这是箭头函数的完整形式,使用大括号 {} 来包裹函数体。
  • 行为:返回一个箭头函数,该函数在其函数体内执行 controller.abort(),并显式地使用 return 返回 controller.abort() 的结果(通常是 undefined)。

# AbortController取消请求的三种封装方式

# 第一种:自定义hooks

封装自定义hooks,hooks方式即上面第一种useAbortRequest

# 第二种:工厂函数

封装一个axios工厂函数

const service = axios.create({
    baseURL: '/proxyApi',
    timeout: 20000
})

export const axiosRequestWithAbort = <T>(
    options: AxiosRequestConfig = {}
): { request: Promise<T>; controller: AbortController } => {
    const controller = new AbortController()
    const signal = controller.signal

    const config: AxiosRequestConfig = { ...options, signal }
    const request = service(config)
    return { request, controller }
}

// 使用
export const getNumberAbort = () =>
axiosRequestWithAbort<{ number: number }>({
    url: '/test/getNumber',
    method: 'get'
})

useEffect(() => {
    const { request, controller } = getNumberAbort()
    const fetchData = async () => {
        const data = await request
        if (data?.number) {
            setNumber(data.number)
        }
    }
    fetchData()
    return () => {
        controller.abort()
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 第三种:为所有请求都创建一个AbortController

service.interceptors.request.use(
    (config) => {
        // 请求拦截器,添加 AbortController,
        // 保存当前请求的 controller,key 可以用请求 URL 等唯一标识
        const controller = new AbortController()
        config.signal = controller.signal

        if (config.url) {
            controllers.set(config.url, controller)
        }
        
        return config
    },
    (error) => Promise.reject(error)
)

// 响应拦截器,移除已完成请求的 AbortController
service.interceptors.response.use(
    (response) => {
        if (response.config.url) {
            controllers.delete(response.config.url)
        }
        if (response.status === 200) {
            return response.data
        }
    },
    (err) => {
        Promise.reject(err).catch((err) => {
            // 如果请求被取消了
            if (axios.isCancel(err)) {
                console.log('Request canceled', err.message)
            } else console.error('Request failed', err)
        })
    }
)

export function abortRequest(url: string) {
    const controller = controllers.get(url)
    if (controller) {
        controller.abort()
        controllers.delete(url)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

需要取消请求时,只需调用abortRequest即可

优化:当同时发起相同的请求,而query参数不同时

Map的key需要设置成唯一

service.interceptors.request.use(
    (config) => {
        // 请求拦截器,添加 AbortController,
        // 保存当前请求的 controller,key 可以用请求 URL 等唯一标识
        const controller = new AbortController()
        config.signal = controller.signal
        let key = config.url
        if (config.params) {
            key = `${config.url}?${qs.stringify(config.params)}`
        }
        if (key && controllers.has(key)) {
            controllers.get(key)?.abort()
        }
        if (key) {
            controllers.set(key, controller)
        }

        const store = useUserStore.getState()
        console.log('store: ', store.username)

        return config
    },
    (error) => Promise.reject(error)
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# AbortController的另一用处-移除事件处理函数

对,没错,AbortController还可以用来取消事件监听

const listener = () => {
    console.log('click')
}
let abortController: AbortController

const buttonStartEvent = () => {
    if (abortController) {
        abortController.abort('重复点击,取消事件')
    }
    abortController = new AbortController()
    const signal = abortController.signal
    window.addEventListener('click', listener, { signal })
}
const buttonStopEvent = () => {
    abortController.abort('取消事件')
    // window.removeEventListener('click', listener)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AbortController的强大之处,在使用removeEventListener移除事件处理函数时,需要确保addEventListener和removeEventListener的第二个参数listener是同一个。而使用AbortController则无需考虑这个。

# AbortController的abort的reason属性有什么用

AbortController.abort(reason?: any)

const abortController = new AbortController()
const signal = abortController.signal

signal.addEventListener('abort', () => {
    if (signal.reason instanceof Error) {
        console.error('Aborted with error:', signal.reason.message)
    } else {
        console.log('Aborted:', signal.reason)
    }
})
1
2
3
4
5
6
7
8
9
10