The characteristics of the setState

  1. State can only be defined within the Contructor function
  2. State updates can only be done on setState, state updates, rerender (elements are immutable, updates are new elements replacing old elements)
  3. SetState updates can be asynchronous, status updates are merged, and some status updates are put into queues to centralize updates and improve performance
import React from 'react'
import ReactDOM from 'react-dom'
class Counter extends React.Component {
    constructor(props) {
    	super(props)
    	this.state = { number: 0 }
    }
    
    add() {
    	
    	this.setState({ number: this.state.number + 1 })
    	console.log(this.state) //0
    	this.setState({ number: this.state.number + 1 })
    	console.log(this.state) //0
    	setTimeout(() => {
    		this.setState({ number: this.state.number + 1 })
    		console.log(this.state) //2
    		this.setState({ number: this.state.number + 1 })
    		console.log(this.state) //3
    	})
    }
    render() {
    	return (
    		<button onClick={this.add.bind(this)}>{this.state.number}</button>
    	)
    }
}
ReactDOM.render(<Counter />, document.getElementById('root'))

Copy the code

The code execution looks like this:

Code implementation

1. Mount DOM elements

  • Because state can only define assignments in constructors, it must be a class component
  • The render function of the class component returns the HTML structure from which we can generate DOM elements to mount on the container

Here we use counters as an example :(code based on native)

Class Component {constructor(props) {this.props = props} // Generate a DOM structure from the HTML string returned by rendercreateDomFromHtmlString() {
    	let htmlString = this.render()
    	let divDom = document.createElement('div')
    	divDom.innerHTML = htmlString
    	letDomElement accordingly = divDom. Children [0] / / to the dom elements, here is refers to the button click event binding approach domElement accordingly. The addEventListener ('click', this.add.bind(this))
    	returnDomElement accordingly} / / mounted on mount the dom structure to the designated container (container) {container. The appendChild (enclosing createDomFromHtmlString ())}} class Counter extends Component { constructor(props) { super(props) this.state = { number: 0 } } add = () => { console.log('add')}render() {
    	return `<button>${this.state.number}</button>`
    }
}

new Counter().mounted(document.getElementById('root'))

Copy the code

The code execution effect is shown below

2. Update the status

  • Implement the setState method to copy the new state to the old state

  • Implement the updateComponent method, replacing the old element with the new one

  • Handling of event binding methods for dom

    • Since the return value of Render is a template string, using inline properties to bind methods to events cannot be mapped to component instances
    • The solution we provide here is event delegate, which delegates all the event methods of dom elements to the global DOM element (window) window.trigger(event,method)
    • We are creating a dom element, add a component to the dom element attribute, the attribute value for the current instance ponent = this.domElement.com this convenient follow-up in delegate methods found in the events triggered by the instance
Class Component {constructor(props) {this.props = props} // Generate a DOM structure from the HTML string returned by rendercreateDomFromHtmlString() {
    	let htmlString = this.render()
    	let divDom = document.createElement('div'// Save the generated element to the instance as the old element this.domElement = divdom.children [0] // 2. Copies the current instance of a component to create real dom on the component of this.domElement.com ponent = thisreturn this.domElement
    }
    
    setObject.assign(this.state, partialState) // re-render this.updatecomponet ()}updateComponet() {// New element replaces old elementlet oldElement = this.domElement
    	letnewElement = this.createDomFromHtmlString() oldElement.parentElement.replaceChild(newElement, OldElement)} / / mounted on mount the dom structure to the designated container (container) {container. The appendChild (enclosing createDomFromHtmlString ())}} window.trigger = (event, method) => { // 3. Gets an instance of the current DOM, which in turn gets the method for event bindinglet component = event.target.component
    component[method].call(component)
    }
    class Counter extends Component {
    constructor(props) {
    	super(props)
    	this.state = { number: 0 }
    }
    add = () => {
    	this.setState({
    		number: this.state.number + 1
    	})
    }
    render() {// 1. Since it is a template string, it cannot be mapped to an instance, so we delegate it to the global DOM (window) element using the event delegate mechanismreturn `<button onclick="trigger(event,'add')">${this.state.number}</button>`
    }
}

new Counter().mounted(document.getElementById('root'))



Copy the code

The code execution looks like this:

3. Implement setState update queue

  1. Define a batch update strategy batchingStrategy. The default batch update policy is false
  2. Define a updater, the updater, to hold the state to be updated (addState)
  3. $updater. AddState state of preservation needs to be updated, first determine whether need to batch updates, if required, will save the current state of the need to update the component to batchingStrategy. DirtyComponent, otherwise, update the current component directly, this.component.updateComponent()
  4. Event binding method is triggered when batchingStrategy. IsBatchingUpdate = true
  5. Method is executed to perform a series of operations, such as state saving, at which point the state is updated
  6. Method performs end, batchingStrategy isBatchingUpdate = false, all need to update the status to render dirty components to render batchingStrategy. BatchUpdate ()
  7. Component.updatecomponet () re-renders and merges the saved states that need to be updated, taking the last state that needed to be updated
// batchingStrategy const batchingStrategy = {isBatchingUpdate:false, // do you need to batch update dirtyComponents: [], // dirtyComponents: the status of components is inconsistent with that displayed on the interfacebatchUpdate() {enclosing dirtyComponents. ForEach ((component) = > {component. UpdateComponet ()})}} / / update the class Updater { Constructor (Component) {this.component.pendingStates = []} // Save the state to update addState(partialState) { This.pendingstates.push (partialState) // Check whether it is batch update, if the component needs to update state is added to dirtyComponnet, Otherwise directly update component batchingStrategy. IsBatchingUpdate? BatchingStrategy. DirtyComponents. Push ponent (this.com) : this.component.updateComponet() } } class Component { constructor(props) { this.props = props this.$updater= new Updater(this)} // Generate a DOM structure based on the HTML string returned by rendercreateDomFromHtmlString() {
		let htmlString = this.render()
		let divDom = document.createElement('div'// Save the generated element to the instance as the old element this.domElement = divdom.children [0] // 2. Copies the current instance of a component to create real dom on the component of this.domElement.com ponent = thisreturn this.domElement
	}

	setState(partialState) {// Status update: // object.assign (this.state, partialState) // re-render // this.updatecomponet () // Save the state that needs to be updated to this.$updater.addState(partialState)
	}

	updateComponet() {// update the status in batches.$updater.pendingStates. ForEach ((partialState) => {object.assign (this.state, partialState)}) // The state to save this.$updater.pendingstates. length = 0 // New element replaces old elementlet oldElement = this.domElement
		letnewElement = this.createDomFromHtmlString() oldElement.parentElement.replaceChild(newElement, OldElement)} / / mounted on mount the dom structure to the designated container (container) {container. The appendChild (enclosing createDomFromHtmlString ())}} Window.trigger = (event, method) => {// Before the method is executed, set the batch update totrue
	batchingStrategy.isBatchingUpdate = true// 3. Get the current dom instance, and then get the event binding methodletComponent = event.target.component Component [method].call(component) // After the method executes, set the batch update tofalse
	batchingStrategy.isBatchingUpdate = falseBatchingStrategy. BatchUpdate () / / batch updates, } Class Counter extends Component {constructor(props) {super(props) this.state = {number: constructor(props); 0 } } add = () => { this.setState({ number: this.state.number + 1 }) console.log(this.state) //0 this.setState({ number: this.state.number + 1 }) console.log(this.state) //0setTimeout(() => {
			this.setState({ number: this.state.number + 1 })
			console.log(this.state) //2
			this.setState({ number: this.state.number + 1 })
			console.log(this.state) //3
		})
	}
	render() {// 1. Since it is a template string, it cannot be mapped to an instance, so we delegate it to the global DOM (window) element using the event delegate mechanismreturn `<button onclick="trigger(event,'add')">${this.state.number}</button>`
	}
}

new Counter().mounted(document.getElementById('root'))

Copy the code

The code execution looks like this:

The transaction transaction

  1. Transactions: Methods that need to be executed are wrapped in a Wrapper and executed through the Perform method of Transaction
  2. The initialize method is performed before preform, and the close method is performed after perform
  3. We adapt the batch update state change in the delegate method that the code made above to use things
class Transaction { constructor(wrappers) { this.wrappers = wrappers } perform(method) { this.wrappers.forEach((wrap) => Wrap. Initialize ()) method() this.wrappers. ForEach ((wrap) => wrap. Close ())}} isBatchingUpdate:false, // do you need to batch update dirtyComponents: [], // dirtyComponents: the status of components is inconsistent with that displayed on the interfacebatchUpdate() {enclosing dirtyComponents. ForEach ((component) = > {component. UpdateComponet ()})}} / / update the class Updater { Constructor (Component) {this.component.pendingStates = []} // Save the state to update addState(partialState) { This.pendingstates.push (partialState) // Check whether it is batch update, if the component needs to update state is added to dirtyComponnet, Otherwise directly update component batchingStrategy. IsBatchingUpdate? BatchingStrategy. DirtyComponents. Push ponent (this.com) : this.component.updateComponet() } } class Component { constructor(props) { this.props = props this.$updater= new Updater(this)} // Generate a DOM structure based on the HTML string returned by rendercreateDomFromHtmlString() {
		let htmlString = this.render()
		let divDom = document.createElement('div'// Save the generated element to the instance as the old element this.domElement = divdom.children [0] // 2. Copies the current instance of a component to create real dom on the component of this.domElement.com ponent = thisreturn this.domElement
	}

	setState(partialState) {// Status update: // object.assign (this.state, partialState) // re-render // this.updatecomponet () // Save the state that needs to be updated to this.$updater.addState(partialState)
	}

	updateComponet() {// update the status in batches.$updater.pendingStates. ForEach ((partialState) => {object.assign (this.state, partialState)}) // The state to save this.$updater.pendingstates. length = 0 // New element replaces old elementlet oldElement = this.domElement
		letnewElement = this.createDomFromHtmlString() oldElement.parentElement.replaceChild(newElement, OldElement)} / / mounted on mount the dom structure to the designated container (container) {container. The appendChild (enclosing createDomFromHtmlString ())}}let wrappers = [
	{
		initialize() {// Before the method executes, set the batch update totrue
			batchingStrategy.isBatchingUpdate = true
		},
		close() {
			batchingStrategy.isBatchingUpdate = falseBatchingStrategy. BatchUpdate () / / batch updates, }}] const Transaction = new Transaction(wrappers) window.trigger = (event, method) => {// Before the method executes, Set batch update totrue
	// batchingStrategy.isBatchingUpdate = true// 3. Get the current dom instance, and then get the event binding methodletcomponent = event.target.component // component[method].call(component) Transaction.perform (component[method].bind(component)) // After the method executes, set the batch update tofalse
	//batchingStrategy.isBatchingUpdate = false/ / / / batchingStrategy. BatchUpdate () to carry on the batch update, } Class Counter extends Component {constructor(props) {super(props) this.state = {number: constructor(props); 0 } } add = () => { this.setState({ number: this.state.number + 1 }) console.log(this.state) //0 this.setState({ number: this.state.number + 1 }) console.log(this.state) //0setTimeout(() => {
			this.setState({ number: this.state.number + 1 })
			console.log(this.state) //2
			this.setState({ number: this.state.number + 1 })
			console.log(this.state) //3
		})
	}
	render() {// 1. Since it is a template string, it cannot be mapped to an instance, so we delegate it to the global DOM (window) element using the event delegate mechanismreturn `<button onclick="trigger(event,'add')">${this.state.number}</button>`
	}
}

new Counter().mounted(document.getElementById('root'))

Copy the code

conclusion

  1. SetState is when a state update causes the component to re-render
  2. Component re-rendering recreates the DOM element replacement
  3. SetState has batch updates
  4. Batch updating is saving the state that needs to be updated to pendingStates on the current component instance and the components that need to be re-rendered to dirtyComponents
  5. When batch updating, the array of dirty components is iterated over and each is rerendered according to its state
  6. When rerendering, state merge takes the last submitted state that needs to be updated (for a single attribute of the state, the old and new states are merged, not completely overwritten)