preface

One of the “weird” things about setState is that when the method is called to change state, it sometimes looks like it’s executed synchronously and sometimes it looks like it’s executed asynchronously, which is a common interview question. Why does this happen? I’m going to talk to you about it today and try to do a simple demo to give you a better understanding of how setState works. Mini-setstate code repository: source code tickets, you can first pull down, while reading the article while debugging.

Front knowledge

Before learning, we should first know a few knowledge points:

  • Javascript’s event-loop mechanism is an Event loop.
  • React event mechanism.

Strange phenomenon

First, what is this weird situation?

class App extends React.Component{
    constructor(props){
        super(props);
        this.btnClick = this.btnCLick.bind(this);
    }
    state = {
        a: 1
    }
    btnClick(){
        this.setState({
            a: this.state.a + 1
        });
        this.setState({
            a: this.state.a + 1
        });
        console.log('The value of a when clicked is:'.this.state.a);
    }
    render(){
        return (
            <>
                <div>hello william</div>
                <button onClick={this.btnClick}>Click on the button</button>
            </>)}}Copy the code

The printed record of the console shows that the value of A when clicked is: 1. From this point of view, calling setState looks asynchronous, but let’s look at the next example

class App extends React.Component{
    constructor(props){
        super(props);
        this.btnClick = this.btnCLick.bind(this);
    }
    state = {
        a: 1
    }
    btnClick(){
        setTimeout(() = > {
            this.setState({
                a: this.state.a + 1
            })
            console.log('The value of A when clicked'.this.state.a);
        },0)}render(){
        return (
            <>
                <div>hello william</div>
                <button onClick={this.btnClick}>Click on the button</button>
            </>)}}Copy the code

The printed record of the console shows that the value of A when clicked is: 2. It’s synchronized from here. What’s going on in between? The following will show you the mystery, please be patient to watch.

why

First, it’s important to understand that asynchronous operations are not a React Bug, but intentional. React calls it batch updating. Why do you say so? As you can imagine, if the state update is triggered multiple times in a single call, the render function will be triggered multiple times, resulting in the page being forced to render multiple times, and performance will suffer. In most cases, an event calls setState multiple times. In fact, we only need to render it once, so the asynchronous operation of React is a means of internal performance optimization. Why is setTimeout a synchronous operation? Let’s keep watching.

How to implement asynchronous update of setState class?

First of all, when seeing setTimeout, students who are familiar with the JS event loop mechanism may think that it adopts the way of microtask at first, but in fact, React uses a mechanism commonly used in operational databases to implement it. This mechanism is called transaction mechanism. This mechanism provides better control than using microtasks directly.

The simplest version of a transaction implementation

Why use the most? Because important things are written three times, the actual transaction mechanism is much more complex than my implementation, so I highlight the simplest version, Lust for Life Max, and then we look at the code

// Transaction instance
const transaction = {
    perform(fn) {
        this.initialAll()
        fn.call(app);
        this.close();
    },
    initialAll() {
        app.isBatchingUpdate = true;
        // do something
    },
    close() {
        app.isBatchingUpdate = false;
        app.updateState();
        // do something}}Copy the code

This is the simplest version of a transaction, a bit like the Express onion, with a pre – and post-processing. So let’s look at the code, and the key thing we need to do is perform. In fact, the onClick function that we declared in our class is not immediately fired when the button is clicked. It’s passed as the FN parameter to Perform, so we can do some pre-processing before the onClick function fires. For example, change a variable called isBatchingUpdate to true. So what is the isBatchingUpdate variable? Let’s move on

Variable lock isBatchingUpdate

React will use this variable to decide whether to update the state immediately or delay updating the state.

// Log the component that has changed
const dirtyComponent = new Set(a);/ / the base class
class Component {
    // Batch update tokens can also be called variable locks
    isBatchingUpdate = false
    // The preprocessing state defaults a to 1
    preState = {
        a: 1
    }
    state = {
        a: 1
    }
    setState(changeState) {
        if (!this.isBatchingUpdate) {
            this.updateNow(changeState);
        } else {
            this.queueUpdate(changeState)
        }
    }
    // Final version updates status
    updateState() {
        Object.assign(this.state, this.preState);
    }
    // Update immediately
    updateNow(changeState) {
        Object.assign(this.preState, changeState);
        Object.assign(this.state, this.preState);
        this.render();
    }
    // We can also pass in functions to show the basic principles first, and then add
    queueUpdate(changeState) {
        Object.assign(this.preState, changeState);
        dirtyComponent.add(this); }}Copy the code

This is a React.Component that I simulated, in which I briefly implemented setState, which is the core of batch update. The setState here will determine whether the current batch update state is in the state, if it is, the state will be updated to the internal preprocessing state preState, preState will remember your modification, at the same time, the modified state of the component, Put it inside a dirtyComponent. Then it’s our normal inheritance.

/ / class components
class App extends Component {
    state = {
        a: 1
    }
    onClick() {
        this.setState({ a: this.state.a + 1 });
        console.log('The value of A when clicked'.this.state.a)
        this.setState({ a: this.state.a + 1 });
        console.log('The value of A when clicked'.this.state.a);
        this.test();
    }
    test() {
        this.setState({ a: this.state.a + 1 });
        console.log('The value of A when clicked'.this.state.a);
    }
    JSX -> React. CreateElement -> vnode to display the final data structure of the virtual DOM directly
    render() {
        console.log('Value of A at render time'.this.state.a);
        return {
            type: 'div'.props: {
                id: "btn".children: [].onClick: () = > this.onClick()
            }
        }
    }
}
Copy the code

React: setState: transaction: React: transaction: setState: Transaction: React: Transaction: Transaction: React: Transaction: Transaction: React: Transaction: Transaction: React: Transaction: Transaction: React

// The build instance is mounted at Window for display purposes, and the source code is saved in a separate place
window.app = new App();
// Get the virtual DOM node
const vnode = app.render();
React itself implements event delegation, but for demonstration purposes, it listens for events directly on the DOM node
document.getElementById('btn').addEventListener('click'.() = > {
    React contains a method to find vNodes and a method to match events
    transaction.perform(vnode.props.onClick);
    if(dirtyComponent.size ! = =0) {
        dirtyComponent.forEach(component= > component.render());
        dirtyComponent.clear();
    }
    // do something
})
Copy the code

As you can see, when we click the React button, React will first find our registered vNode and the corresponding event in the vNode, and then the transaction instance will loop our onClick method, thus opening the isBatchingUpdate variable before executing. As long as our method is not complete, we will keep our changes in preState and never update to the actual state due to variable locks. Until our method is finished executing, the transaction’s post-function closes our isBatchingUpdate, overwrites preState to our actual state and performs the render operation, and the batch update is complete.

conclusion

So far, the mini-version of setState is implemented, here is mainly to explain the truth, there may be some details and the source a little different, but the core idea is the same, we can study together interested.

added

Q: Why does setTimeout synchronize again?

A: setTimeout will be synchronized because setTimeout will put the function in the next macro task, which is just out of the control of the transaction, and will display the synchronization update condition.

Q: Why not Promise.resolve().then() instead?

A: I have also seen many articles that directly use promise.resolve (). Then (), but I don’t think it is right. In my opinion, the two are fundamentally different. Promise.resolve(). Then uses the principle of microtask to delay execution. This delay is not easy to control. What if other microtasks are inserted? React advocates functional programming. The idea of functional programming is that everything is transparent, controllable, and predictable. I guess this is the root cause of the transaction mechanism implemented in React.