React source code analysis 2 Design concept of react
Video Explanation (efficient learning): Enter learning
Previous articles:
1. Introduction and interview questions
3.react source code architecture
4. Source directory structure and debugging
6.legacy and concurrent mode entry functions
20. Summary & answers to interview questions in Chapter 1
Asynchronous interruptible
- Where is React15 slow
Before we talk about this part, we need to talk about what factors lead to the slow down of react and the need for reconstruction.
The coordination process before React15 is synchronous, also known as stack reconciler. Because js execution is single threaded, it leads to the failure to respond to some high priority tasks in time when updating time-consuming tasks, such as user input, so the page will get stuck. This is the limitation of cpu.
- Solution
How to solve this problem? Imagine what we would do if we encountered time-consuming code calculation in daily development and single thread environment. First, we might divide the task so that it can be interrupted and give up the execution right when other tasks arrive. When other tasks are executed, The remaining calculations are performed asynchronously from the previously interrupted part. Therefore, the key is to implement a set of asynchronous and interruptible schemes.
- realization
In the solution just mentioned, task segmentation and asynchronous execution are mentioned, and the execution right can be ceded. Therefore, the three concepts in react can be brought out
Fiber: the update of react15 is synchronous. Because it can't divide tasks, it needs a set of data structure so that it can not only correspond to the real dom, but also serve as a separated unit. This is fiber.
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)
- Scheduler: with Fiber, we need to use the browser's time slice to asynchronously execute these Fiber work units. We know that the browser has an api called requestIdleCallback, which can execute some tasks when the browser is idle. We use this api to update react and let high priority tasks respond first, However, the fact is that requestIdleCallback has the problems of browser compatibility and unstable trigger, so we need to implement a set of time slice running mechanism with js. In react, this part is called scheduler.
- Lane: with asynchronous scheduling, we also need to manage the priority of each task in a fine-grained manner so that high priority tasks can be executed first. Each Fiber work unit can also compare the priority. Tasks with the same priority can be updated together. Think about whether it is more cool.
Generated upper layer implementation
With this asynchronous and interruptible mechanism, we can implement batch updates and suspend
The following two figures are the differences before and after using asynchronous interruptible update. You can experience it
Algebraic Effects
In addition to the bottleneck problem of cpu, there are also problems related to side effects, such as data acquisition, file operation and so on. The performance of different devices and network conditions are different. How can react deal with these side effects so that we can have the best practice in coding and consistent performance in running applications? This requires that react has the ability to separate side effects. Why should it separate side effects? Because it needs to be decoupled, which is algebraic effect.
Question: we have all written codes to obtain data. We show loading before obtaining data and cancel loading after obtaining data. Assuming that our equipment performance and network conditions are good and the data will be obtained soon, is it necessary for us to show loading at the beginning? How can we have a better user experience?
Take a look at the following example
function getPrice(id) { return fetch(`xxx.com?id=${productId}`).then((res)=>{ return res.price }) } async function getTotalPirce(id1, id2) { const p1 = await getPrice(id1); const p2 = await getPrice(id2); return p1 + p2; } async function run(){ await getTotalPrice('001', '002'); }
getPrice is an asynchronous method to obtain data. We can use async+await to obtain data, but this will cause the run method calling getTotalPrice to become an asynchronous function. This is the infectivity of async, so we can't separate the side effects.
function getPrice(id) { const price = perform id; return price; } function getTotalPirce(id1, id2) { const p1 = getPrice(id1); const p2 = getPrice(id2); return p1 + p2; } try { getTotalPrice('001', '002'); } handle (productId) { fetch(`xxx.com?id=${productId}`).then((res)=>{ resume with res.price }) }
Now change to the following code, where perform and handle are fictional syntax. When the code is executed to perform, the execution of the current function will be suspended and captured by the handle. The handle function body will get the productId parameter. After obtaining the data, resume price will return to the place where the performance was suspended and return price, This completely separates the side effects from gettotalpierce and getPrice.
The key process here is perform to pause the function execution, handle to obtain the function execution right, and resume to hand over the function execution right.
But these grammars are fictional after all, but look at the code below
function usePrice(id) { useEffect((id)=>{ fetch(`xxx.com?id=${productId}`).then((res)=>{ return res.price }) }, []) } function TotalPirce({id1, id2}) { const p1 = usePrice(id1); const p2 = usePrice(id2); return <TotalPirce props={...}> }
Is it familiar to replace getPrice with usePrice and gettotalpierce with totalpierce components? This is the ability of hook to separate side effects.
We know that the generator can also pause and resume the program. Can't we just use the generator? However, to resume the execution after the generator pauses, we still have to exchange the execution right to the direct caller, who will continue to hand over along the call stack. Therefore, it is also infectious, and the generator can't calculate and sort the priority.
function getPrice(id) { return fetch(`xxx.com?id=${productId}`).then((res)=>{ return res.price }) } function* getTotalPirce(id1, id2) { const p1 = yield getPrice(id1); const p2 = yield getPrice(id2); return p1 + p2; } function* run(){ yield getTotalPrice('001', '002'); }
Decoupling side effects are very common in the practice of functional programming, such as Redux saga, which separates side effects from saga. It does not deal with side effects 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 react has Fiber. After updating the Fiber, return the execution right to the browser and let the browser decide how to schedule later. Therefore, Fiber has to be a linked list structure to achieve this effect,
Suspension is also an extension of this concept. I feel a little when I see the specific suspension source code. Let's start with an example
const ProductResource = createResource(fetchProduct); const Proeuct = (props) => { const p = ProductResource.read( // Write asynchronous code in a synchronous way! props.id ); return <h3>{p.price}</h3>; } function App() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <Proeuct id={123} /> </Suspense> </div> ); }
You can see the ProductResource Read is a completely synchronous writing method, which completely separates the part of obtaining data from the Proeuct component. In the source code, ProductResource Read will throw a special promise before getting the data. Due to the existence of the scheduler, the scheduler can capture the promise, pause the update, and return the execution right after getting the data. ProductResource can be local storage, or even redis, mysql and other databases, that is, component as a service. server Component may appear in the future.