react 使用缓存
by Marvin Frachet
由Marvin Frachet
Well, this year seems to be the year of React. You’ve probably heard of the new killer feature that is coming with the 16.7-alpha.0 — Hooks. You’ve probably also heard about some other great and cool stuff like Time Slicing or even Suspense.
好吧,今年似乎是React的一年。 您可能已经听说过16.7-alpha.0附带的新杀手级功能-挂钩。 您可能还听说过其他一些很棒的东西,例如“时间片”或“暂停”。
This article doesn’t aim at describing how to use some of the new features but rather at proving how they may have been built. Just for the sake of understanding what we are playing with.
本文的目的不是描述 如何使用某些新功能,而不是证明它们是如何构建的。 只是为了了解我们在玩什么。
It’s also written in the way I have discovered the feature. It’s probably not the way it has been thought up, but this is how I got the points.
它也是按照我发现该功能的方式编写的。 这可能不是经过深思熟虑的方式,但这就是我的观点。
What you’ll find while reading:
阅读时会发现:
What made me want to write this post was this special, and experimental, feature that allows the use of asynchronous operations using a synchronous API:
让我想要写这篇文章的是这项特殊的,实验性的功能,它允许通过同步 API使用异步操作:
const bulbasaur = ApiResource.read()
?… What the? Synchronous?!
const bulbasaur = ApiResource.read()
吗?……什么? 同步吗?
The react-cache library creates the ability to use asynchronous operations with a synchronous API. This is the feature that made me want to learn how React is working under the hood. Here’s a presentation by presented by Dan Abramov and Andrew Clark on this library:
react-cache库创建了将异步操作与同步API结合使用的功能。 这就是让我想了解React如何在幕后工作的功能。 这是Dan Abramov和Andrew Clark在此库上的演示文稿:
How is that even possible? How can we get some remote data using synchronous calls?
这怎么可能呢? 我们如何使用同步调用获取一些远程数据?
Let’s deep dive into this example and try to understand how react-cache implements such a functionality and discover how it can work. This story starts with the fiber architecture.
让我们深入研究此示例,并尝试了解react-cache如何实现这种功能并发现其功能。 这个故事始于光纤架构 。
Fiber architecture allows React to take control over task executions. It has been built to solve multiple problems that React suffered from. Here are the two that caught my attention:
光纤架构使React可以控制任务执行。 它是为解决React所遇到的多个问题而构建的。 这是引起我注意的两个:
Everything that triggers a state change — not only with React — inside a JavaScript application is due to asynchronous operations. These include setTimeout
, fetch
, and listeners for events.
JavaScript应用程序内部触发状态更改的所有原因(不仅是通过React触发)都是由于异步操作。 其中包括setTimeout
, fetch
和事件的侦听器。
Asynchronous operations are managed through multiple JavaScript core concepts:
异步操作通过多个JavaScript核心概念进行管理:
If you’re not familiar with these concepts, I suggest you take a look at this video by Jake Archibald:
如果您不熟悉这些概念,建议您看一下Jake Archibald的这段视频:
Thanks to fiber, user inputs are resolved before other asynchronous operations such as fetch calls.
借助光纤,用户输入可以在其他异步操作(例如访存调用)之前得到解析 。
How is this even possible?
这怎么可能?
Well, Archibald’s talk above was the first paved stone of my own path of learning about how event loop works. He says that micro tasks — generated through the Promise API, for example — are executed and flushed before the next macro task. This process uses callback-based methods like setTimeout
.
嗯,阿奇博尔德的上述演讲是我自己关于事件循环如何工作的研究之路的第一个铺路石。 他说,例如,通过Promise API生成的微任务在下一个宏任务之前就已执行并清除。 此过程使用基于回调的方法,如setTimeout
。
So, if you remember my “user input versus fetching data” comparison, how did the team make fetch
resolutions after onChange
resolutions?
因此,如果您还记得我的“用户输入与获取数据”的比较,那么团队是如何在 onChange
解析度之后制定fetch
解析度的?
None of these concepts fit in the same spec, WhatWG / HTML5 / Ecma-262, and are provided from different places like the browser or JS engines.
这些概念都不适合同一规范WhatWG / HTML5 / Ecma-262 ,并且是从不同的位置(例如浏览器或JS引擎)提供的。
I mean, how are we supposed to resolve a Promise
after a setTimeout
?
我的意思是,我们应该如何在setTimeout
之后解决Promise
?
This sounded absolutely crazy to me and it was really hard to get an idea of how it could be working. The fact is that it takes place in a higher level.
这听起来对我来说绝对是疯狂的,并且真的很难知道它是如何工作的。 事实是它发生在更高的层次上。
Later, I watched the incredible talk from Brandon Dail at React Rally. This presents the new Time Slicing and Suspense features that have been shipped thanks to the React fiber architecture:
后来,我观看了React Rally上Brandon Dail的精彩演讲。 这展示了由于React光纤架构而带来的新的时间分片和暂挂功能 :
According to Dail, fiber is just like the usual JavaScript callstack where each item in the stack is called a fiber. It is different to the callstack that relies on frames which represent functions (+ metadata). Rather, a fiber represents a component (+ metadata). Let’s see a fiber as a huge box around a component that knows everything about it.
根据Dail的说法,fibre就像通常JavaScript调用堆栈一样,其中堆栈中的每个项目都称为fibre 。 它与依赖表示功能(+元数据)的 框架的调用堆栈不同。 相反,纤维代表 组件(+元数据) 。 让我们将光纤视为一个巨大的盒子,围绕它了解所有组件。
There is an important difference between these two concepts.
这两个概念之间存在重要区别。
On the first hand, the callstack is a functionality that has been built on top of the native part driving JavaScript Code. It aims to stack every JavaScript function call, and run them on their own. Each time we call a function it’s added to the stack. Without the callstack, we wouldn’t be able to have clear and detailed error stacktraces. And since the callstack is not reachable from a JavaScript code, it’s really difficult and even impossible to take control over it.
首先,调用栈是一项功能,它是在驱动 JavaScript代码的本机部分之上构建的。 它旨在堆叠每个JavaScript函数调用,并自行运行它们。 每次我们调用一个函数时,它都会被添加到堆栈中。 没有调用堆栈,我们将无法获得清晰而详细的错误堆栈跟踪。 而且由于无法从JavaScript代码访问调用堆栈,因此对其进行控制确实非常困难,甚至不可能。
On the other hand, fibers — like a stack of fiber — represent the same concept, but built in JavaScript code. The tiniest unit is not functions, but a component. It actually runs in a JavaScript universe.
另一方面,纤维(就像一堆纤维)代表相同的概念,但是内置在JavaScript代码中。 最小的单元不是功能,而是组件。 它实际上在JavaScript Universe中运行。
The fact that the fiber architecture is completely built in JavaScript means that we can use it, access it, and modify it. We can work on it using standard JavaScript.
光纤体系结构完全用JavaScript构建的事实意味着我们可以使用,访问和修改它。 我们可以使用标准JavaScript对其进行处理。
What has driven me in the wrong direction was that I thought React was using a workaround to cut-off the internal way JavaScript is working. It’s not the case. Fibers are simply JavaScript objects that own information about React components and that can interact with their lifecycles. It can only act on React internal functionalities.
导致我朝错误方向前进的原因是,我认为React在使用一种替代方法来切断JavaScript工作的内部方式。 事实并非如此 。 光纤只是拥有关于React组件的信息并可以与其生命周期进行交互JavaScript对象。 它只能作用于React内部功能。
The idea is not to redefine how JavaScript should be working, like telling that fetch
microtask resolution should be executed before callback tasks. It’s more on which React methods should be called or not in a specific context, like interrupting the different lifecycle method calls.
这个想法不是要重新定义JavaScript的工作方式,就像告诉回调任务之前应该执行fetch
微任务解析。 在特定的上下文中, 应该调用或不调用 React方法更多,例如中断不同的生命周期方法调用。
Hey wait! You say that fibers can control absolutely everything in a React App? But how can a component tell React to stop doing anything?
嘿,等等! 您说光纤可以完全控制React App中的所有内容吗? 但是组件如何告诉React停止做任何事情?
React is able to control components, and to know if a component is running, thanks to the fiber architecture. What is missing now is a way to tell React that something has changed for a specific component, so it will handle this change.
由于采用了光纤架构,React能够控制组件并知道组件是否正在运行。 现在缺少的是一种告诉React特定组件已发生更改的方法,因此它将处理此更改。
This is where algebraic effects enter the game.
这就是代数效应进入游戏的地方。
Algebraic effects are not something that exist in JavaScript. I will try to explain what they are with a higher level explanation.
代数效应在JavaScript中不存在。 我将尝试通过更高层次的解释来解释它们的含义。
Algebraic effects are a concept that allows to send some information somewhere, a bit like a dispatcher. The idea is to call a specific function that will interrupt the currently running function at a precise position to let a parent function handle a computation. When the parent computation finishes, it can resume the program to the initial position where the information has been sent.
代数效应是一种概念,它允许将信息发送到某个地方,有点像调度程序。 想法是调用一个特定的函数,该函数将在精确位置中断当前正在运行的函数,以使父函数处理计算。 当父计算完成时,它可以将程序恢复到已发送信息的初始位置。
Some languages such as OCaml or Eff benefit from these feature natively. This is a really interesting abstraction since the implementation details only rely upon the parent:
诸如OCaml或Eff之类的某些语言本身就从这些功能中受益。 这是一个非常有趣的抽象,因为实现细节仅依赖于父对象:
Wouldn’t it be awesome to have such a feature in JavaScript?
在JavaScript中拥有这样的功能会很棒吗?
The React team has created a similar approach in a React context dealing with the JavaScript try/catch
block. According to Dail, it’s the closest available concept in JavaScript.
React团队在React上下文中创建了一种类似的方法来处理JavaScript try/catch
块。 根据Dail的说法,它是JavaScript中最接近的可用概念。
Throwing something allows sending information to a parent, somewhere. The first parent who catches the information is able to deal with it and make computations on it.
扔东西可以将信息发送到某个地方的父母。 捕获信息的第一个父母能够处理该信息并对其进行计算。
Imagine the following code that tries to fetch Bulbasaur using a synchronous API:
想象以下尝试使用同步API提取Bulbasaur的代码:
This piece of code may be weird since it’s not really common to fetch data using a synchronous API. Let’s jump inside the customFetch
function implementation:
这段代码可能很奇怪,因为使用同步API来获取数据并不常见。 让我们跳入customFetch
函数实现:
Oh wait! This absolutely doesn’t look like a fetch! I don’t get what this function aims to do at all…
等一下! 这绝对看起来不像是获取! 我完全不知道此功能的目的是什么…
Well, imagine something around the component, let’s say a fiber that looks like:
好吧,想象一下组件周围的东西,假设纤维看起来像:
Take some time to read the code.
花一些时间阅读代码。
Now, let’s jump to the customFetch
implementation:
现在,让我们跳到customFetch
实现:
The important part in the previous snippets is the try/catch
block.
前面的摘录中的重要部分是try/catch
块。
Let’s sum up what’s happening through these different pieces of code:
让我们总结一下这些不同的代码段中正在发生的事情:
The Pokemon
component calls the customFetch
method.
Pokemon
组件调用customFetch
方法。
The customFetch
method tries to read its internal cache, but it’s empty. So it throws something / somewhere — algebraic effects.
customFetch
方法尝试读取其内部缓存,但它为空。 因此,它会在某处/某处抛出—代数效应。
The fiber
parent catches that information, handles it, and fetches the data. Then it populates the customFetch
cache with the data.
fiber
父级会捕获该信息,进行处理,然后获取数据。 然后,使用数据填充customFetch
缓存。
A re-render occurs in Component(args)
and, now, the customFetch
cache is full. The data is now available in the component using a synchronous API.
重新渲染发生在Component(args)
并且现在customFetch
缓存已满。 现在,可以使用同步API在组件中使用数据。
Take a look at the react-cache
implementation details and check the different throws.
Something may have caught your attention during this process: render
has been called twice. One for throwing the error — pausing the component — and one for getting the data — resuming the component. It’s okay with React to trigger multiple render
calls since it’s solely a pure function — it doesn’t have any side effects on its own.
在此过程中可能引起了您的注意: render
已被调用两次。 一种用于引发错误( 暂停组件),另一种用于获取数据( 恢复组件)。 React可以触发多个render
调用,因为它只是一个纯函数-它本身没有任何副作用。
Wait… What? render
doesn’t have any side effects? What about the DOM?
等等...什么? render
没有任何副作用? DOM呢?
If you’ve been working with React for a long time, you may have heard that it’s not a good practice to re-render multiple times. Before fiber architecture, every time we were calling the render function React was making some internal computations and then modified the DOM accordingly. For example, this happened when calling the render function through setState
. The process was inlined:
如果您使用React已有很长时间,您可能听说过多次重新渲染不是一个好习惯。 在光纤架构之前,每次我们调用渲染函数React都会进行一些内部计算,然后相应地修改DOM。 例如,这是通过setState
调用render函数时发生的。 内联过程:
setState
→render
→ compare Virtual Nodes → update the DOM nodes
setState
→ render
→比较虚拟节点→更新DOM节点
Dealing with fiber, the process is a bit different. It has introduced a concept of queue and batches that allows high performance DOM modifications.
处理光纤时,过程有所不同。 它引入了队列和批处理的概念,该概念允许进行高性能DOM修改。
The idea is quite simple. We assume that screens can run ~60 frames per second. From this assumption, and using the available JavaScript functions, it’s possible to make some computations and DOM modifications only every ~16.7ms. With fiber, React can enqueue multiple modifications and commit them about 60 times per second.
这个想法很简单。 我们假设屏幕每秒可以运行约60帧。 基于此假设,并使用可用JavaScript函数,可能仅每〜16.7ms进行一些计算和DOM修改。 使用光纤,React可以加入多个修改并每秒提交约60次。
This kind of modification has allowed React to split into three phases with their own advantages and particularities:
这种修改使React可以根据自身的优势和特殊性分为三个阶段:
The render phase is pure and deterministic. It has no side effects and the different functions that it’s composed of can be called multiple times. The render phase is interruptible — it’s not the render
function that is in pause mode, but the whole phase
渲染阶段是纯的和确定的。 它没有副作用,并且其组成的不同功能可以多次调用。 渲染阶段是可中断的 -处于暂停模式的不是render
函数,而是整个阶段
The commit phase actually modifies the DOM and is not interruptible. React can’t pause during that phase.
提交阶段实际上是在修改DOM, 并且不可中断 。 在该阶段,React不能暂停。
This set of three phases has introduced the Time Slicing capabilities. React is able to pause during the render phase, in between two component function calls, and to resume that phase when necessary.
这三个阶段的集合介绍了时间分片功能。 在渲染阶段,React能够在两个组件函数调用之间暂停,并在必要时恢复该阶段。
In fiber, render
only aims to get the latest available representation of a component based on its internal state to make some comparisons and know if React has to change the DOM or not. If a commit modification is required, it will add the modification to a “work in progress” queue.
在光纤中, render
仅旨在基于组件的内部状态获取组件的最新可用表示形式 ,以进行一些比较,并了解React是否必须更改DOM。 如果需要提交修改,它将把修改添加到“正在进行的工作”队列中。
The React team has made huge performance improvements thanks to React Concurrent (Time Slicing + Suspense) and the fiber architecture. They have created workarounds to counter different browser problems like event prioritisation and concurrency.
由于使用React Concurrent(时间分片+暂挂)和光纤体系结构,React团队实现了巨大的性能提升。 他们创建了变通办法来应对不同的浏览器问题,例如事件优先级和并发性。
If we take a step back, isn’t that what they have shown? Prioritisation seems to be the new challenge for browser and front-end frameworks.
如果我们退后一步,那不是他们所显示的吗? 优先级似乎是浏览器和前端框架的新挑战。
Other teams are also working on improving the actual state of the art and even propose future APIs. This is Google’s take:
其他团队也在努力改善实际水平,甚至提出未来的API。 这是Google的看法:
react 使用缓存