react源码分析——自己实现异步setState

众所周知,很多人都知道react的this.setState()异步的,并且只能通过这个函数来改变状态。但是,都是知其然,不知其所以然。

其实不是不能通过this.state来改变状态,只是这样的改变并 不会引起组件的更新,因为react不像vue那样监听属性的改变来更新视图,而是通过this.setState()函数的调用来更新视图,前面我们写了一个简单的实现。

import { renderComponent } from '../react-dom'

class Component {
    constructor(props = {}){
        this.props = props;
        this.state={}
    }
    setState(stateChange) {
        Object.assign(this.state, stateChange);
        // 更改参数以后需要重新渲染组件
        renderComponent(this,)
    }
}
export default Component;

就是调用函数后直接更新视图,这是没有做任何的性能优化。如果说遇到下面的情况:

 componentDidMount() {
        console.log('组件加载完成')
        for(let i = 0; i < 10; i++) {
            console.log(i)
            this.setSate({
                num: i
            })
        }
    }

循环更新状态,在很短的时间内,this.setState()调用的10次,可能也有百次千次的情况,这个时候如果我们不做优化,直接渲染组件,this.setState()调用了多少次,组件就更新了多少次,这样就会很消耗性能。还有可能导致内存溢出。 

而真实的this.setState函数内部做了相应的性能优化,使用react框架来测试一下,例如:

import React from 'react'
import ReactDOM  from 'react-dom'

class Home extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            num: 1
        }
    }
    componentWillMount() {
        console.log('组件将要加载')
    }
    componentDidMount() {
        console.log('组件加载完成')
        for(let i = 0; i < 10; i++) {
            console.log(i)
            this.setSate({
                num: i
            })
        }
    }
    componentWillReceiveProps() {
        console.log('组件将要接受新的参数')
    }
    componentWillUpdate() {
        console.log('组件将要更新') 
    }
    componentDidUpdate() {
        console.log('组件更新完成')
    }
    componentWillUnmount() {
        console.log('组件卸载')
    }
    handlerClick() {
        this.setSate({
            num: this.state.num + 1,
        })
    }
    render() {
        return (
            <div className='active'>
              {this.state.num}
              <h1 >react</h1>
              <button key='9' onClick={this.handlerClick.bind(this)}>改变状态</button>
            </div>
        )
    }
}
ReactDOM.render(<Home title='home' />, document.getElementById('app'));

 

我们在组件加载完成的生命周期中,调用了10次this.setState(),可以看到打印了10次,但是组件将要更新和组件更新完成只执行了一次。

这是因为react短时间内会将多个 setState() 调用合并为一次更新。batch update 批量更新,这就是react做的性能优化,下面我们自己来实现一下。

先来分析一下 this.setState()函数的参数,上面提到可一种,对象。

另一种 setState() 的形式,接受一个函数。这个函数将接收前一个状态作为第一个参数,应用更新时的 props 作为第二个参数,代码如下:

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

先来分析一次,this.setDtate()需要做些什么?

首先需要存储上一次的状态,然后延迟更改状态并更新了组件。我们怎么去找这个点呢?刚好最后一个状态的时候去更新组件呢?

react利用了队列的先进先出特性,来实现了这个功能。

我们在react文件夹下面创建一个set-state-queue.js。这个文件负责实现以上的功能。

我们需要两个队列,一个存储当前的状态和相应的组件实例,一个只存储组件实例。

首先,是需要存储传递进来的状态值。然后再去存储当前的组件实例,只存一次当前的组件实例。

我们可以直接把组件的状态挂在组件实例上,便于后面使用。

import { renderComponent } from "../react-dom";

const setSateQueue = []; //存储组件实例及对应的状态

const renderQueue = []; // 存放组件实例

export function enqueueSetState(stateChange, component) {
// 在合适的时候调用flush();
    if(setSateQueue.length === 0) {
        setTimeout(() => {
            flush();
        },0)
    }
        // 短时间内合并多个setSate
    setSateQueue.push({
        stateChange,
        component
    })
    // 看下renderQueue中有没有组件
    let r = renderQueue.some((item)=>item === component);
    if(!r) {
        renderQueue.push(component)
    }

}

flush函数负责更新状态,并在最后一个状态更新完以后,渲染组件。因为flush是异步调用的,所以当开始调用的时候,setSateQueue已经被加入了很多内容。

 stateChange就是当前this.setState()的第一个参数,component是当前组件实例。

stateChange有两种情况,一种是对象,一种是函数。

setSateQueue队列遵循先进先出的原则,直到最后进去的一位取出,这是队列为空,就可以更新改变状态然后渲染组件了。

renderQueue组件队列也是先进先出。

function flush() {
    let item, components;
    while(item = setSateQueue.shift()) {
        const { stateChange,component  } = item;
        // 保存之前状态
        if(!(component && component.prevState)) {
            component.prevState = Object.assign({},component.state)
        }
        if(typeof stateChange === 'function') {
            Object.assign(component.state, stateChange(component.prevState, component.props))
            console.log(component.state)
        } else {
            Object.assign(component.state, stateChange)
        }
        component.prevState = component.state;
    }
    while(components = renderQueue.shift()) {
        renderComponent(components)
    }
}

调整下react/component.js

import { renderComponent } from '../react-dom'
import { enqueueSetState } from './set-state-queue'

class Component {
    constructor(props = {}){
        this.props = props;
        this.state={}
    }
    setSate(stateChange) {
        enqueueSetState(stateChange, this);
    }
}
export default Component;

因为所有的组件都继承Componnet。所有组件都可以直接调用this.setState()。this就是当前组件。

import React from './react'
import ReactDOM  from './react-dom'

class Home extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            num: 1
        }
    }
    componentWillMount() {
        console.log('组件将要加载')
    }
    componentDidMount() {
        console.log('组件加载完成')
        for(let i = 0; i < 10; i++) {
            console.log(i)
            this.setSate({
                num: i
            })
        }
    }
    componentWillReceiveProps() {
        console.log('组件将要接受新的参数')
    }
    componentWillUpdate() {
        console.log('组件将要更新') 
    }
    componentDidUpdate() {
        console.log('组件更新完成')
    }
    componentWillUnmount() {
        console.log('组件卸载')
    }
    handlerClick() {
        this.setSate({
            num: this.state.num + 1,
        })
    }
    render() {
        return (
            <div className='active'>
              {this.state.num}
              <h1 >react</h1>
              <button key='9' onClick={this.handlerClick.bind(this)}>改变状态</button>
            </div>
        )
    }
}
ReactDOM.render(<Home title='home' />, document.getElementById('app'));

实现了跟真实react一样的效果。用react测试一下,setState

 

import React, {Component, PureComponent } from 'react';

class App extends Component {
  state = {
    count: 0
  };

  componentDidMount() {
    // 生命周期中调用
    this.setState({ count: this.state.count + 1 });
    console.log("lifecycle: " + this.state.count);
    setTimeout(() => {
      // setTimeout中调用
      this.setState({ count: this.state.count + 1 });
      console.log("setTimeout: " + this.state.count);
    }, 0);
    document.getElementById("div2").addEventListener("click", this.increment2);
  }

  increment = () => {
    // 合成事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("react event: " + this.state.count);
  };

  increment2 = () => {
    // 原生事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("dom event: " + this.state.count);
  };

  render() {
    return (
      <div className="App">
        <h2>couont: {this.state.count}</h2>
        <div id="div1" onClick={this.increment}>
          click me and count+1
        </div>
        <div id="div2">click me and count+1</div>
      </div>
    );
  }
}
export default App;

 

探讨前,我们先简单了解下react的事件机制:react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClickonChange这些都是合成事件。

那么以上4种方式调用setState(),后面紧接着去取最新的state,按之前讲的异步原理,应该是取不到的。然而,setTimeout中调用以及原生事件中调用的话,是可以立马获取到最新的state的。根本原因在于,setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套。

 多次请求合并,提高性能:

increment = () => {
    // 合成事件中调用
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: 3});
    this.setState({ count: 4 });
    
  };
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

异步里面多次调用不会合并:

 increment = () => {
    // 合成事件中调用
    
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      this.setState({ count: 3});
      this.setState({ count: 4 });
    })
    
  };
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

总结:

这里只是介绍了react的一些思想,真正的源码远不是这么简单,想了解更多的,可以直接去解读源码,大致思想是一致的。

  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  • setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setStatesetState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值