第十五章 数据获取 与 竞态条件 【上】

在获取数据时,值得大书特书的,就是 竞态条件。也许你即使开发过大型的复杂应用,但依然没有遇到过这个问题。但要真遇到了,定位它 和 处理它 会非常有挑战。因为 fetch 和 任何 async 函数的本质都是 Promise,所以这一章的主要 内容是关于 Promise的。

我们会在处理一个有 竞态条件 问题的应用 的过程中,学习到:

  • 什么是Promise,以及平平无奇的Promise如何不经意间产生 竞态条件 问题。
  • 为什么 竞态条件 问题 会产生。
  • 四种处理该问题的方法。

什么是Promise?

在讨论 竞态条件 之前,我们需要搞明白什么是 Promise,以及 为什么需要它。

本质上来说, Promise就是一个 期约。JS在执行代码时,通常是同步执行的:一行一行地往下走。而 Promise是其中几个异步执行代码的方式。我们可以通过Promise,来先执行下一行代码,而无需等待当前任务出结果。而Promise又会在当前任务出结果时,通知我们。

Promise使用最多的场景,就是数据获取:

js 复制代码
console.log('first step');

fetch('/some-url')
    .then(() => {
        // wait for Promise to be done
        // log stuff after the promise is dome
        console.log('second step'); // will log THIRD (if successfull)
    })
    .catch(() => {
        console.log('something bad happened'); // will log THIRD (if error happens)
    })
console.log('third step');

这段代码是这样运作的:

Promise 与 竞态条件

Promise最有趣的是,它产生 竞态条件 的场景。为了研究 竞态条件 问题,我写了一个简单的demo。

代码示例: advanced-react.com/examples/15...

这个页面的左边有tabs 栏。点击tab时,右边会渲染出对应的页面,并获取相关的页面。但是,当我们来回切换tab时,用户体验就会非常糟糕:点到了第一个tab,但还是在渲染第二个tab的内容,等等。

这个应用的代码大概是这样的:有两个组件,App组件和Page组件。App组件用来管理当前生效页面的状态,而Page组件负责渲染详情页面的内容。

js 复制代码
const App = () => {
    const [page, setPage] = useState('1');
    
    return ( 
        <>
            {/*left column buttons*/}
            <button onClick={() => setPage('1')}>Issue 1</button>
            <button onClick={() => setPage('2')}>Issue 2</button>
            
            {/*the actual content*/}
            <Page id={page} />
        </>
    );
};

Page组件接受当前页面的id作为属性,使用该id作为参数获取对应的数据,并渲染这些数据。简化后的 Page组件大致是这样的:

js 复制代码
const Page = ({ id }: { id: string }) => { 
    const [data, setData] = useState({});
    
    // pass id to fetch relevant data
    const url = `/some-url/${id}`;
    
    useEffect(() => {
        fetch(url)
        .then((r) => r.json())
        .then((r) => {
            // save data from fetch request to state
            setData(r); 
        });
    }, [url]);
    // render data
    return ( 
        <>
            <h2>{data.title}</h2>
            <p>{data.description}</p>
        </>
    );
};

有了id,我们可以决定从哪里获取数据。数据是在useEffect钩子里获取的,然后把数据保存起来。这段代码看起来很好。那么,这段代码是如何引起 竞态条件的 呢?

产生竞态条件的原因

产生这个问题的两个根本原因: Promise本身 和 React生命周期。

从 React生命周期角度而言:

  • App组件被挂载了
  • Page组件被挂载了,其默认属性id的值为1
  • Page组件中 第一次出触发useEffect钩子

之后,Promise生效了:useEffect里的fetch是一个promise,一个异步操作。这个promise发送了真实的请求,React之后继续运行,并没有等待promise的结果。两秒之后,promise返回了结果。promise的 .then被触发了,在.then中,又调用了setData来更改状态,所以Page组件用新的状态更新了视图。

如果,视图已经被渲染完成了,我点击tab按钮,会有这个事件流:

  • App组件更新了page状态,并传递给Page组件
  • 状态更新引起了App组件的重新渲染
  • 因此,Page组件也重新渲染了
  • Page组件 useEffect钩子的依赖参数里有id, 因为id发生了变化,useEffect再次被触发
  • useEffect 中的 fetch被新的id触发了,2秒后,setData被再次调用,Page组件被更新,我们可以根据数据被更新后的视图。

但是,如果我在一个fetch还在拉取数据完成时,点击另一个tab按钮,更改了id,会发生什么事情?一件非常有趣的事情会发生!

  • App组件会导致Page组件又重新渲染。
  • useEffect也会被触发(因为id变化了)。
  • fetch会在此被触发,React会继续运行下去。
  • 之后,第一个fetch完成了。这个fetch也有setData的引用,而setData属于同一个Page组件(记住 - 它只是更新,组件还是那个组件)。
  • setData在第一个fetch内被触发了,所以Page组件用第一个fetch得到数据进行更新。
  • 之后,第二个fetch完成了。第二个fetch依旧在这儿,只是在后台运行着罢了。第二个fetch也有Page组件的setData的引用,Page组件会更新,只不过,这次用的是第二个fetch里面的数据。

然后,竞态条件就产生了。但跳转到新的页面后,我们会看到页面的闪烁:页面先渲染了第一次fetch所获取的数据,紧接着又渲染了第二次fetch所获取的数据。

如果第二个fetch比第一个fetch先完成,就更有趣了。我们会先看到要导航的页面的,之后又回到了先前的页面。

你可以在下面的代码里看到这个场景。等待所有内容首次加载完成,然后导航到第二页,接着迅速再导航回第一页。

代码示例: advanced-react.com/examples/15...

这段朴实无华的代码,给我们带来了可怕的bug。该怎么解决呢?

相关推荐
百万蹄蹄向前冲12 分钟前
组建百万前端梦之队-计算机大学生竞赛发展蓝图
前端·vue.js·掘金社区
云隙阳光i25 分钟前
实现手机手势签字功能
前端·javascript·vue.js
imkaifan1 小时前
vue2升级Vue3--native、对inheritAttrs作用做以解释、声明的prop属性和未声明prop的属性
前端·vue.js·native修饰符·inheritattrs作用·声明的prop属性·未声明prop的属性
觉醒法师1 小时前
HarmonyOS NEXT - 电商App实例三( 网络请求axios)
前端·华为·typescript·axios·harmonyos·ark-ts
Danta1 小时前
HTTP协议版本演进:从HTTP/0.9到HTTP/3的高分面试回答
前端·网络协议·面试
柠檬树^-^2 小时前
app.config.globalProperties
前端·javascript·vue.js
太阳花ˉ2 小时前
react(一):特点-基本使用-JSX语法
前端·react.js
赵大仁2 小时前
深入解析 React Diff 算法:原理、优化与实践
前端·react.js·前端框架
1024小神2 小时前
vue/react/vite前端项目打包的时候加上时间最简单版本,防止后端扯皮
前端·vue.js·react.js
起来改bug2 小时前
【pptx-preview】react+pptx预览
javascript·react.js·pptx