如何处理useEffect中的“竞争条件”的影响?

309 阅读2分钟

如何处理useEffect中的“竞争条件”的影响?

在使用useEffect请求数据的时候,响应数据可能会以不同的顺序到达。

比如:一个组件接受一个id作为prop,使用iduseEffect中获取数据,可能会遇到一些奇怪的事情,比如组件有时能正确显示数据,有时展示的却是过时的旧数据。

这时候我们就遇到了race condition竞争条件,组件会根据首先完成的请求显示对应结果,在useEffect中获取数据时,如果id更改足够快,该组件就具有了竞争条件:

import React, { useEffect, useState } from 'react';

export default function DataView(props) {
    const [data, setData] = useState(null);
    
    useEffect(() => {
        const fetchData = async () => {
            setTimeout(async () => {
                const response = await fetch(`https://xxx/api/book/${props.id}`);
                const newData = await response.json();
                setData(newData);
            }, Math.round(Math.random() * 3000));
        }
        
        fetchData();
    }, [props.id])
    
    if (!data) return null;
    
    return <div>{data.name}</div>
}

上面的代码片段中在每个fetchData请求中添加了一个3s左右的随机延迟,如果外部props.id的值变更速度够快的情况下,该组件会发出多个请求,这些请求随机无序完成,组件的UI展示结果将是最后完成请求的结果。

修复useEffect的竞争条件

诀窍:利用useEffect的clean-up清理功能

  • 使用一个布尔标志,通过条件判断只呈现最后一个结果
  • 使用AbortController终止过期请求

使用带有布尔标志的清理函数

import React, { useEffect, useState } from 'react';

export default function DataView(props) {
    const [data, setData] = useState(null);
    
    useEffect(() => {
        let active = true;
        
        const fetchData = async () => {
            setTimeout(async () => {
                const response = await fetch(`https://xxx/api/book/${props.id}`);
                const newData = await response.json();
                if (active) {
                    setData(newData);
                }
            }, Math.round(Math.random() * 3000));
        }
        
        fetchData();
        
        return () => {
            active = false;
        }
    }, [props.id])
    
    if (!data) return null;
    
    return <div>{data.name}</div>
}

useEffect清理函数会在组件卸载之前运行,以防止内存泄漏,此外,如果一个组件多次渲染,则在下一个效果之前会清除前一个效果,在上面的代码中:

  • 更改props.id会导致重新渲染
  • 每次重新渲染会触发清理函数执行,将active设置为false
  • activefalse时,过时的请求响应数据就无法更新我们的组件状态

使用AbortController的清理函数

import React, { useEffect, useState } from 'react';

export default function DataView(props) {
    const [data, setData] = useState(null);
    
    useEffect(() => {
        const abortController = new AbortController();
        
        const fetchData = async () => {
            setTimeout(async () => {
                try {
                    const response = await fetch(`https://xxx/api/book/${props.id}`,{
                        signal: abortController.signal,
                    });
                    const newData = await response.json();
                    setData(newData);
                } catch (error) {
                    if (error.name === 'AbortError') {
                        // aborting a fetch throws an error
                        // 
                    }
                }
            }, Math.round(Math.random() * 3000));
        }
        
        fetchData();
        
        return () => {
            abortController.abort();
        }
    }, [props.id])
    
    if (!data) return null;
    
    return <div>{data.name}</div>
}
  • useEffect顶部初始化AbortController
  • AbortController.signal传递给fetchoptions参数
  • 捕获抛出的AbortError,当调用abort()时,fetch()reject并返回AbortError

虽然在useEffect中请求数据是一种很流行的方式,但是不推荐,因为直接在Effect中编写数据请求会显得重复,并且很难添加缓存和服务端渲染等优化。