当前,呈现组件有两个限制:处理器能力和网络数据传输速率。每当需要向用户显示某些内容时,当前版本的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和组件更新期间显示过时的数据
尝试并发模式时,请尝试一下。并发模式使您获得令人印象深刻的结果:以任何方便的顺序快速流畅地加载组件,超流体接口。细节的文档中描述的那样,有演示用,是值得探讨在自己的例子。而且,如果您对光纤架构的工作方式感到好奇,那么这里有一个有趣的演讲的链接。
评估您的项目-新工具可以改善什么?而且,当竞争模式退出时,请随时行动。一切都会很棒!