React 的 Suspense 功能令人兴奋,它即将到来,将使开发人员能够轻松地让其组件延迟渲染,直到它们“准备好”,从而带来更流畅的用户体验。“准备好”在此处可以指许多方面。例如,您的数据加载实用程序可以与 Suspense 结合使用,允许在任何数据正在传输时显示一致的加载状态,而无需为每个查询手动跟踪加载状态。然后,当您的数据可用且您的组件“准备好”时,它将进行渲染。这是最常与 Suspense 一起讨论的主题,我之前也写过关于它的文章;但是,数据加载只是 Suspense 可以改善用户体验的众多用例之一。我今天想谈论的另一个用例是图像预加载。
您是否曾经制作或使用过一个 Web 应用,在该应用中,在到达屏幕后,您的位置会随着图像下载和渲染而发生抖动和跳跃?我们称之为内容重排,它既令人震惊又令人不快。Suspense 可以帮助解决这个问题。您知道我说过 Suspense 的全部意义在于阻止组件渲染,直到它准备好为止吗?幸运的是,“准备好”在此处非常开放——出于我们的目的,可以包括“我们需要的已预加载的图像”。让我们看看如何操作!
Suspense 快速入门
在深入探讨细节之前,让我们快速了解 Suspense 的工作原理。它有两个主要部分。首先是组件挂起的概念。这意味着 React 尝试渲染我们的组件,但它尚未“准备好”。发生这种情况时,组件树中最接近的“后备”将进行渲染。我们很快就会看看如何制作后备(这相当简单),但是组件告诉 React 它尚未准备好的方式是抛出一个 Promise。React 将捕获该 Promise,意识到组件尚未准备好,并渲染后备。当 Promise 解析时,React 将再次尝试渲染。重复此过程。是的,我有点过于简化了,但这正是 Suspense 工作原理的要点,我们将在进行过程中扩展其中一些概念。
Suspense 的第二个部分是引入“过渡”状态更新。这意味着我们设置状态,但告诉 React 状态更改可能会导致组件挂起,如果发生这种情况,则不渲染后备。相反,我们希望继续查看当前屏幕,直到状态更新准备好,此时它将进行渲染。当然,React 为我们提供了一个“pending”布尔指示器,让开发人员知道此过程正在进行中,以便我们可以提供内联加载反馈。
让我们预加载一些图像!
首先,我想指出,本文末尾有一个我们正在制作的完整演示。如果您只想跳入代码,请随时现在打开演示。它将展示如何将 Suspense 与过渡状态更新结合使用来预加载图像。这篇文章的其余部分将逐步构建该代码,并沿途解释如何以及为什么。
好的,让我们开始吧!
我们希望我们的组件挂起,直到所有图像都已预加载。为了尽可能简化操作,让我们创建一个 <suspenseimage></suspenseimage>
组件,该组件接收 src 属性,预加载图像,处理异常抛出,然后在一切准备就绪时渲染一个 <img src="/static/imghwm/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174358885649082.png?x-oss-process=image/resize,p_40" class="lazy" alt="Pre-Caching Images with React Suspense ">
在 HTML 中使用图像,但我们也可以使用 JavaScript 中的 Image()
对象以命令式方式创建图像;此外,我们这样创建的图像具有一个 onload
回调,该回调在图像…加载时触发。它看起来像这样:
const img = new Image(); img.onload = () => { // 图像已加载 };
但是我们如何将其与异常抛出结合起来呢?如果您像我一样,您首先可能会想到类似这样的东西:
const SuspenseImg = ({ src, ...rest }) => { throw new Promise((resolve) => { const img = new Image(); img.onload = () => { resolve(); }; img.src = src; }); return <img alt="" src="%7Bsrc%7D">; };
当然,问题在于这将始终抛出一个 Promise。每次 React 尝试渲染 <suspenseimg></suspenseimg>
实例时,都会创建一个新的 Promise,并立即将其抛出。相反,我们只想在图像加载之前抛出一个 Promise。有一句老话,计算机科学中的每个问题都可以通过添加一层间接性来解决(除了间接性层数过多的问题),所以让我们这样做,并构建一个图像缓存。当我们读取 src 时,缓存将检查它是否已加载该图像,如果没有,它将开始预加载并抛出异常。而且,如果图像已预加载,它将只返回 true 并让 React 继续渲染我们的图像。
这是我们的 <suspenseimage></suspenseimage>
组件的样子:
export const SuspenseImg = ({ src, ...rest }) => { imgCache.read(src); return <img src="%7Bsrc%7D" alt="用反应悬念的预处理图像" >; };
这是我们缓存的最小版本的样子:
const imgCache = { __cache: {}, read(src) { if (!this.__cache[src]) { this.__cache[src] = new Promise((resolve) => { const img = new Image(); img.onload = () => { this.__cache[src] = true; resolve(this.__cache[src]); }; img.src = src; }); } if (this.__cache[src] instanceof Promise) { throw this.__cache[src]; } return this.__cache[src]; } };
它并不完美,但目前足够了。让我们继续使用它。
实现
请记住,下面有一个指向完整工作演示的链接,因此,如果我在任何特定步骤中移动得太快,请不要绝望。我们将边走边解释。
让我们从定义我们的后备开始。我们通过在组件树中放置一个 Suspense 标记来定义后备,并通过 fallback 属性传递我们的后备。任何挂起的组件都将向上搜索最近的 Suspense 标记,并渲染其后备(但是如果没有找到 Suspense 标记,则会抛出错误)。一个真正的应用程序可能在整个过程中都有许多 Suspense 标记,为其各个模块定义特定的后备,但对于此演示,我们只需要一个包装我们的根应用程序的标记。
function App() { return ( <suspense fallback="{<Loading"></suspense>}> <showimages></showimages> ); }
<loading></loading>
组件是一个基本的旋转器,但在实际应用程序中,您可能希望渲染实际尝试渲染的组件的某种空壳,以提供更无缝的体验。
有了这个,我们的 <showimages></showimages>
组件最终将使用以下内容渲染我们的图像:
<div> {images.map((img) => ( <div key="{img}"> <suspenseimg alt="" src="%7Bimg%7D"></suspenseimg> </div> ))} </div>
在初始加载时,我们的加载旋转器将显示,直到我们的初始图像准备好,此时它们将同时显示,没有任何交错的重排卡顿。
过渡状态更新
一旦图像到位,当我们加载下一批图像时,我们希望它们在加载后显示,当然,在它们加载时保持屏幕上的现有图像。我们使用 useTransition
钩子来做到这一点。这将返回一个 startTransition
函数和一个 isPending
布尔值,它指示我们的状态更新正在进行中,但已挂起(或者即使它尚未挂起,如果状态更新只是花费的时间太长,也可能仍然为 true)。最后,当调用 useTransition
时,您需要传递一个 timeoutMs
值,这是 isPending
标志可以为 true 的最大时间量,之后 React 将放弃并渲染后备(请注意,timeoutMs
参数可能会在不久的将来被删除,当更新现有内容时,过渡状态更新只需等待尽可能长的时间)。
这是我的样子:
const [startTransition, isPending] = useTransition({ timeoutMs: 10000 });
在我们的后备显示之前,我们将允许 10 秒钟过去,这在现实生活中可能太长了,但对于此演示来说是合适的,尤其是在您可能故意在 DevTools 中降低网络速度以进行实验时。
以下是如何使用它。当您单击加载更多图像的按钮时,代码如下所示:
startTransition(() => { setPage((p) => p 1); });
该状态更新将使用我的 GraphQL 客户端 micro-graphql-react 触发新的数据加载,该客户端与 Suspense 兼容,在查询正在进行时将为我们抛出一个 Promise。一旦数据返回,我们的组件将尝试渲染,并在我们的图像预加载时再次挂起。在所有这些事情发生的同时,我们的 isPending
值将为 true,这将允许我们在现有内容的顶部显示加载旋转器。
避免网络瀑布
您可能想知道 React 在图像预加载正在进行时如何阻止渲染。使用上面的代码,当我们这样做时:
{images.map((img) => (
……以及其中渲染的 <suspenseimage></suspenseimage>
,React 是否会尝试渲染第一张图像,挂起,然后重新尝试列表,超过第一张图像(现在在我们的缓存中),然后挂起第二张图像,然后是第三张、第四张等等。如果您之前阅读过关于 Suspense 的内容,您可能想知道我们是否需要在所有这些渲染发生之前手动预加载列表中的所有图像。
事实证明,无需担心,也无需进行尴尬的预加载,因为 React 对其在 Suspense 世界中渲染事物的方式相当聪明。当 React 遍历我们的组件树时,它不会在遇到挂起时停止。相反,它会继续尝试渲染我们组件树中的所有其他路径。因此,是的,当它尝试渲染图像 0 时,将发生挂起,但 React 将继续尝试渲染图像 1 到 N,然后才挂起。
您可以通过在加载新图像集时查看完整演示中的“网络”选项卡来查看此操作。您应该会看到整个图像桶立即显示在网络列表中,一个接一个地解析,并且完成后,结果应该显示在屏幕上。为了真正放大这种效果,您可能希望将网络速度降低到“快速 3G”。
为了好玩,我们可以通过在 React 尝试渲染我们的组件之前手动从我们的缓存中读取每个图像来强制 Suspense 遍历我们的图像,遍历组件树中的每条路径。
images.forEach((img) => imgCache.read(img));
我创建了一个演示来说明这一点。如果您同样在加载新图像集时查看“网络”选项卡,您将看到它们按顺序添加到网络列表中(但不要在降低网络速度的情况下运行此操作)。
延迟挂起
在使用 Suspense 时,需要记住一个推论:尽可能在渲染的后期和组件树的低层挂起。如果您有一些渲染一堆挂起图像的 <imagelist></imagelist>
,请确保每个图像都在其自己的组件中挂起,以便 React 可以单独访问它,这样就不会有任何图像阻塞其他图像,从而导致瀑布。
此规则的数据加载版本是,数据应尽可能由实际需要它的组件加载。这意味着我们应该避免在一个组件中执行以下操作:
const { data1 } = useSuspenseQuery(QUERY1, vars1); const { data2 } = useSuspenseQuery(QUERY2, vars2);
我们想要避免这种情况的原因是,查询一将挂起,然后是查询二,导致瀑布。如果这根本无法避免,我们将需要在挂起之前手动预加载这两个查询。
演示
这是我承诺的演示。它与我上面链接的演示相同。
打开演示如果您运行它并打开开发工具,请确保取消选中 DevTools 网络选项卡中显示的“禁用缓存”框,否则您将破坏整个演示。
该代码几乎与我之前显示的代码相同。演示中的一个改进是我们的缓存读取方法具有以下行:
setTimeout(() => resolve({}), 7000);
很好地预加载所有图像,但在现实生活中,我们可能不想无限期地阻止渲染,仅仅是因为一两张落后的图像加载缓慢。因此,在一段时间后,我们只需发出绿灯,即使图像尚未准备好。用户将看到一两张图像闪烁,但这比忍受软件冻结的挫败感要好。我还想指出,七秒钟可能过长了,但对于此演示,我假设用户可能会在 DevTools 中降低网络速度以更清晰地查看 Suspense 功能,并希望支持这一点。
该演示还有一个预缓存图像复选框。默认情况下选中它,但您可以取消选中它以使用普通的 <img alt="用反应悬念的预处理图像" >
标记替换 <suspenseimage></suspenseimage>
组件,如果您想将 Suspense 版本与“普通 React”进行比较(只是不要在结果出现时选中它,否则整个 UI 可能会挂起并渲染后备)。
最后,与 CodeSandbox 一样,某些状态有时可能会不同步,因此如果事情开始看起来奇怪或损坏,请点击刷新按钮。
杂项
在将此演示放在一起时,我意外地犯了一个巨大的错误。我不希望演示的多次运行会因为浏览器缓存它已经下载的图像而失去其效果。因此,我使用缓存破坏器手动修改所有 URL:
const [cacheBuster, setCacheBuster] = useState(INITIAL_TIME); const { data } = useSuspenseQuery(GET_IMAGES_QUERY, { page }); const images = data.allBooks.Books.map( (b) => b.smallImage `?cachebust=${cacheBuster}` );
INITIAL_TIME 在模块级别(即全局)使用以下行定义:
const INITIAL_TIME = new Date();
如果您想知道为什么我没有这样做:
const [cacheBuster, setCacheBuster] = useState( new Date());
……这是因为这会造成可怕的可怕后果。在第一次渲染时,图像尝试渲染。缓存导致挂起,React 取消渲染并显示我们的后备。当所有 Promise 都已解析后,React 将尝试重新进行初始渲染,并且我们的初始 useState 调用将重新运行,这意味着:
const [cacheBuster, setCacheBuster] = useState( new Date());
……将重新运行,并具有新的初始值,导致一组全新的图像 URL,这将再次无限期地挂起。组件将永远不会运行,并且 CodeSandbox 演示会停止运行(这使得调试变得令人沮丧)。
这似乎是一个由此特定演示的独特要求引起的奇怪的特殊问题,但有一个更大的教训:渲染应该是纯净的,没有副作用。React 应该能够多次尝试重新渲染您的组件,并且(给定相同的初始道具)应该从另一端获得相同的确切状态。
以上是用反应悬念的预处理图像的详细内容。更多信息请关注PHP中文网其他相关文章!

Netnewswire是2002年首次亮相的经典RSS应用程序之一。当它运行5.0时,我非常震惊,并于2019年8月开源!你可以正确地抓住它

最近,我开始使用Apple Pencil使用Procreate应用在iPad上绘制iPad。我喜欢这种方式绘画的灵活性。通常让我脱离的是什么


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

螳螂BT
Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

Dreamweaver Mac版
视觉化网页开发工具

ZendStudio 13.5.1 Mac
功能强大的PHP集成开发环境

MinGW - 适用于 Windows 的极简 GNU
这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

SublimeText3汉化版
中文版,非常好用