The execution of JS is usually in a single-threaded environment. When encountering time-consuming code, the first thing we think of is to divide the task so that it can be interrupted, and at the same time, give up the execution right when other tasks arrive. After execution, the rest of the computation is performed asynchronously, starting from the part that was interrupted before. So the key is to implement a set of asynchronous interruptible solutions. So how do we implement a solution that has task splitting, asynchronous execution, and gives up execution rights. React gives a corresponding solution.
background
React originated from Facebook's internal project to build Instagram's website and was open sourced in May 2013. The framework is mainly a JavaScript library for building user interfaces, mainly for building UI, which is unique for the front-end world of two-way data binding at that time. More uniquely, he introduced the mechanism of partial refresh in page refresh. There are many advantages. The main features of react after summarizing are as follows:
1.1 Transformation
The framework thinks that the UI just transforms the data into another form of data through the mapping relationship. The same input must have the same output. This happens to be a pure function.
1.2 Abstraction
In actual scenarios, only one function is needed to implement complex UI. Importantly, you need to abstract the UI into multiple hidden internal details, and you can use multiple functions. Implementing complex user interfaces by calling one function within another is called abstraction.
1.3 Combination
In order to achieve reusable features, then every combination, only create a new container for them. You also need "other abstract containers to compose again." That's two or more containers. The different abstractions are merged into one.
React's core value will always focus on the goal to do update . Combining the update with the ultimate user experience is what the React team has been working on.
slow down ==> upgrade
As the application becomes more and more complex, in the React15 architecture, the dom diff time exceeds 16.6ms, which may cause the page to freeze. So what are the factors that make react slow and need to be refactored.
In versions before React 15, the coordination process was synchronous, also known as stack reconciler, and because the execution of js was single-threaded, which resulted in the inability to respond to some high-priority tasks in time when updating time-consuming tasks, such as When a user enters a page while processing a time-consuming task, it will stutter. The reason why the page is stuck is likely to be caused by high CPU usage. For example, when rendering a React component, when making network requests, and when executing functions, the CPU will be occupied, and if the CPU usage is too high, it will feel blocked. How to solve this problem?
In our daily development, the execution of JS is usually in a single-threaded environment. When encountering time-consuming code, our first thought is to divide the task so that it can be interrupted, and when other tasks come Give up the right of execution, and when other tasks are executed, the rest of the calculations are asynchronously executed from the previously interrupted part. So the key is to implement a set of asynchronous interruptible solutions.
So how do we implement a solution that task division, asynchronous execution , and give up the execution right. React gives a corresponding solution.
2.1 Task division
How to perform the split task in a single thread, especially in react15, the update process is synchronous, we can't split it arbitrarily, so react provides a set of data structures that allow it to map both the real dom and the split unit. This leads us to our Fiber.
Fiber
Fiber is React's smallest unit of work, where everything is a component. On an HTML page, integrating multiple DOM elements together can be called a component, an HTML tag can be a component (HostComponent), and an ordinary text node can also be a component (HostText). Each component corresponds to a fiber node , and many fiber nodes are nested and associated with each other to form a fiber tree (why use a linked list structure: because the linked list structure is for space and time, it is very good for insertion and deletion operations. ) , as the relationship between the Fiber tree and the DOM represented below:
Fiber树 DOM树
div#root div#root
| |
<App/> div
| / \
div p a
/ ↖
/ ↖
p ----> <Child/>
|
a
A DOM node must have a fiber node node, but a fiber node has a very matching DOM node node. The structure of a fiber as a unit of work is as follows:
export type Fiber = {
// 识别 fiber 类型的标签。
tag: TypeOfWork,
// child 的唯一标识符。
key: null | string,
// 元素的值。类型,用于在协调 child 的过程中保存身份。
elementType: any,
// 与该 fiber 相关的已解决的 function / class。
type: any,
// 与该 fiber 相关的当前状态。
stateNode: any,
// fiber 剩余的字段
// 处理完这个问题后要返回的 fiber。
// 这实际上就是 parent。
// 它在概念上与堆栈帧的返回地址相同。
return: Fiber | null,
// 单链表树结构。
child: Fiber | null,
sibling: Fiber | null,
index: number,
// 最后一次用到连接该节点的引用。
ref:
| null
| (((handle: mixed) => void) & { _stringRef: ?string, ... })
| RefObject,
// 进入处理这个 fiber 的数据。Arguments、Props。
pendingProps: any, // 一旦我们重载标签,这种类型将更加具体。
memoizedProps: any, // 用来创建输出的道具。
// 一个状态更新和回调的队列。
updateQueue: mixed,
// 用来创建输出的状态
memoizedState: any,
mode: TypeOfMode,
// Effect
effectTag: SideEffectTag,
subtreeTag: SubtreeTag,
deletions: Array<Fiber> | null,
// 单链表的快速到下一个 fiber 的副作用。
nextEffect: Fiber | null,
// 在这个子树中,第一个和最后一个有副作用的 fiber。
// 这使得我们在复用这个 fiber 内所做的工作时,可以复用链表的一个片断。
firstEffect: Fiber | null,
lastEffect: Fiber | null,
// 这是一个 fiber 的集合版本。每个被更新的 fiber 最终都是成对的。
// 有些情况下,如果需要的话,我们可以清理这些成对的 fiber 来节省内存。
alternate: Fiber | null,
};
After understanding the structure of the fiber, how is the linked list tree linked between the fiber and the fiber? Here we introduce double buffer mechanism
The tree that is refreshed in the page to render the user interface, called current, is used to render the current user interface. Whenever there is an update, Fiber builds a workInProgress tree (in memory) created from the updated data in the React element. React performs work on this workInProgress tree and uses this updated tree on the next render. Once this workInProgress tree is rendered to the UI, it becomes the current tree.
2.2 Asynchronous execution of
So how is the fiber executed asynchronously by the time slice? It provides an idea. The example is as follows
let firstFiber
let nextFiber = firstFiber
let shouldYield = false
//firstFiber->firstChild->sibling
function performUnitOfWork(nextFiber){
//...
return nextFiber.next
}
function workLoop(deadline){
while(nextFiber && !shouldYield){
nextFiber = performUnitOfWork(nextFiber)
shouldYield = deadline.timeReaming < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
We know that the browser has an api called requestIdleCallback , which can perform some tasks when the browser is idle. We use this api to perform react updates, so that high-priority tasks respond first. For the requsetIdleCallback function, the following is the principle.
const temp = window.requestIdleCallback(callback[, options]);
For ordinary user interaction, the rendering time from the rendering of the previous frame to the next frame belongs to the idle time of the system. Input input, the fastest single-character input time is 33ms on average (triggered by pressing the same key continuously), which is equivalent to , there will be idle time greater than 16.4ms between the previous frame and the next frame, that is to say, for any discrete interaction, the minimum system idle time is also 16.4ms, that is to say, the shortest frame length of discrete interaction is generally 33ms.
The timing of the requestIdleCallback callback is executed in the idle time between the rendering of the previous frame after the callback registration is completed and the rendering of the next frame
callback is the callback function to be executed, and the deadline object will be passed in as a parameter. The deadline contains:
timeRemaining: Remaining time, in ms, refers to the remaining time of the frame.
didTimeout: Boolean, true indicates that the callback is not executed in this frame, and it times out.
There is an important parameter timeout in options. If the timeout is given, the callback will be executed immediately when the time is up, regardless of whether there is any remaining time.
callback。
But the fact is that requestIdleCallback has browser compatibility and trigger instability , so we need to use js to implement a time slice running mechanism, which is called scheduler in react. At the same time, the React team has not seen any browser manufacturers actively promoting the process of covering requestIdleCallback, so React can only use a partial hack polyfill solution.
requestIdleCallback polyfill scheme (Scheduler)
The problem with requestIdleCallback is mentioned above. The time slice running mechanism implemented in react is called scheduler. The premise of understanding time slice is to understand the whole process of page rendering in general scenarios, which is called a frame, and a complete process of browser rendering is roughly
Execute JS--->Calculate Style--->Build Layout Model (Layout)--->Draw Layer Style (Paint)--->Composite Calculation Rendering Result (Composite)
* Features of frame : *
The rendering process of the frame is after the JS execution process or after an event loop
The rendering of the frame is handled in a separate UI thread, and there is also the GPU thread, which is used to draw the 3D view
The rendering of the frame and the update rendering of the frame are asynchronous processes, because the screen refresh rate is a fixed refresh rate, usually 60 times/second, that is to say, the time to render a frame should be as low as 16.6 milliseconds as much as possible. In some high-frequency interactive actions, there will be frame loss and stalling, which is caused by the asynchronous frame rendering and refresh rate.
The usual user interaction does not require the rendering time of a frame to be less than 16.6 milliseconds, but it also needs to follow the Google's RAIL model.
So how does the Polyfill scheme control the execution of tasks within a fixed number of frames? Basically, it uses requestAnimationFrame to control a batch of flat tasks to execute within a time slice of 33ms.
Lane
The above is our asynchronous scheduling strategy, but only asynchronous scheduling, how do we determine which tasks should be scheduled, which tasks should be scheduled first, and which should be scheduled later, which leads to Lane similar to microtasks and macrotasks
With asynchronous scheduling, we also need to manage the priority of each task in a fine-grained manner, so that high-priority tasks are executed first, each Fiber work unit can also compare priorities, and tasks of the same priority can be updated together
For the design of the lane, you can read this:
https://github.com/facebook/react/pull/18796github.com/facebook/react/pull/18796
Application scenarios
With the asynchronous interruptible allocation mechanism described above, we can implement a series of operations such as batchUpdates batch updates:
Before updating fiber
After updating fiber
In addition to the CPU bottleneck problem above, there is also a class of problems related to side effects, such as data acquisition and file operations. Different devices have different performance and network conditions. How does react deal with these side effects, so that we can best practice when coding and run the application consistently, which requires react to have the ability to separate side effects.
design serve computer
We have all written code to obtain data, display loading before data is obtained, and cancel loading after data is obtained. Assuming that our device performance and network conditions are good, and the data will be obtained soon, then we still need to start at the beginning. Time to show loading? How can we have a better user experience?
Take a look at the example below
function getSomething(id) {
return fetch(`${host}?id=${id}`).then((res)=>{
return res.param
})
}
async function getTotalSomething(id1, id2) {
const p1 = await getSomething(id1);
const p2 = await getSomething(id2);
return p1 + p2;
}
async function bundle(){
await getTotalSomething('001', '002');
}
We can usually use async+await to obtain data, but this will cause the calling method to become an asynchronous function, which is the characteristic of async and cannot separate side effects.
To separate side effects, refer to the code below
function useSomething(id) {
useEffect((id)=>{
fetch(`${host}?id=${id}`).then((res)=>{
return res.param
})
}, [])
}
function TotalSomething({id1, id2}) {
const p1 = useSomething(id1);
const p2 = useSomething(id2);
return <TotalSomething props={...}>
}
This is the ability of hooks to decouple side effects.
Decoupling side effects is very common in the practice of functional programming, such as redux-saga, which separates side effects from saga, does not handle side effects by itself, and is only responsible for initiating requests.
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
Strictly speaking, react does not support Algebraic Effects, but after performing the update with the help of fiber, it returns the execution right to the browser, allowing the browser to decide how to schedule later. Suspense is also an extension of this concept.
const ProductResource = createResource(fetchProduct);
const Proeuct = (props) => {
const p = ProductResource.read( // 用同步的方式来编写异步代码!
props.id
);
return <h3>{p.price}</h3>;
}
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Proeuct id={123} />
</Suspense>
</div>
);
}
It can be seen that ProductResource.read is written synchronously, and the part of obtaining data is separated from the Product component. The principle is that ProductResource.read will throw a special Promise before obtaining data. Due to the existence of scheduler, scheduler can capture this promise , suspend the update, and return the execution right after the data is obtained. The ProductResource here can be localStorage or even databases such as redis and mysql. This is the prototype of the server component as I understand it.
As the core source content of react16.5+ version, this article analyzes the mechanism of asynchronous scheduling and allocation, and understanding the principle will enable us to have a better overall view in the case of system design and model construction. It also has a certain auxiliary role for the design of more complex business scenarios. This is just the first article of the react source code series, and it will continue to be updated in the future. I hope it can help you.
happy hacking~~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。