React中的并发模式:使Web应用程序适应设备和互联网速度

在本文中,我将介绍React中的并发模式。让我们弄清楚它是什么:功能是什么,出现了什么新工具,以及如何在它们的帮助下优化Web应用程序的操作,以便一切都能为用户服务。并发模式是React中的新功能。它的任务是使应用程序适应不同的设备和网络速度。到目前为止,并发模式是一个可以由库的开发人员更改的实验,这意味着稳定器中没有新工具。我警告过你,现在走吧。



当前,呈现组件有两个限制:处理器能力和网络数据传输速率。每当需要向用户显示某些内容时,当前版本的React都会尝试从头到尾渲染每个组件。接口是否可能冻结几秒钟都没有关系。数据传输也是如此。React绝对会等待组件需要的所有数据,而不是逐个绘制。







竞争机制解决了这些问题。有了它,React可以暂停,优先化甚至撤销以前阻塞的操作,因此在并发模式下,无论是否已接收所有数据或仅接收部分数据,都可以开始渲染组件。



并发模式是光纤架构



竞争模式并不是开发人员突然决定添加的新事物,一切都在那里正常工作。提前准备好发布。在版本16中,React引擎已切换为Fiber架构,该架构在原则上类似于操作系统中的任务调度程序。调度程序在进程之间分配计算资源。它可以随时切换,因此用户有一个进程并行运行的错觉。



光纤体系结构的作用相同,但具有组件。尽管事实上它已经在React中,但是Fiber架构似乎处于悬浮动画状态,并且没有最大程度地使用其功能。竞争模式将以全功率将其打开。



在正常模式下更新组件时,必须在屏幕上绘制一个全新的框架。在更新完成之前,用户将看不到任何内容。在这种情况下,React会同步工作。光纤使用不同的概念。每隔16 ms会有一个中断和检查:虚拟树是否已更改,是否出现了新数据?如果是这样,用户将立即看到它们。



为什么是16ms?React开发人员努力以接近每秒60帧的速度重新绘制屏幕。为了使60个更新适应1000ms,您需要大约每16ms执行一次。因此,该图。竞争模式开箱即用,并添加了新工具,使前端生活变得更好。我将详细介绍每个。



悬念



在React 16.6中引入了Suspense作为动态加载组件的机制。在并发模式下,此逻辑得以保留,但是会出现其他机会。暂挂成为一种与数据加载库结合使用的机制。我们通过库请求特殊资源并从中读取数据。



暂挂同时读取尚未准备好的数据。怎么样?我们要求提供数据,直到数据完整为止,我们已经开始小批量阅读它们。对于开发人员来说,最酷的事情是管理加载数据的显示顺序。通过Suspense,您可以同时且彼此独立地显示页面组件。它使代码简单明了:只需查看Suspense结构即可查看请求数据的顺序。



在“旧” React中加载页面的典型解决方案是渲染时获取。在这种情况下,我们在useEffect或componentDidMount内部渲染之后请求数据。当没有Redux或其他数据层时,这是标准逻辑。例如,我们要绘制2个组件,每个组件都需要数据:



  • 组件要求1
  • 期望…
  • 获取数据->渲染组件1
  • 组件请求2
  • 期望…
  • 获取数据->渲染组件2


在这种方法中,仅在第一个组件被渲染之后才请求下一个组件。这很长且不方便。



让我们考虑另一种方法,即Fetch-Then-Render:首先请求所有数据,然后绘制页面。



  • 组件要求1
  • 组件请求2
  • 期望…
  • 获取组件1
  • 获取组件2
  • 组件渲染


在这种情况下,我们将请求的状态上移某处-我们将其委托给库以处理数据。该方法效果很好,但有细微差别。如果一个组件的加载时间比另一个组件的加载时间长,尽管我们已经向他展示了一些东西,但用户将看不到任何东西。让我们看一下演示中的示例代码,其中包含2个组件:用户和帖子。我们将组件包装在Suspense中:



const resource = fetchData() // -    React
function Page({ resource }) {
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </Suspense>
    )
}


当我们在渲染第一个组件之后请求数据时,这种方法似乎与Fetch-On-Render接近。但是实际上,使用Suspense可以更快地获取数据。这是因为两个请求都是并行发送的。



在Suspense中,您可以指定后备,我们要显示的组件,并传递由组件内部的数据检索库实现的资源。我们按原样使用它。在组件内部,我们从资源中请求数据并调用read方法。这是图书馆对我们的承诺。暂挂将了解数据是否已加载,如果已加载,则将显示它。



请注意,组件正在尝试读取仍在接收过程中的数据:



function User() {
    const user = resource.user.read()
    return <h1>{user.name}</h1>
}
function Posts() {
    const posts = resource.posts.read()
    return //  
}


在Dan Abramov的当前演示中,这种东西用作资源的存根



read() {
    if (status === 'pending') {
        throw suspender
    } else if (status === 'error') {
        throw result
    } else if (status === 'success') {
        return result
    }
}




如果资源仍在加载,我们将Promise对象作为异常抛出。 Suspense捕获到此异常,意识到它是一个Promise,并继续加载。如果到达其他对象的异常而不是Promise,则可以清楚地看到该请求以错误结束。返回完成的结果后,Suspense将显示它。对于我们来说,获取资源并在其上调用方法很重要。内部如何实现是库开发人员的决定,主要是Suspense了解其实现。



什么时候请求数据?在树的顶部询问不是一个好主意,因为可能永远不需要。更好的选择是在事件处理程序内部导航时立即执行此操作。例如,通过钩子获取初始状态,然后在用户单击按钮后立即请求资源。



这就是它在代码中的样子:



function App() {
    const [resource, setResource] = useState(initialResource)
    return (
        <>
            <Button text='' onClick={() => {
                setResource(fetchData())
            }}>
            <Page resource={resource} />
        </>
    );
}


悬念非常灵活。它可以用于一个接一个地显示组件。



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </Suspense>
)


或者同时,这两个组件都需要包装在一个Suspense中。



return (
    <Suspense fallback={<h1>Loading user and posts...</h1>}>
        <User />
        <Posts />
    </Suspense>
)


或者,通过将组件包装在独立的Suspense中来彼此分开加载。资源将通过库加载。这是非常酷和方便。



return (
    <>
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User />
        </Suspense>
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </>
)


同样,“错误边界”组件将在Suspense中捕获错误。如果出现问题,我们可以显示用户已加载,但帖子尚未加载,并给出错误。



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User resource={resource} />
        <ErrorBoundary fallback={<h2>Could not fetch posts</h2>}>
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </ErrorBoundary>
    </Suspense>
)


现在,让我们看一下其他工具,这些工具可以充分释放竞争机制的全部好处。



暂记单



SuspenseList同时帮助控制Suspense的加载顺序。如果我们需要一个接一个地挂起多个暂挂,而没有它,则它们必须相互嵌套:



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
            <Suspense fallback={<h1>Loading facts...</h1>}>
                <Facts />
            </Suspense>
        </Suspense>
    </Suspense>
)


SuspenseList使这变得容易得多:



return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
        <Suspense fallback={<h1>Loading facts...</h1>}>
            <Facts />
        </Suspense>
    </Suspense>
)


SuspenseList的灵活性是惊人的。您可以根据需要将SuspenseList彼此嵌套,并在其中自定义加载顺序,因为这将方便显示小部件和任何其他组件。



useTransition



一个特殊的挂钩,它将组件的更新推迟到完全准备就绪并删除中间加载状态为止。这是为了什么当改变状态时,React努力使转换尽快。但是有时候花点时间很重要。如果一部分数据是通过用户操作加载的,那么通常在加载时,我们会显示一个加载器或框架。如果数据很快到达,则装载机即使有半圈也没有时间完成。它将闪烁,然后消失,我们将绘制更新的组件。在这种情况下,最好不要显示加载程序。



这是useTransition出现的地方。它如何在代码中工作?我们调用useTransition挂钩,并以毫秒为单位指定超时。如果数据没有在指定时间内到达,那么我们仍将显示加载程序。但是,如果我们更快地获得它们,将会立即过渡。



function App() {
    const [resource, setResource] = useState(initialResource)
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })
    return <>
        <Button text='' disabled={isPending} onClick={() => {
            startTransition(() => {
                setResource(fetchData())
            })
        }}>
        <Page resource={resource} />
    </>
}


有时,当我们转到页面时,我们不想显示加载程序,但是我们仍然需要在界面中进行更改。例如,在过渡期间,请阻止按钮。然后,isPending属性将派上用场-它会通知您我们处于过渡阶段。对于用户而言,更新将是即时的,但在此需要注意的是,useTransition魔术仅影响包装在Suspense中的组件。UseTransition本身不起作用。



过渡在接口中很常见。负责转换的逻辑可以很好地缝到按钮中并集成到库中。如果存在负责页面之间转换的组件,则可以将通过道具传递的onClick包装到handleClick中的按钮,并显示isDisabled状态。



function Button({ text, onClick }) {
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })

    function handleClick() {
        startTransition(() => {
            onClick()
        })
    }

    return <button onClick={handleClick} disabled={isPending}>text</button>
}


useDeferredValue



因此,有一个我们可以进行转换的组件。有时会出现以下情况:用户想要转到另一个页面,我们已经收到了一些数据,并准备显示它。同时,页面彼此之间略有不同。在这种情况下,在加载其他所有数据之前向用户显示陈旧数据是合乎逻辑的。



现在React无法做到这一点:在当前版本中,只有当前状态的数据才能显示在用户屏幕上。但是并发模式下的useDeferredValue可以返回值的延迟版本,显示陈旧数据,而不是在引导时闪烁加载程序或回退。这个钩子获取我们想要获得延迟版本的值以及延迟(以毫秒为单位)。



界面变得超级流畅。可以使用最少的数据进行更新,并且其他所有内容都将逐渐加载。用户给应用程序以快速流畅的印象。实际上,useDeferredValue看起来像这样:



function Page({ resource }) {
    const deferredResource = useDeferredValue(resource, { timeoutMs: 1000 })
    const isDeferred = resource !== deferredResource;
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={deferredResource} isDeferred={isDeferred}/>
            </Suspense>
        </Suspense>
    )
}


您可以将道具的值与通过useDeferredValue获得的道具进行比较。如果它们不同,则页面仍在加载。



有趣的是,useDeferredValue将使您不仅可以对通过网络传输的数据重复执行延迟加载技巧,而且还可以消除由于计算量大而导致的接口冻结。



为什么这么好?不同的设备工作方式不同。如果您在新iPhone上使用useDeferredValue运行应用程序,即使页面很沉,从页面到页面的转换也将是即时的。但是当使用去抖动功能时,即使在功能强大的设备上也会出现延迟。UseDeferredValue和并发模式适用于硬件:如果运行缓慢,则输入仍然会飞动,并且页面本身将在设备允许的情况下进行更新。



如何将项目切换到并发模式?



竞争模式是一种模式,因此您需要启用它。就像拨动开关一样,可使Fiber满负荷工作。从哪里开始?



我们删除了遗产。我们摆脱了代码中所有过时的方法,并确保它们不在库中。如果应用程序在React.StrictMode上运行良好,那么一切都很好-移动将很容易。潜在的复杂性是库中的问题。在这种情况下,您需要升级到新版本或更改库。还是放弃竞争机制。摆脱传统后,剩下的就是切换根目录。



随着并发模式的到来,将提供三种根连接模式:





  • ReactDOM.render(<App />, rootNode)

    竞争模式发布后,旧的渲染模式将被弃用。
  • 阻止模式

    ReactDOM.createBlockingRoot(rootNode).render(<App />)

    作为一个中间阶段,将添加阻止模式,在存在遗留问题或其他困难的项目上,可以使用竞争模式的某些机会。
  • 竞争模式

    ReactDOM.createRoot(rootNode).render(<App />)

    如果一切都很好,没有遗留物,并且可以立即切换项目,请使用createRoot替换项目中的渲染-并开创美好的未来。


结论



通过切换到Fibre,可以使React内部的阻塞操作变得异步。新工具不断涌现,可以轻松地使应用程序适应设备的功能和网络速度:



  • 暂停,您可以通过它指定加载数据的顺序。
  • SuspenseList,使用它更加方便。
  • useTransition用于在Suspense包装的组件之间创建平滑的过渡。
  • useDeferredValue-在I / O和组件更新期间显示过时的数据


尝试并发模式时,请尝试一下。并发模式使您获得令人印象深刻的结果:以任何方便的顺序快速流畅地加载组件,超流体接口。细节的文档中描述的那样,有演示用,是值得探讨在自己的例子。而且,如果您对光纤架构的工作方式感到好奇,那么这里有一个有趣的演讲的链接



评估您的项目-新工具可以改善什么?而且,当竞争模式退出时,请随时行动。一切都会很棒!



All Articles