This paper focuses on introducing the cause and purpose of React refactoring, understanding the meaning of each attribute in Fiber Tree unidirectional linked list structure, sorting out the scheduling process and core implementation means, and getting into the usage and principle of new life cycle, hooks, Suspense, exception capture and other features.
Like to click a like ️, I hope to explore the fun of learning with you in the boring source code, share progress together.
When the react just launch the most revolutionary feature is virtual dom, because it greatly reduced the difficulty of the application development, compared with previous tell the browser what do I need to update my UI, now we only need to tell the react me what is the status of the application UI next, will help us to automate the react all matters between the two.
This frees us from the property manipulation, event handling, and manual DOM updates that are necessary when building the application. The host tree concept makes this excellent framework infinitely possible, and React Native is a great implementation of it in native mobile applications.
But while we enjoyed the comfortable development experience, there were a few questions that were always on our minds:
- What causes react user interaction and animation to stagnate
- How to see elegant exception handling, exception catching and alternate UI rendering
- How to better achieve component reuse and state management
Is this a distortion of human nature, or moral decay/dog head
Can Fiber give us the answer, and what surprises are in store for us? Welcome to Step into Fiber.
So, in a nutshell, what is React Fiber?
Fiber is a reconstruction of the React core algorithm, and the result of the two-year reconstruction is Fiber Reconciler.
What is react coordination
Coordination is an important part of React, including how to compare the differences between old and new trees to update only the differences.
After reconstruction, React is divided into two different stages: Reconciliation and Rendering.
- In the Reconciler phase, React creates two different virtual trees during the component’s secondary initialization and subsequent state updates. Based on the differences between the two trees, React needs to determine how effectively to update the UI to keep the current UI in sync with the latest tree and calculate which parts of the tree need to be updated.
- Renderer stage: The renderer is responsible for taking the information from the virtual component tree and updating the rendering into the application according to its corresponding environment. React = Renderer. The React Renderer, such as The React Dom and React Native Renderer, can generate different instances based on the main environment.
Why rewrite coordination
Animation refers to many frames of static picture, with a certain speed (such as 16 per second) continuous playback, the naked eye due to visual residual image illusion, and mistakenly think the picture of the work of activity. — Wikipedia
The older generation often referred to movies as “moving paintings,” and the flip books we saw when we were kids were pages that were flipped quickly, essentially the same way animation works.
Frame: In the animation process, each still picture is a “frame”; Frame rate: A measure of the number of frames displayed, in Frame per Second (FPS) or Hertz. Frame duration: the dwell time of each still image, usually in ms(ms); Frame loss: In an animation with a fixed frame rate, the duration of one frame is longer than the average frame length, resulting in subsequent frames being squeezed and lost;
The current common frame rate for most laptops and mobile phones is 60Hz, that is, 60 frames per second and the duration of a frame is 16.7ms(1000/60≈16.7), which leaves the developer and UI system around 16.67ms to do all the work needed to generate a single still image (frame). If this is not done within the allocated 16.67ms, the result will be ‘frame loss’ and the interface will not behave smoothly.
GUI rendering thread and JS engine thread in the browser
The GUI rendering thread and the JS engine thread are mutually exclusive in the browser, the GUI thread is suspended (frozen) while the JS engine is executing, and GUI updates are stored in a queue until the JS engine is idle.
React16’s reconciling algorithm before Fiber was Stack Reconciler, which iterates recursively through all Virtual DOM nodes to implement the Diff algorithm, which cannot be interrupted once it starts and does not release the main thread until the entire Virtual DOM tree is constructed. Due to the single-threaded nature of JavaScript, diff can clog UI processes with complex nesting and logical processing of current components, preventing relatively high-priority tasks such as animation and interaction from being handled immediately, causing pages to stagger and drop frames, affecting the user experience.
Seb officially mentioned Fiber on Facebook in 2016, explaining why the framework was being rewritten:
Once you have each stack frame as an object on the heap you can do clever things like reusing it during future updates and yielding to the event loop without losing any of your currently in progress data. Once you have each stack frame as an object on the heap, you can do smart things like reuse it in future updates and pause the event loop without losing any data currently in progress.
Let’s do an experiment
function randomHexColor() {
return (
"#" + ("0000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)
);
}
var root = document.getElementById("root"); // Walk 100000 times oncefunction a() {
setTimeout(function() {
var k = 0;
for (var i = 0; i < 10000; i++) {
k += new Date() - 0;
var el = document.createElement("div");
el.innerHTML = k;
root.appendChild(el);
el.style.cssText = `background:${randomHexColor()}; height:40px`; }}, 1000); } // Perform 100 operations on 100 nodes at a timefunction b() {
setTimeout(function() {
function loop(n) {
var k = 0;
console.log(n);
for (var i = 0; i < 100; i++) {
k += new Date() - 0;
var el = document.createElement("div");
el.innerHTML = k;
root.appendChild(el);
el.style.cssText = `background:${randomHexColor()}; height:40px`; }if (n) {
setTimeout(function() {
loop(n - 1);
}, 40);
}
}
loop(100);
}, 1000);
}
Copy the code
A Execution performance screenshot: The FPS is 1139.6ms
B performance snapshot: FPS ranges from 15ms to 19ms
The reason for this is that the main thread of the browser handles GUI rendering, timer handling, event handling, JS execution, remote resource loading, etc., and when you do one thing, you have to do it before you can move on to the next. If we have enough time, the browser will JIT and hot optimize our code, and some DOM operations will be handled internally with reflow. Reflow is a performance black hole, and most elements of the page are likely to be rearranged.
As a dream front-end dish 🐤, it is our bounden responsibility to provide the best interactive experience for users’ fathers. Put the difficulties on our shoulders and let’s see see React how to solve the above problems.
Fiber, what are you
So let’s first look at what Fiber is as a solution, and then analyze why it can solve the above problems.
Definition:
- React Reconciliation is a re-implementation of the core algorithm
- Virtual stack frame
- Js objects with a flat linked list data store structure, the smallest unit of work that can be split in the Reconciliation phase
For its definition, let’s expand:
Virtual stack frame:
Andrew Clark’s React Fiber architecture document nicely explains the thinking behind Fiber’s implementation, and I’ll quote it here:
Fiber is a reimplementation of the stack, specifically for the React component. You can think of a single Fiber as a virtual stack frame. The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them as needed (and at any time). This is critical to achieving scheduling goals.
JavaScript execution model: Call Stack
JavaScript native execution model: Function execution state is managed through the call stack. Each stack frame represents a unit of work and stores the return pointer of a function call, the current function, call parameters, local variables and other information. Because the execution stack of JavaScript is managed by the engine, once the execution stack is started, it continues to execute until the execution stack is empty. Unable to abort on demand.
React used to use native execution stacks to manage the recursive rendering of the component tree. When deep components recurred to child nodes and could not be interrupted, the main thread clogged up the UI.
Controllable call stack
Ideally, the reconciliation process would be like the one shown below, where heavy tasks are broken up into small units of work that leave you gasp for breath. We need a scheduling of incremental renderings, and Fiber is re-implementing a scheduling of stack frames that can execute them according to their own scheduling algorithm. In addition, because these stacks can divide interruptible tasks into multiple sub-tasks, the previous synchronous rendering can be changed to asynchronous rendering by scheduling sub-tasks according to their priorities and segmenting update.
Its features are time slicing and supense.
Js object with flat linked list data storage structure:
Fiber is a JS object, and fiber is created using the React element. In the React virtual DOM tree, each element has a fiber, thus creating a fiber tree. Each fiber contains not only information about each element but also more information. To facilitate Scheduler for scheduling.
Let’s take a look at the structure of fiber
typeFiber = {| / / / / mark different component typeexport const FunctionComponent = 0;
//export const ClassComponent = 1;
//exportConst HostRoot = 3; This fiber is the root node of the fiber tree. The root node can be nested in the subtreeexport const Fragment = 7;
//export const SuspenseComponent = 13;
//export const MemoComponent = 14;
//exportconst LazyComponent = 16; Tag: WorkTag, // ReactElement key // unique identifier. When we write React, we need to specify the key of the corresponding element if there is a list. Key: null | string, / / ReactElement type, or what we call ` createElement method ` The first parameter to The elementType: any, / / The resolvedfunction// async module resolvedfunction` or ` class `type: any,
// The localState associated with this fiber. // Local state associated with this fiber (e.g. browser environment is DOM node) // reference to the current component instance stateNode: Any, // points to 'parent' in the Fiber tree to return up after processing the nodereturn: Fiber | null, / / / / singly linked lists the tree structure to her first child node child: Fiber | null, / / to his brother/brother/node structurereturnPoint to the same parent node (: Fiber | null, index: number, / / ref attribute ref: null | (((handle: mixed) = > void) & {_stringRef:? String}) | RefObject, / / the new changes brought by the new props pendingProps: any, / / the last rendering done props memoizedProps: Any, // The Update generated by the Fiber component is stored in this queue: UpdateQueue < any > | null, / / the last rendering state / / used to store all state of Hook memoizedState within a component: Any, // a list of fiber-dependent Context firstContextDependency: ContextDependency<mixed> | null, // The mode used to describe the current 'Bitfield' of Fiber and its subtree indicates whether the subtree is rendered asynchronously by default. // When Fiber is created, it inherits the parent Fiber. // Other flags can also be set during creation, but should not be changed after creation. In particular, his sub-fiber was created before // to describe what mode Fiber was in. // This field is actually a number. NoContext: 0b000->0//AsyncMode: 0b001->1//StrictMode: 0b010->2 TypeOfMode, // Effect // Is used to record the specific type of work performed by Side Effect: Placement, Update, etc. EffectTag: SideEffectTag, / / singly linked list is used to quickly find the next side effect nextEffect: Fiber | null, / / subtree first side effect firstEffect: Fiber | null, / / the last one in the subtree side effect lastEffect: Fiber | null, / / representative task which point in time in the future should be done / / not including his subtrees generated task / / by this suspension parameters can also know whether there is a waiting for change, not complete change. // This parameter is generally the same as the Update with the longest expiration time, if any. ChildExpirationTime: expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime // In the process of updating the Fiber tree, each Fiber will have a corresponding Fiber // we call it current <==> workInProgress // After rendering they will switch alternate positions: Fiber | null, ... |};Copy the code
ReactWorkTags Component type
Chain table structure
React16 particularly favors linked lists, which are not contiguous in memory, dynamically allocated, easy to add and delete, lightweight, and asynchronous friendly
Current and workInProgress
The current tree: React creates a Virtual DOM Tree using the React. CreateElement method. Fiber is added to the Tree to record context information. Each Element corresponds to a Fiber Node, and the structure linking Fiber nodes is called Fiber Tree. It reflects the state used to render the UI and map the application. This tree is often referred to as the current tree (the current tree, which records the state of the current page).
WorkInProgress tree: When React passes through the current tree, it creates an alternate node for each pre-existing Fiber node that makes up the workInProgress tree. This node is created using the React element data returned by the Render method. Once the update is processed and all related work is done, React has an alternate tree ready to refresh the screen. Once the workInProgress tree is rendered on the screen, it becomes the current tree. Next time, current state will be copied to WIP for interactive reuse, instead of creating a new object every time update, which will consume performance. This technique of caching two trees simultaneously for reference substitution is called double buffering.
function createWorkInProgress(current, ...) {
let workInProgress = current.alternate;
if(workInProgress === null) { workInProgress = createFiber(...) ; }... workInProgress.alternate = current; current.alternate = workInProgress; .return workInProgress;
}
Copy the code
alternate
Dan used a very appropriate analogy in his talk Beyond React 16: Git branches. You can think of a WIP tree as a branch that forks out of an old tree. If you add or remove features to a new branch, the old branch will not be affected if you make a mistake. When your branch is tested and refined, merge it into the old branch and replace it.
Update
- Used to record changes in component state
- Stored in fiber update Ue
- Multiple updates exist simultaneously
For example, if you set three setstates (), React will not update immediately. Instead, it will update in UpdateQueue
Ps: setState has been questioned for some time as to why it is not synchronous, treating setState() as a request rather than a command to update the component immediately. React delays calls to it for better performance awareness, and then updates multiple components at once. React does not guarantee that state changes will take effect immediately.
export function createUpdate(
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
): Update<*> {
letUpdate: update <*> = {// Task expiration event // When creating each update, you need to set the expiration time, which is also the priority. The longer the expiration time, the lower the priority. Suspense time, // Suspense time, //exportconst UpdateState = 0; Update State //exportconst ReplaceState = 1; Replace State //exportconst ForceUpdate = 2; Force update //exportconst CaptureUpdate = 3; Catch updates (when an exception error occurs) // Specify the type of update, with values of one of the above tags: UpdateState, // Update content, such as'setState 'Payload: null, // callback after update,'setCallback: null; // State ', 'render', callback: null; Null, // Next side effect //next replace //nextEffect: null,};if (__DEV__) {
update.priority = getCurrentPriorityLevel();
}
return update;
}
Copy the code
UpdateQueue
// Create an update queueexport functioncreateUpdateQueue<State>(baseState: State): UpdateQueue<State> { const queue: UpdateQueue<State> = {// apply the updated State baseState, // firstUpdate in the queue firstUpdate: null, // lastUpdate in queue lastUpdate: Null, // Update lastCapturedUpdate: null, // Update lastCapturedUpdate: // Side effect lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, };return queue;
}
Copy the code
Payload in update: Normally we pass in an object when we call setState, but when we use Fiber conciler, we have to pass in a function that returns the state to update. React has supported this approach since early versions, but it’s generally not used. In later versions of React, direct object writing may be deprecated.
setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler
Copy the code
ReactUpdateQueue source
Updater
Each component has a Updater object that associates component element updates with the corresponding Fiber. The scheduler calls the ScheduleWork method to scheduler to schedule the latest fiber.
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
const update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
if(callback ! == undefined && callback ! == null) {if (__DEV__) {
warnOnInvalidCallback(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, enqueueReplaceState(inst, payload, callback) { update.tag = ReplaceState; / /... }, enqueueForceUpdate(inst, callback) {// Same code update.tag = ForceUpdate; / /... }};Copy the code
ReactUpdateQueue=>classComponentUpdater
Effect list
Side Effects: We can think of a component in React as a function that uses state and props to evaluate the UI. Every other activity, such as changing the DOM or calling lifecycle methods, should be considered side-effects, as described in the React documentation:
You’ve likely performed data fetching, subscriptions, or manually changing the DOM 的from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.
React is very fast to update, and it uses some interesting technology to achieve high performance. One of them is to build a linear list of fiber nodes with side-effects, which have the effect of rapid iteration. Iterative linear lists are much faster than trees, and there is no need to spend time on nodes without Side effects.
Each Fiber node can have effects associated with it, represented by the effectTag field in the Fiber node.
The goal of this list is to tag nodes that have DOM updates or other effects associated with them, this list is a subset of the WIP Tree, and is linked using the nextEffect attribute rather than the Child attribute used in the Current and workInProgress trees.
How it work
The core target
- Break up interruptible work into smaller tasks
- Assign task priorities to different types of updates
- Ability to pause, terminate, and reuse rendering tasks when updating
Update Process Overview
Let’s take a look at the Fiber update process and then expand on the core technologies in the process.
Reconciliation has two stages: Reconciliation and Commit
reconciliation
- The first part starts with the reactdom.render () method, which converts the received React Element to a Fiber node, sets its priority, records updates, etc. This part is mainly the preparation of some data.
- The second part is mainly three functions: scheduleWork, requestWork, performWork, that is, arrange work, apply for work, formal work trilogy. React 16’s new asynchronous call feature is implemented in this section.
- The third part is a large loop that traverses all Fiber nodes, calculates all update work through the Diff algorithm, and generates EffectList for use in the COMMIT phase. The core of this section is the beginWork function.
The commit phase
This phase takes all the updates from the Reconciliation phase, commits them and invokes the React-DOM to render the UI. After the UI rendering is complete, the remaining lifecycle functions are called, so exception handling is also done in this section
Assign priorities
The fiber structure listed above has a expirationTime.
ExpirationTime is essentially a priority for fiber Work execution.
// priorityLevel specifies the priorityLevelexportconst NoWork = 0; // Just a little higher than Never to ensure continuity must completeexport const Never = 1;
export const Idle = 2;
exportconst Sync = MAX_SIGNED_31_BIT_INT; // The maximum integer value, which is the maximum value set in V8 for 32-bit systemsexport const Batched = Sync - 1;
Copy the code
ComputeExpirationForFiber function of source, the method used to calculate fiber update task execution time, the late after comparison, decide whether to continue to do the next task.
// Calculate expirationTime for fiber objectsfunctioncomputeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) { ... // ExpirationTime const priorityLevel = getCurrentPriorityLevel(); switch (priorityLevel) {case ImmediatePriority:
expirationTime = Sync;
break; // High-priority tasks such as designing interactions with user inputcase UserBlockingPriority:
expirationTime = computeInteractiveExpiration(currentTime);
break; // A normal asynchronous taskcase NormalPriority:
// This is a normal, concurrent update
expirationTime = computeAsyncExpiration(currentTime);
break;
case LowPriority:
case IdlePriority:
expirationTime = Never;
break;
default:
invariant(
false.'Unknown priority level. This error is likely caused by a bug in ' +
'React. Please file an issue.',); }... }export const LOW_PRIORITY_EXPIRATION = 5000
export const LOW_PRIORITY_BATCH_SIZE = 250
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
)
}
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150
export const HIGH_PRIORITY_BATCH_SIZE = 100
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
)
}
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return(MAGIC_NUMBER_OFFSET - ceiling(// previous algorithm //currentTime - MAGIC_NUMBER_OFFSET + CONTEXT/UNIT_SIZE, MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE, ) ); }Copy the code
// Let's rearrange the formula: / / low 1073741821 - between (1073741821 - currentTime + 500) = > 1073741796-25 ((1073742321 - currentTime) / | 0) x 25 / / high 1073741821 - between (1073741821 - currentTime + 15, 10)Copy the code
In simple terms, the final result increases in units of 25. For example, if you type in between 102 and 126, you get 625, but if you go to 127, you get 650. That’s what happens when you divide by 25.
React updates with a lower priority have a expirationTime interval of 25ms. React allows two similar updates to have the same expirationTime. So as to achieve the purpose of batch update. Just like the doubleBuffer mentioned, React is very thoughtful about improving performance!
Expiration algorithm
- ReactFiberExpirationTime
- SchedulerWithReactIntegration
You would like to see a expirationTime data set
Executive priority
How does Fiber asynchronously coordinate the execution of tasks with different priorities
The two apis provided by the browser are requestIdleCallback and requestAnimationFrame:
RequestIdleCallback: Queuing functions that are called during browser idle time. The developer can perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses.
IdleDeadline in the callback argument retrieves the remaining time of the current frame. Use this information to schedule what needs to be done in the current frame, move on to the next task if you have enough time, and take a break if you don’t.
RequestAnimationFrame: Tells the browser that you want to execute an animation and asks the browser to call the specified callback to update the animation before the next redraw
Cooperative scheduling: This is a “contract” scheduling that requires our programs and browsers to be tightly integrated and trusted. For example, the browser can assign us a slice of execution time, and we need to execute within that time as agreed and give control back to the browser.
What Fiber does is it needs to break down the rendering task and then execute the specified task asynchronously using API scheduling based on priority:
- Low-priority tasks are handled by requestIdleCallback, which limits the execution time of tasks in order to split tasks, while avoiding tasks that take too long to execute and block UI rendering resulting in frame drops.
- High-priority tasks such as animation-related tasks handled by requestAnimationFrame;
Not all browsers support requestIdleCallback, but React implements its own polyfill internally, so you don’t have to worry about browser compatibility. The polyfill implementation is mainly implemented via rAF+ PostMessage (rAF is removed in the latest version, and = SchedulerHostConfig is available for those interested)
The life cycle
Because tasks can be interrupted in the coordination stage, after tasks are sliced and run for a period of time, the control is returned to the react task scheduling module, and then continue to run subsequent tasks according to the priority of tasks. As a result, some components will break in the middle of rendering to run other urgent, higher-priority tasks, and then not pick up where they left off, but start again, so there will be multiple calls in all the life cycles of coordination. To limit the performance cost of repeated calls, React officials removed parts of the coordination lifecycle step by step.
Waste:
- componentWillMount
- componentWillUpdate
- componentWillReceiveProps
Feature:
- static getDerivedStateFromProps(props, state)
- getSnapshotBeforeUpdate(prevProps, prevState)
- componentDidcatch
- staic getderivedstatefromerror
Why is the new life cycle static
Static is ES6, and when we define a function to be static, that means we can’t call the method we defined in our class through this
React is telling me to set derived State only to newProps. Don’t use this to call helper methods. In technical terms, getDerivedStateFromProps should be a pure function with no side effects.
What’s the difference between getDerivedStateFromError and componentDidCatch?
In short, function is different depending on the stage.
GetDerivedStateFromError is raised during the Reconciliation phase, so getDerivedStateFromError captures an error and then changes the component’s state without side effects.
Static getDerivedStateFromError(error) {// Update state so that the UI can be degraded in the next renderingreturn { hasError: true };
}
Copy the code
ComponentDidCatch allows side effects to be performed because it is in the COMMIT phase. It should be used to log errors and the like:
componentDidCatch(error, info) {
// "Component stack"Example: / /in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logComponentStackToMyService(info.componentStack);
}
Copy the code
Life cycle information point here = “Life cycle
Suspense
Suspense’s implementation is weird and controversial. In Dan’s words, you’re going to hate it, and then you’re going to love it.
Suspense tries to solve the asynchronous side effects that have existed since React was born, and it does so gracefully, using “asynchronous but synchronous writing”.
Suspense is only used temporarily with lazy for code splitting, the ability to “pause” rendering while components wait for something, and to show loading, but it’s much more than that. Suspense currently provides a way to handle asynchronous requests for data under the Experimental documentation of Concurrent mode.
usage
Const ProfilePage = react.lazy (() => import()'./ProfilePage')); // Lazy-loaded
// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
Copy the code
Import {unstable_createResource} from'react-cache'
const resource = unstable_createResource((id) => {
return fetch(`/demo/${id}`)})function ProfilePage() {
return( <Suspense fallback={<h1>Loading profile... </h1>}> <ProfileDetails /> <Suspense fallback={<h1>Loading posts... </h1>}> <ProfileTimeline /> </Suspense> </Suspense> ); }function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Copy the code
- In the render function, we can write an asynchronous request requesting data
- React will read this cache from our cache
- If there is a cache, just render normal
- If there is no cache, an exception is thrown, which is a promise
- When the promise is complete, React will go back to render (essentially re-render) and render the data
- Completely synchronous, without any asynchronous callback or anything like that
If you don’t know what that means, let me simply say the following:
Call render function -> find asynchronous request -> hover, wait for asynchronous request result -> render display data
It’s amazing to see how synchronous writing asynchrony with no yield/async/await can be mind-boggling. The nice thing about this, of course, is that our logic is very simple, very clear, no callback, no other stuff, and it’s a lot more elegant and awesome.
The official documentation states that it will also provide an official method for retrieving data
The principle of
React provides the source code for unstable_createResource
export function unstable_createResource(fetch, maybeHashInput) {
const resource = {
read(input) { ... const result = accessResult(resource, fetch, input, key); Switch (result.status) {// The promise is not completedcase Pending: {
const suspender = result.value;
throw suspender;
}
case Resolved: {
const value = result.value;
return value;
}
case Rejected: {
const error = result.value;
throw error;
}
default:
// Should be unreachable
return(undefined: any); }}};return resource;
}
Copy the code
To do this, React uses Promises. A component can throw a Promise in its Render method (or whatever it calls during the component’s rendering, such as the new static getDerivedStateFromProps). React catches the thrown Promise and looks up the tree for the closest component to Suspense, which itself has componentDidCatch, catching the Promise as an error and waiting for its execution to complete and its changed state to re-render the child component.
Suspense components take an element (fallback) as their fallback and render it when the child tree is suspended, regardless of where or why the child node is suspended.
How to achieve exception catching
- The renderRoot function in the Reconciliation phase handles throwException
- CommitRoot function in commit phase, corresponding exception handling method is dispatch
Exception capture in the Reconciliation phase
The react – the reconciler performConcurrentWorkOnRoot
// This is the entry point forEvery Concurrent task, i.e. anything that goes through the Scheduler. // Here is the entry to every concurrent task that passes through the Schedulerfunction performConcurrentWorkOnRoot(root, didTimeout) {
...
do// Start executing a Concurrent task until the Scheduler asks us to concede workLoopConcurrent();break; } catch (thrownValue) { handleError(root, thrownValue); }}while (true); . }functionhandleError(root, thrownValue) { ... throwException( root, workInProgress.return, workInProgress, thrownValue, renderExpirationTime, ); workInProgress = completeUnitOfWork(workInProgress); . }Copy the code
throwException
do {
switch (workInProgress.tag) {
....
case ClassComponent:
// Capture and retry
const errorInfo = value;
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.effectTag & DidCapture) === NoEffect &&
(typeof ctor.getDerivedStateFromError === 'function'|| (instance ! == null && typeof instance.componentDidCatch ==='function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime = renderExpirationTime;
// Schedule the error boundary to re-render using updated state
const update = createClassErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
);
enqueueCapturedUpdate(workInProgress, update);
return; }}... }Copy the code
The throwException function is divided into two parts: 1. Traverses all the parent nodes of the current exception node to find the corresponding error information (error name, call stack, etc.). This part of the code is not shown above
The second part is to traverse all the parent nodes of the current exception node and determine the types of each node, mainly the two types mentioned above. Here we focus on the ClassComponent type and determine whether the node is an exception boundary component (by determining whether there is a componentDidCatch life cycle function, etc.). If the exception bound component is found, the createClassErrorUpdate function is called to create a new update, and this update is placed in the exception update queue of this node, and the update work in this queue is updated in subsequent updates
The commit phase
ReactFiberWorkLoop finishConcurrentRender= commitRoot= commitRootImpl= captureCommitPhaseError
Commit is divided into several sub-phases, each of which a try catch calls captureCommitPhaseError
- Pre-mutate phase: we read the state of the main tree before mutating, getSnapshotBeforeUpdate is called here
- Mutation stage: This is where we change the main tree to complete the transformation of the WIP tree to the Current tree
- Style phase: Calls effect read from the main tree after being changed
export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
if (sourceFiber.tag === HostRoot) {
// Error was thrown at the root. There is no parent, so the root
// itself should capture it.
captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
return;
}
let fiber = sourceFiber.return;
while(fiber ! == null) {if (fiber.tag === HostRoot) {
captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
return;
} else if (fiber.tag === ClassComponent) {
const ctor = fiber.type;
const instance = fiber.stateNode;
if (
typeof ctor.getDerivedStateFromError === 'function' ||
(typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance))
) {
const errorInfo = createCapturedValue(error, sourceFiber);
const update = createClassErrorUpdate(
fiber,
errorInfo,
// TODO: This is always sync
Sync,
);
enqueueUpdate(fiber, update);
const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
if(root ! == null) { ensureRootIsScheduled(root); schedulePendingInteractions(root, Sync); }return; } } fiber = fiber.return; }}Copy the code
The captureCommitPhaseError function does something similar to the throwException in the previous section. It iterates through all the parents of the current exception node, finds the exception bound component, creates an Update, Call the component’s componentDidCatch lifecycle function in update.callback.
Note that throwException and captureCommitPhaseError traverse a node from the parent of the exception node, So exception catching is typically wrapped by an exception bound component with componentDidCatch or getDerivedStateFromError, which cannot catch and handle its own errors.
Hook associated
Function Component and Class Component
The Class component disadvantage
- State logic is hard to reuse: It is difficult to reuse state logic between components, perhaps using render props or HOC. However, both render properties and high-level components will wrap a layer of parent containers (usually div elements) around the original component, resulting in hierarchical redundancy that becomes complex and difficult to maintain:
- Mixing irrelevant logic in lifecycle functions (e.g. ComponentDidMount registers events and other logic in componentWillUnmount, and unloads events in componentWillUnmount, which makes it easy to write bugs. Class components have access to state all over the place, making it hard to break components into smaller components
- This points to the problem: when a parent passes a function to a child, this must be bound
However, prior to 16.8 react functional components were very weak and could only work with pure presentation components, mainly due to the lack of state and lifecycle.
Hooks advantage
- Can optimize three major problems for class components
- Reusing state logic without modifying component structure (custom Hooks)
- Ability to break down interrelated parts of a component into smaller functions (such as setting up subscriptions or requesting data)
- Separation of concerns for side effects: Side effects are the logic that does not occur during the data-to-view transformation, such as Ajax requests, accessing native DOM elements, local persistent caching, binding/unbinding events, adding subscriptions, setting timers, logging, etc. In the past, these side effects were written in the class component lifecycle functions. UseEffect is executed after the rendering is complete, useLayoutEffect is executed after the browser layout and before the painting.
Capture props and Capture Value features
capture props
class ProfilePage extends React.Component {
showMessage = () => {
alert("Followed " + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return<button onClick={this.handleClick}>Follow</button>; }}Copy the code
function ProfilePage(props) {
const showMessage = () => {
alert("Followed " + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
Copy the code
Both components describe the same logic: the user name passed in by the alert parent 3 seconds after the button is clicked.
Isn’t the React documentation describing props Immutable data? Why does it change at run time?
The reason for this is that, although props is immutable, this is mutable in the Class Component, so a call to this. Props causes the latest props to be accessed each time.
React itself will change this in real time in order to get the latest version in the life cycle and render replay, which is what this does in class.
This reveals an interesting observation about the user interface, if we say that the UI is conceptually a function of the current application state, and that event handling is part of the render result, our event handling belongs to render with specific props or states. Each Render content takes a snapshot and is retained, so when Rerender the State changes, N Render states are formed, and each Render State has its own fixed Props and State.
However, getting this.props in a setTimeout callback breaks the connection, losing the binding to a particular render, and therefore losing the correct props.
Function Component has no syntax for this.props, so props is always immutable.
Address of the test
Capture value in hook
function MessageThread() {
const [message, setMessage] = useState("");
const showMessage = () => {
alert("You said: " + message);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = e => {
setMessage(e.target.value);
};
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
Copy the code
Hook weights also have their own Props and State for each rendering. If you want to get the latest value at all times, you can use useRef to bypass Capture Value
const lastest = useRef("");
const showMessage = () => {
alert("You said: " + lastest.current);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = e => {
lastest.current = e.target.value;
};
Copy the code
Address of the test
Implementation principles of Hooks
The state and props of the Class Component are recorded on Fiber, and will be updated in this. State and props of the component after fiber is updated. Not the class Component itself. Because hooks are in function Component, they don’t have their own this, but we write state and props not on class Component this, but on Fiber. So we have the ability to record the state and get the updated state during the function Component update.
React relies on the order in which hooks are called
Call it three times daily
function Form() {
const [hero, setHero] = useState('iron man');
if(hero){
const [surHero, setSurHero] = useState('Captain America');
}
const [nbHero, setNbHero] = useState('hulk');
// ...
}
Copy the code
Let’s see how our useState is implemented
// useState implements the list import React from'react';
import ReactDOM from 'react-dom';
let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;
function useState(initState) {
let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};
function setState(newState) { currentHook.memoizedState = newState; render(); } // If a useState is not executed, the Next pointer movement will fail, and the data access will failif// console.log(workInProgreshook. next) {// Console. log(workInProgressHook); workInProgressHook = workInProgressHook.next; }elseWorkInProgressHook. Next = currentHook; workInProgressHook. Next = currentHook; MemoizedState: initState, next: null} workInProgressHook = currentHook; // console.log(firstWorkInProgressHook); }return [currentHook.memoizedState, setState];
}
function Counter() {// Every time the component is rerendered, useState is reexecuted const [name,setName] = useState('counter');
const [number, setNumber] = useState(0);
return (
<>
<p>{name}:{number}</p>
<button onClick={() => setName('New counter'+ Date. Now the new counter ())} > < / button > < button onClick = {() = >setNumber(number + 1)}>+</button>
</>
)
}
function render() {// Set workInProgressHook to firstWorkInProgressHook = firstWorkInProgressHook; ReactDOM.render(<Counter/>, document.getElementById('root'));
}
render();
Copy the code
The currentHook is set by the next pointer to the workInProgressHook, so if you break the order of the calls in the conditional statement, the next pointer will be pointing in the wrong direction. The setState that you’re passing in is not going to change the value properly because
Hooks = “react-use” for various custom packages
Why are sequential calls important to React Hooks?
THE END
Second post on nuggets, Chen is also react dishes 🐔, hope to discuss and learn with everyone, to advance to the advanced front-end architecture! Let’s love Fiber
Reference:
How and why React Fiber uses a linked list to traverse the component tree React Fiber The React Fiber framework (1) is supported by the help of x64 and x64. The React Fiber framework is supported by the help of X64 and x64. The React Fiber framework is supported by the help of X64 and x64.