开发直呼内行,解读react的setSate的异步问题

时间: 2018-12-22阅读: 498标签: 异步

时间: 2019-12-01阅读: 81标签: react

react官网教程基础解析

在我们阅读文档的时候,大多都说react的setState是异步的,可是它真的是异步的吗?如果是,那我们还可以猜想:那可以不可以同步?那什么时候需要异步,什么时候需要同步呢?

众所周知, React 是通过管理状态来实现对组件的管理,而setState是用于改变状态的最基本的一个方法,虽然基础,但是其实并不容易掌握,本文将结合部分源码对这个方法做一个相对深入的解析。

1、使用redux和没有redux,react写法有什么不同吗?

答:组件写法一样,但是state不一定交给组件内部管理,可能放到store上统一管理。

我们先来看下react的官方对setSate的说明:

基本用法

2、认识react,一个hello world!

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

将setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。

首先官方文档给出的,它的基本API:

3、如何使用react?

答:推荐你使用ES6语法来写react,首先你需要Babel编译你的ES6代码,其次,你才可以使用比如 => (箭头函数),class(类),模板文字,let和const语句等ES6语法。

一个很经典的例子:

// 接受2个参数,updater 和 callbacksetState(updater[, callback])// 第一种:updater可以是对象setState({ key: val}, newState={ // callback里可以获取到更新后的newState})// 第二种:updater可以是函数,返回一个对象值setState((state, props)={ return { key: val }}), newState={})

4、JSX介绍

答:JSX是一种表达式,它有一个根标签,在内部可以嵌入表达式,使用{}(大括号)包裹起来。它看起来就是html的一部分,或者叫一个html模块。

class T extends React.Component {
    render() {
        return <div className="left-enter" style={}>{value}</div>
    }
}

从上面的代码例子你可以看到几个和html不同的地方,class =》className,style是一个object,你还可以在dom元素中使用{}插入数据。

使用JSX还可以防止XSS(跨站脚本攻击),因为JSX只是表达式,它需要先转换成字符串,然后才能渲染到真实DOM上面,但对于真正的黑客来说,这种做法也不是安全的。

// 初始state.count 当前为 0componentDidMount(){ this.setState({count: state.count + 1}); console.log(this.state.count)}

其中updater表示新的state值可以是返回一个对象的函数,也可以直接是一个对象。这部分的内容会通过浅比较被合并到state中去。官方文档很明确的告诉我们:

4、元素和组件的概念

react组件:

class T extends React.Component {
    render() {
        return <div className="left-enter" style={}>{value}</div>
    }
}

react元素:

<div className="left-enter" style={}>{value}</div>

如果你熟悉react,你一定知道最后的输出结果是0,而不是1。

setState 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

5、组件的使用

函数组件:函数组件没有状态和生命周期,但是你可以返回一个react元素。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

class组件:非常强大,有自己的state和生命周期。和函数组件一样,class组件也需要返回一个react元素。

class Welcome extends React.Component {
  componentWillMount() {}
  componentDidMount() {}
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

在一个庞大复杂的网站应用中,要如何拆分组件呢?官网上说组件拆分的越细,复用性就越强,从实际开发中来看,这个说法没有错,但是
会带来一个比较严重的问题,就是组件太多,管理起来不方便。有人使用第三方react组件的时候,只有那些文档非常强大的开源组件
才能给你的开发提高效率。如果你自己的组件也想拆分到细致,那么写好文档是最重要的一步。

react还提到了一点,传递给组件的数据是"只读"的,要保证组件中的数据是"纯数据",输入即输出。那么,如果你需要在组件中修改props.data
该怎么做呢?

render() {
    const { data } = this.props
    //定义一个新的变量来保存修改后的值。
    let _data = data + 1;
}

我们再来看一个例子

因此这个api的第二个参数callback,允许我们在setState执行完成后做一些更新的操作。

6、组件的状态和生命周期

前面我们提到组件分为函数组件和类组件,函数组件是无状态,类组件有状态和生命周期。

什么是状态?

答:通俗理解,就是组件不同时候的不同表现,比如,一个按钮组件,可能有激活状态,不可点击状态,显示状态,隐藏状态等,在react用state来保存这些状态。
而state本身不仅仅表示组件状态,还可以保存组件的数据。

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        isShow: true,
        text: props.text,
        disabled: true
    };
  }

  render() {
      const { isShow, text, disabled} = this.state
      return <button disabled={disabled} style={{display: isShow ? "block" : "none"}}>{text}</button>
  }
}

如果要修改state,请使用,注意,你不能在render函数里面直接修改state,而是要通过事件去触发state更新。

this.setState({
    isShow: false,
    disabled: false
})

由于setState有批处理功能,所以该方法可能不一定同步更新,如果你需要依赖上一次的状态和本次状态的计算,那么需要写成下面这种形式。

this.setState((prevState, props) => {    
      text: prevState.text++
    });

demo网址:http://codepen.io/hyy1115/pen/GmdOKJ?editors=0011

有时候,子组件不需要关注自身的状态,而是通过父组件的状态来改变,这时候的子组件可以写成函数形式,通过props传递父组件给的状态。

react生命周期
生命周期表示组件的一生,从出生到辉煌到死亡,中间最主要也是最常用的3个状态是:

componentWillMount:出生了,把组件的状态和属性都设置好。

componentDidMount:渲染出来了,我不再是JSX,而是真实DOM了。

componentWillUnmount:要死了,死之前把遗产处理好。

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        isShow: true,
        text: props.text,
        disabled: true
    };
  }

  componentWillMount() {
      //出生了,可以给我数据和设置我的状态
  }
  componentDidMount() {
      //活着多好
  }
  componentWillUnmount() {
      //要死了,把我的一生痕迹都清除
  }

  render() {
      const { isShow, text, disabled} = this.state
      return <button disabled={disabled} style={{display: isShow ? "block" : "none"}}>{text}</button>
  }
}

还有其他几个生命周期,并不是非常常用,需要用到的时候去看下别人的博客。

class Demo extends Component { constructor(props) { super(props); this.state = { number: 0 }; } render() { return button ref="button" onClick={this.onClick.bind(this)}点我/button; } componentDidMount() { //手动绑定mousedown事件 this.refs.button.addEventListener( "mousedown", this.onClick.bind(this) ); //setTimeOut setTimeout(this.onClick.bind(this), 1000); } onClick(event) { if (event) { console.log(event.type); } else { console.log("timeout"); } console.log("prev state:", this.state.number); this.setState({ number: this.state.number + 1 }); console.log("next state:", this.state.number); }}export {Demo};

以上稍微回顾下基础知识部分,接下来我们正式开始详细探讨。

7、事件处理

 <button onClick={(e) => this.handleClick(e)}>
 按钮
</button>

<input type="text" onChange={(e) => this.handleClick(e)} />

在这个组件中采用3中方法更新state

关于第一个函数参数

8、条件渲染

前面button的例子我们已经使用到了条件渲染,条件渲染通过state来判断,常用的是控制style、className、DOM属性,JSX。

举几个常用的例子。

render() {
    return (
        <div>
        {
            this.state.isShow && <button>按钮</button>    
        }
        </div>
    )
}

render() {
    return (
        <div>
        {
            this.state.isShow ? <button>按钮</button> : 文本
        }
        </div>
    )
}

render() {
    return <button disabled={this.state.disabled}>按钮</button>
}
 1.在div节点中绑定onClick事件 2.在componentDidMount中手动绑定mousedown事件 3.在componentDidMount中使用setTimeout调用onClick

为了避免枯燥,我们带着问题来继续研究:

9、列表渲染

2个注意点:

数组要判断是否为空;

必须给一个key。

render() {
    const { arr } = this.state
    return arr.length > 0 && arr.map((value, key) => <li key={key}>{value}</li> )
}

在点击组件后,你可以猜到结果吗?输出结果是:

问题1:setState使用函数参数和对象参数有何区别?在回答这个问题之前,请先看这个很常用的计时器的例子:

10、表单

我曾经经历过的一次阿里的面试,就考到了react表单的知识点。

受控组件:由react控制输入的表单组件。

在下面的例子中,input的value值由state来决定,用户输入触发onChange事件,然后更新state,达到修改value的目的。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  render() {
    return (
          <input type="text" value={this.state.value} onChange={this.handleChange} />
    );
  }
}

或许你没看出来和正宗input元素的区别,看一个真实DOM元素的例子,value由inupt自身维护,我们没有给value绑定值。

<input type="text">

textarea和input是一样的用法。

select有些许不同,将value绑定到select上,而不是option。

<select value={this.state.value} onChange={this.handleChange}>
    <option value="1">1</option>
    <option value="2">2</option>
</select>

还有一种是多个输入框的情况,比如登录,有账号、密码等,这时候操作这些不同的input可以通过ref或者name,class,id等方法去setState,看
官方demo。

class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
      </form>
    );
  }
}

不受控组件:很简单,就是DOM自己维护状态的组件,不受react控制。你可以给它设置defaultValue,但是不能去setState。

<input type="text" ref={(input) => this.input = input} defaultValue="默认值"/>

相信有人会试过设置defaultValue之后执行了setState去修改value,这样做控制台会发出警告。

总结:受控组件是指受react控制的组件,表单组件中的value和state同步,不受控组件是指不受react控制的组件,表单组件中的
value不通过state同步,只能操作DOM去读取value。

timeoutprev state: 0next state: 1mousedownprev state: 1next state: 2clickprev state: 2next state: 2
class Demo extends Component { constructor(props) { super(props); this.state = { count: 0 //初始值 }; } increaseCount = () = { this.setState({count: this.state.count + 1}); } handleClick = ()={ this.increaseCount(); } render() { return ( div className="App-header" button onClick={this.handleClick}点击自增/button count:{this.state.count} /div ); }}

11、状态提升

你一定听说过变量提升,函数提升,那么状态提升是什么呢?

首先你得了解双向绑定和单向数据流,双向绑定中,数据可以在不同的组件之间实现共享,这样做的确有很大的好处,但是在react中,
不推荐使用双向绑定,而是使用状态提升的方式。

记得和阿里的一个面试官聊的时候,他要求我用react实现双向绑定,而我认为react应该采用状态提升来实现。最后没说服他,或许让Dan来
和他聊聊才有用,哈哈。

状态提升:state推崇单向数据流,数据从父组件通过props流向子组件,如果你在子组件中,需要修改state来和其他子组件共享数据更新,
你需要使用回调函数给使数据更新给父组件,然后从父组件流向其他的子组件,这样做是保证数据有单一的来源。

如果实子组件和子组件之间任意共享数据,那么,后期维护会比较痛苦,特别是找bug的时候。

看一个状态提升的例子吧。

class Child extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.upDateValue(e.target.value);
  }

  render() {
    const {name, value} = this.props;
    return (
      <div>
        <p>{name}:</p>
        <input value={value}
               onChange={this.handleChange} 
          />
      </div>
    );
  }
}

class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: '', name: ''};

    this.upDateValue = this.upDateValue.bind(this);
  }

  upDateValue(value) {
    this.setState({value: value})
  }

  render() {
    const {value} = this.state;

    return (
      <div>
        <Child name="组件1" value={value} upDateValue={this.upDateValue} />
        <Child name="组件2" value={value} upDateValue={this.upDateValue} />
      </div>
    );
  }
}

ReactDOM.render(
  <Demo />,
  document.getElementById('root')
);

demo网址:http://codepen.io/hyy1115/pen/xdjoZQ?editors=0011

结果似乎有点出人意料,三种方式只有在div上绑定的onClick事件输出了可以证明setState是异步的结果,另外两种方式显示setState似乎是同步的。

这个代码看起来没有什么问题, 每次点击的时候也能自增1,完全符合预期效果。但是接下来!我们希望通过改动handleClick,使得每次点击时,count自增2次,即:

12、选择组合还是继承?

用过原生js或者jQuery的同学可能对基础非常熟悉,继承可以实现扩展很多功能。

在react组件开发中,我们的每个react组件都是继承于React.Component。

class MyComponent extends React.Component {

}

我们不推荐你继承MyComponent。

//不推荐
class NextComponent extends MyComponent {

}

你应该充分利用react组件的强大性能,开发各种你需要的组件继承至React.Component。组件之间的嵌套非常强大,你可以嵌套函数组件,嵌套类组件。

详情前往:https://facebook.github.io/react/docs/composition-vs-inheritance.html

总结:1.在组件生命周期中或者react事件绑定中,setState是通过异步更新的。2.在延时的回调或者原生事件绑定的回调中调用setState不一定是异步的。

 handleClick = ()={ this.increaseCount() this.increaseCount() }

这个结果并不说明setState异步执行的说法是错误的,更加准确的说法应该是setState不能保证同步执行。

这时候就会惊奇地发现,每次点击后,count还是自增1!问题出在哪里呢?

其实就是this.setState({count: this.state.count + 1});这种写法,由于前面提到了setState并非同步方法,所以这里的this.state.count并不能保证取到最新的值,这时候我们可以采用第二种写法:

 // 这里我们用的函数参数的方式 increaseCount = () = { this.setState((state)={return{count: state.count + 1}}); }

这时候再试试,发现计时器可以按照预期效果执行,此时可以回答问题1了:如果setState时需要根据现有的state来更新新的state,那么应该使用函数参数来保证取到最新的state值。

答案1: 如果需要依赖当前state的值来更新下一个值的情况,需要使用函数作为参数,因为函数才能保证取到最新的state

关于批量更新

接下来要研究的,就是重头戏--setState更新过程,可能你看过的文档都告诉你,setState不会保证立即执行,而是会在某个时机批量更新所有的component。

那么问题来了: 为什么要设定这个批量机制,这个批量更新的过程到底又是如何执行的呢?

问题2: setStates为什么要设定批量更新机制?

这一点其实是处于大型应用的性能考虑,首先我们都知道,component的render是很耗时的。想象这种场景:

如果在某个复合组件由一个Parent和一个Child组成,在一个click事件中,Parent组件和Child组件都需要执行setState,如果没有批量更新的机制,那么首先父组件的setState会触发父组件的re-render并且也会触发一次子组件的render,而子组件自己的setState还要触发一次它自身的re-render,这样会导致Childrerender两次,批量更新机制就是为了应对这种情况而产生的。

所以紧接着问题来了:

问题3:批量更新的过程是怎么执行的

为了回答这个问题,我整理了一下react中setState相关的源码(源码学习的步骤放在最后,有兴趣的小伙伴可以阅读,想直接看结论也可以略过),抛开一些对主流程影响不大的细节(去掉了一些错误抛出之类的代码,提高阅读效率),梳理出这样的一个大概的流程:(如果图被压缩看不清请点击)

大致分为以下阶段:

首先判断执行setState的上下文环境,是否处于事件系统或者Mount周期中(这一点很重要!!!,后面会详细说明)某个component执行setState将新的state值partialState放入component对应的instance变量,这里简单介绍下,react在内存中为每个component创建了一个对应的instance对象,用来保存对应的一些属性,方便在更新以及其他过程时清晰的使用component对应的属性。把缓存完变量的component,放入全局变dirtyComponents数组中,根据第一步的判断,判断目前是否要立即批量更新(如果是则直接更新,如果正处于handle event或者mount阶段,则等到阶段末尾再执行更新)

PS:这个过程中的关键步骤,react15.6的源码是使用了事务transition的写法来实现的,但是我认为对于解释setState的内容并非必要的,所以在本文不深入说明,剥离出来说明是为了让读者更容易理解。关于transition如果有必要,之后另外写文章说明

从以上的描述可以看到,我把判断执行setState的上下文环境放在开头,为什么要这样呢,接下来我们看另外一个有趣的例子,把最前面的计时器例子,稍微改变一下:

class Main extends Component { constructor(props) { super(props); this.state = { count: 0 //初始值 }; } // 注意这个函数的改动 increaseCount = () = { this.setState({count: this.state.count + 1}); console.log("第1次输出", this.state.count) setTimeout(()={ this.setState({count: this.state.count + 1}); console.log("第2次输出", this.state.count) this.setState({count: this.state.count + 1}); console.log("第3次输出", this.state.count) },0) } handleClick = ()={ this.increaseCount(); } render() { return ( div className="App-header" button onClick={this.handleClick}点击自增/button count:{this.state.count} /div ); }}

点击一下按钮之后,你能直接回答出上面的3次输出分别为多少吗?

答案:三次输出分别是0, 2, 3.

首先对于第一个console,对照上面的流程可知,setState执行时正处于click事件handle阶段,因此本次的更新将被放入更新队列并推迟更新,因此立即console无法获得最新的结果(类似的 如果我们在componentwillMount等生命周期阶段进行setState操作并立刻console也拿不到最新的值,因为也得走批量更新的路线);所以第一次输出是0其次第二个和第三个setState被放在setTimeout中,之前我在写异步事件队列的时候有说过,由于js的单线程,所有异步的操作都会被放在异步队列里,因此这两次调用setState时,函数调用栈和第一次是不一样的,它们并没有处于事件handle或者component Mount阶段,因此调用setState后,会立即执行批量更新(其实这时候也会把当前组件放入dirtyComponents中,只是此时恰好只有一个dirtyComponent,就会被执行批量更新),因此,后面两次的更新可以立刻拿到变更的值, 因此分别输出23

其实到这里,我们就应该思考一个问题:问题4: 为什么React特意在Mount过程和事件处理系统中安排批量更新机制呢?答案4:回想起那么说过的设置批量更新的初衷,是为了减少整个应用内非必要的render从而提升性能,而最有可能需要render的时机其实就是:

Mount一个component的阶段,本身存在render过程;事件处理函数内部,经常有可能是多个组件,可能对于一个事件都进行了setState操作;

综上所述,不难猜想,其实React应该是希望在所有的地方都强制控制setState进行异步批量更新,而从目前版本(本文所用的源码是15.6)来说,能够逾越这个控制的,一般是只有手动setTimeout或者promise.then(常见于请求数据之后更新某个state)。

附录 相关源码阅读顺序

这部分是我个人关于setState在react源码的阅读顺序,仅供参考,希望可以对研究源码的小伙伴有所帮助:

// 1. react-15.6.0/src/isomorphic/modern/class/ReactBaseClasses.jsReactComponent.prototype.setState = function(partialState, callback) { // 省略错误捕获和异常处理 // updater实际是注入的 可以直接全局查找enqueueSetState方法 this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); }};// 通过全局查找enqueueSetState方法,找到以下内容// 2.react-15.6.0/src/renderers/shared/stack/reconciler/ReactUpdateQueue.jsenqueueSetState:function(publicInstance, partialState){// 这里可以简单理解为: 当前component上保存一个_pendingStateQueue数组,值为partialState,internalInstance实际上是内存中与当前component对应的一个变量,专门用来存储当前component对应的属性 var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState', ); if (!internalInstance) { return; } var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance);}// 查找enqueueUpdate方法// 3. react-15.6.0/src/renderers/shared/stack/reconciler/ReactUpdateQueue.jsenqueueUpdate: function enqueueUpdate(internalInstance) { ReactUpdates.enqueueUpdate(internalInstance);}// 查找ReactUpdates的enqueueUpdate// 4.react-15.6.0/src/renderers/shared/stack/reconciler/ReactUpdates.js// 这里if (!batchingStrategy.isBatchingUpdates)便是批量更新执行的关键分支语句ReactUpdates.enqueueUpdate = function (component){ // 如果不在批量更新 那直接执行 if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 如果在 那存起来 延后执行 dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; }}// 5.先看!batchingStrategy.isBatchingUpdates路线查找// react-15.6.0/src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js// 这里要很注意,每次调用batchedUpdates 都会使ReactDefaultBatchingStrategy.isBatchingUpdates 变成 true;所以必须全局查找 调用batchingStrategy.batchedUpdates()的地方,查找后会发现是在Mount和eventHandle过程,这也就是得出前面流程图里,得出第一步骤的关键点var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, /** * Call the provided function in a context within which calls to `setState` * and friends are batched such that components aren't updated unnecessarily. */ batchedUpdates: function(callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations if (alreadyBatchingUpdates) { return callback(a, b, c, d, e); } else { // 这个地方用了transaction的写法,看起来稍微有点绕,建议稍微了解下transaction的概念 return transaction.perform(callback, null, a, b, c, d, e); } },}; // 6.flushBatchedUpdates 遍历dirtyComponents 循环执行runBatchedUpdatesar flushBatchedUpdates = function() { while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); transaction.perform(runBatchedUpdates, null, transaction); ReactUpdatesFlushTransaction.release(transaction); } if (asapEnqueued) { asapEnqueued = false; var queue = asapCallbackQueue; asapCallbackQueue = CallbackQueue.getPooled(); queue.notifyAll(); CallbackQueue.release(queue); } }}// 这是最后遍历更新的地方 其实没什么好看的了function runBatchedUpdates(){ // 遍历所有的dirtyComponents 依次更新component 并缓存callbacks}

小结

(看了一眼上一篇又过去了接近3个月-_-!)本文主要针对setState的批量更新过程和常见几个问题做了一些相关解析,希望对大家有帮助,关于transaction以及具体的更新比对过程后续找机会另外说明(理直气壮鸽子王-_-),如果读者有感兴趣的其他话题也欢迎提出来探讨。

原文:

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:开发直呼内行,解读react的setSate的异步问题

相关阅读