The setState synchronous asynchronous problem, React batch update has always been a relatively fuzzy problem, this paper hopes to explain this problem from the perspective of framework design.

React has a UI = f(data) formula: The UI is derived from data, so when we’re writing an application, all we care about is how the data changes, data > data, so UI > UI, and in this process, we don’t really care how the UI changes to UI, React handled this part of the work for us.

So how does React know which DOM needs to be modified when data changes? The simplest and most violent thing is that you rebuild the entire DOM tree each time. React actually uses a technique called virtual-DOM: It uses JS objects to represent THE DOM structure, and obtains incremental changes to the DOM tree by comparing the difference between previous and later JS objects. Virtual-dom greatly reduces DOM manipulation through violent JS calculation, making UI = F (data) model performance is not so slow, of course, you use native JS/jquery direct manipulation of DOM is always the fastest.

SetState batch update

In addition to virtual-dom optimization, reducing the frequency of data updates is another method, namely batch updates of React. Such as:

G () {this.setState({age: 18}) this.setState({color: 'black '})} f() {this.setState({name: 'yank'}) this.g()}Copy the code

React synthesizes it into a setState call

f() {
	this.setState({
		name: 'yank'.age: 18.color: 'black'})}Copy the code

Let’s take a quick look at how setState merges through pseudocode.

SetState implementation

setState(newState) {
	if (this.canMerge) {
		this.updateQueue.push(newState)
		return 
	}
	
	// Here's the real update: dom-diff, lifeCycle.... }Copy the code

Then the f method is called

G () {this.setState({age: 18}) this.setState({color: 'black '})} f() {this.canmerge = true this.setState({name: 'yank'}) this.g() this.canmerge = false const finalState =... This.setstate (finaleState)}Copy the code

As you can see, setState first determines whether it can merge, and if it can merge, it returns directly.

When I use React, I didn’t set this.canMerge. We didn’t. React implicitly set it for us! React has complete control over event handlers that declare cycles. React executes these functions inside React.

class A extends React.Component {
	componentDidMount() {
		console.log('... ')
	}

	render() {
		return (<div onClick={() = > {
			console.log('hi')
		}}></div>}}Copy the code

React executes the canMerge logic before and after executing componentDidMount, as well as the event handler. React delegates all events, and executes the React logic before executing your handler function, so React also has a chance to execute the canMerge logic.

Batch updates are great drops! We certainly want any setState to be batched. The key is whether React has the opportunity to perform canMerge logic, i.e. whether React has control over the target function. Without control, once setState returns early, there is no chance to apply the update.


class A extends React.Component {
	handleClick = (a)= > {
		this.setState({x: 1})
		this.setState({x: 2})
		this.setState({x: 3})
		
		setTimeout((a)= > {
			this.setState({x: 4})
			this.setState({x: 5})
			this.setState({x: 6})},0)
	}	
	
	render() {
		return (<div onClick={this.handleClick}></div>}}Copy the code

React has the opportunity to perform canMerge logic, so x is 1, 2, and 3 are merged. After handleClick ends, canMerge is reset to false. Notice that there is a setTimeout(fn, 0). React has no control over setTimeout. React cannot perform canMerge logic before and after setTimeout, so x is 4, 5, and 6 cannot be merged, so fn will have three dom-diff occurrences. React has no control over many situations: Promise.then(FN), fetch callback, XHR network callback, and so on.

Unstable_batchedUpdates Manual merge

So if x is 4, is there a way to combine 5, 6? Yes, you can use the unstable_batchedUpdates API as follows:

class A extends React.Component {
	handleClick = (a)= > {
		this.setState({x: 1})
		this.setState({x: 2})
		this.setState({x: 3})
		
		setTimeout((a)= > {
			ReactDOM.unstable_batchedUpdates((a)= > {
				this.setState({x: 4})
				this.setState({x: 5})
				this.setState({x: 6})})},0)
	}	
	
	render() {
		return (<div onClick={this.handleClick}></div>}}Copy the code

This API, without much explanation, is clear when we look at its pseudo-code

function unstable_batchedUpdates(fn) {
	this.canMerge = true
	
	fn()
	
	this.canMerge = false
	const finalState = ...  // Merge finalState with this.updateQueue
	this.setState(finaleState)
}
Copy the code

So, setState in unstable_batchedUpdates will also be merged.

The specification of forceUpdate

The forceUpdate function name is “forceUpdate”. Since it is “forced update” there are two misleading issues:

  1. Is forceUpdate synchronous? Does “force” guarantee that the call is then directly dom-diff?
  2. Do you “force” an update of the entire component tree? Do you include your own, posterity components? Neither issue is clearly addressed in official documentation.
class A extends React.Component{
	
	handleClick = (a)= > {
		this.forceUpdate()
		this.forceUpdate()
		this.forceUpdate()
		this.forceUpdate()
	}
	
	shouldComponentUpdate() {
		return false
	}
	
	render() {
		return (
			<div onClick={this.handleClick}>
				<Son/>// a component</div>)}}Copy the code

For the first question: forceUpdate behaves the same as setState in terms of whether it is batch or not. React has control over functions that are batch.

ShouldComponentUpdate (); shouldComponentUpdate (); shouldComponentUpdate ();

So forceUpdate can simply be understood as this.setState({}), but this setState does not call its own “shouldComponentUpdate” declaration cycle.

The imagination of Fiber

Showing developers calling unstable_batchedUpdates is not elegant and developers should not be influenced by the implementation details of the framework. But as mentioned earlier, React has no control function, and unstable_batchedUpdates seem inevitable. However, the Fiber architecture of react16.x may be changed. Let’s take a look at the update under Fiber

setState(newState){
	this.updateQueue.push(newState)
	requestIdleCallback(performWork)
}
Copy the code

RequestIdleCallback calls the function when the browser is idle and is a low-priority function.

Now let’s consider:

handleClick = (a)= > {
		this.setState({x: 1})
		this.setState({x: 2})
		this.setState({x: 3})
		
		setTimeout((a)= > {
			this.setState({x: 4})
			this.setState({x: 5})
			this.setState({x: 6})},0)}Copy the code

All updates are queued when x is 1, 2, 3, 4, 5, and 6, and requestIdleCallback takes care of performing uniform updates when the browser is idle.

As the scheduling of fiber is complicated, this is just a brief explanation. Whether it can be merged depends on the priority and other factors. Fiber’s architecture does allow for more elegant batch updates without requiring developers to explicitly call unstable_batchedUpdates

AD time

Finally, let’s advertise our open source RN-to-applet engine, Alita, which takes a run-time approach to JSX as opposed to existing community compile-time solutions, as described in this article.

Therefore, Alita has a built-in mini-React. This mini-React also provides the function of synthesizes setState/forceUpdate updates and provides the unstable_batchedUpdates interface. Alita Minil-React is a small implementation of react. The code is available at github.com/areslabs/al… .

Alita address: github.com/areslabs/al… . Welcome to Star & PR & Issue