1

解密setState

React是通过管理状态来实现对组件的管理。react通过this.state来访问state,通过this.setState()方法来更新state。当this.setState()被调用的时候,react会重新调用render方法来重新渲染UI。

setState异步更新

setState通过一个队列机制实现state更新。当执行setState时,会将需要更新的state合并后放入队列,而不是直接更新this.state。队列机制可以高效的批量更新state,如果不是通过setState而是直接修改this.state,那么state是不会被放入状态队列,当下次调用setState并对状态队列合并时,将会忽略之前被直接修改的state。相关代码如下:

//将新的state合并到状态队列中
var nextState = this._propcessPendingState(nextProps, nextContext);
//根据更新队列和shouldComponentUpdate的状态来判断是否需要更新组件
var shouldUpdate = 
    this._pendingForceUpdate || 
    !inst.shouldComponentUpdate ||
    inst.shouldComponentUpdate(nextProps, nextState, nextContext)

setState循环调用风险

当调用setState时,实际上会执行enqueueSetState方法,并对partialState以及_pendingStateQueue更新队列进行合并操作,最终通过enqueueUpdate执行state更新。

performUpdateIfNecessary方法会获取_pendingElement、_pendingStateQueue、_pendingForceUpdate。并调用receiveComponentupdateComponent方法进行组件更新。

如果在shouldComponentUpdatecomponentWillUpdate方法中调用setState,此时this._pendingStateQueue != null, 则performUpdateIfNecessary方法就会调用updateComponent方法进行组件更新,但updateCompnent又会调用shouldComponentUpdatecomponentWullUpdate方法,因此造成循环调用,使得浏览器内存占满后崩溃。

clipboard.png

看下setState的源码:

//更新state
ReactComponent.prototype.setState = function(partialState, callback) {
    this.updater.enqueueSetState(this, partialState);
    if(callback) {
        this.updater.enqueueCallback(this, callback, 'setState')
    }
}

enqueueSetState: function(publicInstace, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
        publicInstance, 
        'setState'
    )
    
    if(!internalInstance) {
        return
    }
    
    //更新队列合并操作
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = [])
    
    queue.push(partialState)
    enqueueUpdate(internalInstance)
    
    //如果存在_pendingElement, _pengdingStateQueue和_pendingForceUpdate,则更新组件
    performUpdateIfNecessary: function(transaction) {
        if(this._pendingElement != null) {
            ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
        }
        if(this._pendingStateQueue !== null || this._pendingForceUpdate) {
            this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context)
        }
    }
}

setState调用栈

既然setState最终是通过enqueueUpdate执行state更新,那么enqueueUpdate如何更新state的呢?
先看如下例子:

import React, { Component } from "react"

class Comp extends Component {
    constructor(props) {
        super(props)
        this.state = {
            val: 0
        }
    }
    
    componentDidMount() {
        this.setState({val: this.state.val+1})
        console.log(this.state.val)
        this.setState({val: this.state.val+1})
        console.log(this.state.val)
        setTimeout(()=>{
        this.setState({val: this.state.val+1})
        console.log(this.state.val)
        this.setState({val: this.state.val+1})
        console.log(this.state.val)
        })
    }
    
    render() {
        return null
    }
}

上面代码打印出来的分别是0、0、2、3.
如果答案和你心中的不一样,那么enqueueUpdate到底做了什么?

clipboard.png

enqueueUpdate源码:

function enqueueUpdate(component) {
    esureInjected()
    
    //如果不在批量更新模式
    if(!batchingStrategy.isBatchingUpdates) {
        batchingStrategy.batchedUpdates(enequeueUpdate, component)
        return;
    }
    //如果处于批量更新模式,则将该组件保存在dirtyComponent中
    dirtyComponents.push(component)
}

如果isBatchingUpdates为true,则对所有队列中的更新执行batchedUpdates方法,否则只把当前组件(即调用了setState的组件)放入dirtyComponents数组中。

那么batchingStrategy究竟是什么,其实他是一个简单的对象,定义了一个isBatchingUpdates的布尔值,以及batchedUpdates方法。

var ReactDefaultBatchingStrategy = {
    isBatchingUpdates: false,
    batchedUpdate: function(callback, a, b, c, d, e) {
       if(alreadyBatchingUpdates) {
           callback(a,b,c,d,e)
       } else {
           transaction.perform(callback, null, a,b,c,d,e)
       }  
    }
}

该方法中有一个transaction.perform,涉及到事务的概念。

初识事务

clipboard.png

事务就是将需要执行的方法使用wrapper封装起来,再通过事务提供的perform执行。而在perform之前,先执行所有wrapper中的initialize方法,执行完perform之后(即执行完method方法后)再执行所有的close方法。一组initialize以及close方法被称为一个wrapper。从图中可以看到事务支持多个wrapper叠加。

到实现上,事务提供了一个mixin方法供其他模块实现自己需要的事务,而要使用事务的模块,除了需要把mixin混入自己的事务实现中外,还要额外实现一个抽象的getTransactionWrappers接口。这个接口用来获取所有需要封装的前置方法(initialize)和收尾方法(close)。因此它需要返回一个数组对象。每个对象分为有key为initialize和close方法。下面是一个简单使用事务的例子:

var Transaction = require('./Transaction')

//我们自己定义的事务
var MyTransaction = function() {
    //...
}

Object.assign(MyTransacton.prototype, Transaction.Mixin, {
    getTransactionWrappers: function() {
        return [{
            initialize: function() {
                console.log('before method perform')
            },
            close: function() {
                console.log('after method perform')
            }
        }]
    }
})

var transaction = new MyTransaction()
var testMethod = function() {
    console.log('test')
}
transaction.perform(testMethod)
//before method perform
//test
//after method perform

解密

那么事务到底是如何导致前面所述的setState的各种不同表现呢?
首先,我们要了解事务跟setState的不同表现有什么关系?4次setState简单归类,前两次属于一类,因为他们在同一次调用栈执行,setTimeout中的2次setState属于另外一类。

clipboard.png
如图,在componentDidMount中直接调用的两次setState比setTimeout中调用的两次setState,其调用栈简单的多。


fsrookie
2.9k 声望256 粉丝

目前很多文章都是摘抄记录其他教程。见谅。