When it comes to react fiber, most people know that this is a new feature of react. After reading some articles on the Internet, they can probably say keywords such as "fiber", "a new data structure", and "dispatch mechanism when updating".
But if asked:
- With react fiber, why don't you need vue fiber?
- Before recursive traversal of the virtual dom tree was interrupted, you had to start from scratch, why can you recover from the breakpoint with react fiber?
This article will clarify these two issues from the responsive design of the two frameworks. It does not involve obscure source code. Whether you have used react or not, you will not have much resistance to reading.
what is responsive
Whether you use react or vue, the term "responsive update" is definitely not unfamiliar.
Responsive, intuitively, the view updates automatically. If you start to use the framework directly at the beginning of the front-end, you will take it for granted, but before the "responsive framework" was born, it was very troublesome to implement this function.
Next, I will make a time display and implement it with native js, react, and vue respectively:
- Native js:
If you want to change the content on the screen, you must first find the dom ( document.getElementById
), and then modify the dom ( clockDom.innerText
).
<div id="root">
<div id="greet"></div>
<div id="clock"></div>
</div>
<script>
const clockDom = document.getElementById('clock');
const greetDom = document.getElementById('greet');
setInterval(() => {
clockDom.innerText = `现在是:${Util.getTime()}`
greetDom.innerText = Util.getGreet()
}, 1000);
</script>
With a responsive framework it's easy
- react:
To modify the content, you only need to call setState
to modify the data, and then the page will be re-rendered.
<body>
<div id="root"></div>
<script type="text/babel">
function Clock() {
const [time, setTime] = React.useState()
const [greet, setGreet] = React.useState()
setInterval(() => {
setTime(Util.getTime())
setGreet(Util.getGreet())
}, 1000);
return (
<div>
<div>{greet}</div>
<div>现在是:{time}</div>
</div>
)
}
ReactDOM.render(<Clock/>,document.getElementById('root'))
</script>
</body>
- vue:
We don't need to pay attention to the dom as well. When modifying the data, directly modify the this.state=xxx
, and the page will display the latest data.
<body>
<div id="root">
<div>{{greet}}</div>
<div>现在是:{{time}}</div>
</div>
<script>
const Clock = Vue.createApp({
data(){
return{
time:'',
greet:''
}
},
mounted(){
setInterval(() => {
this.time = Util.getTime();
this.greet = Util.getGreet();
}, 1000);
}
})
Clock.mount('#root')
</script>
</body>
The responsive principle of react and vue
When modifying the data mentioned above, react needs to call setState
method, and vue directly modifies the variable. It seems that the usage of the two frameworks is different, but the principle of responsiveness is here.
Modifying data from the perspective of the underlying implementation: In react, the state of components cannot be modified. setState
did not modify the variables in the original memory, but opened up a new memory;
Vue, on the other hand, directly modifies the original memory that saves the state.
Therefore, it is often seen that a word "immutable" often appears in react-related articles, which is immutable in translation.
The data has been modified, and the next step is to solve the update of the view: in react, after calling setState
method, the component will be re-rendered from top to bottom. The meaning of top-down is that the component and its subcomponents all need to be rendered; while vue Use Object.defineProperty
(vue@3 migrated to Proxy) to hijack the data setting ( setter
) and acquisition ( getter
), that is to say, vue can accurately know which part of the view template uses this data, and when this data is modified , which tells the view that you need to re-render.
So when a piece of data changes, React's component rendering consumes a lot of performance - the state of the parent component is updated, and all child components have to be rendered together. It cannot be as accurate as the granularity of the current component like Vue.
For proof, I wrote a demo with react and vue respectively. The function is very simple: the parent component nests the child component, clicking the button of the parent component will modify the state of the parent component, and clicking the button of the child component will modify the state of the child component.
For a better comparison, to visualize the rendering stage, instead of using the more popular React functional components, Vue also uses an uncommon render method:
class Father extends React.Component{
state = {
fatherState:'Father-original state'
}
changeState = () => {
console.log('-----change Father state-----')
this.setState({fatherState:'Father-new state'})
}
render(){
console.log('Father:render')
return (
<div>
<h2>{this.state.fatherState}</h2>
<button onClick={this.changeState}>change Father state</button>
<hr/>
<Child/>
</div>
)
}
}
class Child extends React.Component{
state = {
childState:'Child-original state'
}
changeState = () => {
console.log('-----change Child state-----')
this.setState({childState:'Child-new state'})
}
render(){
console.log('child:render')
return (
<div>
<h3>{this.state.childState}</h3>
<button onClick={this.changeState}>change Child state</button>
</div>
)
}
}
ReactDOM.render(<Father/>,document.getElementById('root'))
The above is the effect when using react, modify the state of the parent component, the parent and child components will be re-rendered: click change Father state
, not only print Father:render
, but also print child:render
.
(Poke here try the online demo)
const Father = Vue.createApp({
data() {
return {
fatherState:'Father-original state',
}
},
methods:{
changeState:function(){
console.log('-----change Father state-----')
this.fatherState = 'Father-new state'
}
},
render(){
console.log('Father:render')
return Vue.h('div',{},[
Vue.h('h2',this.fatherState),
Vue.h('button',{onClick:this.changeState},'change Father state'),
Vue.h('hr'),
Vue.h(Vue.resolveComponent('child'))
])
}
})
Father.component('child',{
data() {
return {
childState:'Child-original state'
}
},
methods:{
changeState:function(){
console.log('-----change Child state-----')
this.childState = 'Child-new state'
}
},
render(){
console.log('child:render')
return Vue.h('div',{},[
Vue.h('h3',this.childState),
Vue.h('button',{onClick:this.changeState},'change Child state'),
])
}
})
Father.mount('#root')
The effect of using vue above, no matter which state is modified, the component will only re-render the smallest particles: click change Father state
, only Father:render
child:render
not be printed.
(Poke here try the online demo)
Impact of Different Responsive Principles
The first thing to emphasize is that the "render", "render" and "update" mentioned above do not mean that the browser actually renders the view. Instead, at the javascript level, the framework calls the render method implemented by itself to generate an ordinary object, which saves the properties of the real DOM, which is often referred to as the virtual DOM. This article will use component rendering and page rendering to distinguish between the two.
Each view update process is like this:
- Component rendering generates a new virtual dom tree;
- Compare the old and new virtual dom trees to find the changed parts; (also known as the diff algorithm)
- Create real DOM for the parts that really change, mount them to the document, and re-render the page;
Due to the different responsive implementation principles of react and vue, when the data is updated, the react component will render a larger virtual dom tree in the first step.
what is fiber
So much has been said above, all to make it easier to explain why react fiber is needed: when the data is updated, react generates a larger virtual dom tree, which brings a lot of pressure to the diff in the second step - we think It takes longer to find the part that really changed. js occupies the main thread for comparison, the rendering thread cannot do other work, and the user's interaction cannot be responded to, so react fiber appears.
React fiber can't shorten the comparison time, but it allows the diff process to be divided into small pieces, because it has the ability to "save the progress of the work". js will compare a part of the virtual DOM, and then let the main thread do other work for the browser, and then continue to compare, reciprocate in turn, wait until the final comparison is completed, and update it to the view at one time.
fiber is a new data structure
As mentioned above, react fiber enables the diff stage to have the ability to save the progress of the work, and this part will explain why.
To find the part where the state changes before and after, we must traverse all nodes.
In older architectures, nodes were organized in a tree: each node had multiple pointers to child nodes. The easiest way to find the changing parts of the two trees is depth-first traversal. The rules are as follows:
- Starting from the root node, traverse all the child nodes of the node in turn;
- When the traversal of all child nodes of a node is completed, the node traversal is considered complete;
If your system has learned data structures, you should be able to react quickly, this is just a follow-up traversal of depth-first traversal. According to this rule, the order in which the nodes complete the traversal is marked in the graph.
This traversal has a characteristic that it must be done all at once. Assuming that the traversal is interrupted, although the index of the node in progress can be retained, the next time we continue, we can indeed continue to traverse all the child nodes under the node, but there is no way to find its parent node - because each node has only its children Pointer to the node. There is no way to recover from a breakpoint, but to start all over again.
Take this tree as an example:
An interruption occurred when traversing to node 2. We save the index of node 2. When we resume the next time, we can traverse to nodes 3 and 4 below it, but we cannot retrieve nodes 5, 6, 7, and 8.
In the new architecture, each node has three pointers: respectively pointing to the first child node, the next sibling node, and the parent node. This data structure is fiber, and its traversal rules are as follows:
- Starting from the root node, traverse the child nodes and sibling nodes of the node in turn. If both are traversed, return to its parent node;
- When the traversal of all child nodes of a node is completed, the node traversal is considered complete;
According to this rule, the order in which node traversal is completed is also marked in the graph. Compared with the tree structure, it will be found that although the data structure is different, the traversal start and completion order of nodes are exactly the same. The difference is that when the traversal is interrupted, as long as the index of the current node is preserved, the breakpoint can be recovered - because each node maintains the index of its parent node.
Also interrupted when traversing to node 2, the fiber structure enables all remaining nodes to be walked.
This is why the rendering of react fibers can be interrupted. Although trees and fibers look similar, in essence, one is a tree and the other is a linked list.
fiber is fiber
This data structure is called fiber because the translation of fiber is fiber, which is considered to be an implementation form of coroutine. A coroutine is a smaller scheduling unit than a thread: its start and pause can be controlled by the programmer. Specifically, react fiber is a "progress bar" rendered by a component controlled by the api requestIdleCallback
.
requesetIdleCallback
is a callback belonging to a macro task, just like setTimeout. The difference is that the execution timing of setTimeout is controlled by the callback time we pass in, and requestIdleCallback is controlled by the refresh rate of the screen. This article does not discuss this part in depth, just need to know that it will be called every 16ms, and its callback function can obtain the time that can be executed this time. In addition to the callback of requesetIdleCallback
, every 16ms has other work, so it can be The time used is indeterminate, but as soon as the time is up, the traversal of the node is stopped.
The method of use is as follows:
const workLoop = (deadLine) => {
let shouldYield = false;// 是否该让出线程
while(!shouldYield){
console.log('working')
// 遍历节点等工作
shouldYield = deadLine.timeRemaining()<1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
The callback function of requestIdleCallback can check how much time is left for its own use through the incoming parameter deadLine.timeRemaining()
. The demo above is also pseudocode for react fiber working.
However, due to poor compatibility and the low frequency of the callback function being called, react actually uses a polyfill (its own api) instead of requestIdleCallback.
Now, it can be summed up: React Fiber is an update mechanism proposed by React 16. It replaces the tree with a linked list and connects the virtual dom, so that the process of component update can be interrupted and resumed; it splits the work of component rendering into will actively yield the main thread of rendering.
Changes brought by react fiber
First, put a comparison chart that is widely circulated in the community, which are implemented with react 15 and 16 respectively. It's a triangle of varying width, the number in the middle of each small circle changes over time, and beyond that, hovering the mouse changes the color of the small dot.
(Poke here is react15-stack online address|Here is react16-fiber )
In practice, you can find two characteristics:
- After using the new architecture, the animation becomes smooth, and the change of width will not be stuck;
- After using the new architecture, the user response is faster, and the color changes faster when the mouse is hovered;
Let’s stop here for a while. Are these two points brought to us by fiber? It is understandable that the user responds faster, but can the use of react fiber bring about a speedup in rendering?
The root cause of smooth animation must be that more animation frames can be obtained in one second. But when we used react fiber, it didn't reduce the total time needed for the update.
In order to facilitate understanding, I made a picture of the state during refresh:
The above is the time point of obtaining each frame when using the old react, and the following is the time point of obtaining each frame when using the fiber architecture, because the component rendering is fragmented, the time point for completing a frame update is pushed back instead , we take some time slices to process user responses.
It should be noted here that there will not be a situation where "one component rendering is not completed, and part of the page is rendered and updated", and react will ensure that each update is complete.
But the page animation does become smooth, why is that?
I downed the code repository of the project and took a look at its animation implementation: the component animation effect is not obtained by directly modifying width
, but using the transform:scale
attribute with 3D transformation. If you've heard of hardware acceleration, you probably know why: this way the re-rendering of the page does not depend on the main rendering thread in the picture above, but is done directly in the GPU. In other words, the rendering main thread only needs to ensure that there are some time slices to respond to user interaction.
-<SierpinskiTriangle x={0} y={0} s={1000}>
+<SierpinskiTriangle x={0} y={0} s={1000*t}>
{this.state.seconds}
</SierpinskiTriangle>
Modify line 152 in the project code and change the graphics change to width width
. You will find that even if you use react fiber, the animation will become quite stuck, so the smoothness here is mainly due to CSS animation. (Try cautiously on a computer with little memory, the browser will freeze)
React is not as good as vue?
We now know that react fiber is to make up for the shortcomings of "brainless" refresh and imprecision when updating. Does this mean that react performance is worse?
Not really. Which is better or worse is a very controversial topic, and I will not make an evaluation here. Because Vue implements accurate updates also comes at a cost. On the one hand, it is necessary to configure a "monitor" for each component to manage the dependency collection of views and the release notifications when data is updated, which also consumes performance; On the other hand, vue can achieve dependency collection thanks to its template syntax and achieve static compilation, which cannot be done by react using the more flexible JSX syntax.
Before the appearance of react fiber, react also provided PureComponent, shouldComponentUpdate, useMemo, useCallback and other methods for us to declare which sub-components do not need to be updated together.
Epilogue
Going back to the first few questions, the answers are not difficult to find in the text:
- Because of its inherent shortcomings, react cannot be updated accurately, so it needs react fiber to slice the component rendering work; while vue is based on data hijacking, the update granularity is very small, and there is no such pressure;
- The data structure of react fiber allows nodes to trace back to their parent nodes, and as long as the interrupted node index is retained, the previous work progress can be resumed;
If this article is helpful to you, give me a like ~ this is very important to me
(Click to see better! >3<)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。