模拟React中的setState方法

setState是react开发中很重要的一个方法,在react的官方文档中介绍了setState正确使用的三件事:html

  • 不要直接修改 State
  • State 的更新多是异步的
  • State 的更新会被合并

State 的更新会被合并

官方文档中讲到,出于性能的考虑,react一般会把多个setState()合并成一个调用,从而提升渲染的性能,所以下面的代码,实际上只更新了一次:react

this.state = {index:0};
componentDidMount(){
    this.setState({ index: this.state.index + 1 })  // {index:0}
    this.setState({ index: this.state.index + 1 })  // {index:0}
}

若是要解决这个问题,能够在setState中传入回调函数:git

this.state = {index:0};
componentDidMount(){
    this.setState((state)=>({index:state.index+1}));
    this.setState((state)=>({index:state.index+1}));
}

State 的更新多是异步的

this.state = {index:0};
componentDidMount(){
    this.setState({ index: this.state.index + 1 })  //{index:0}
    this.setState({ index: this.state.index + 1 }) // {index:0}

    setTimeout(() => {
      this.setState({ index: this.state.index + 1 }) //{index:2}
      this.setState({ index: this.state.index + 1 }) //{index:3}
    })
}

能够看到,在didMount函数中,setState的执行结果在做用域内和异步函数内的区别。github


这篇文章主要讲如何经过原生js,模拟setState的执行机制,从而更好地了解setState方法的使用:数组

需求分析

咱们模拟一个计数器按钮,每点击一次按钮,数字+1缓存

<button>0</button>

1、初始化文件

第一步,生成index.html模板文件,编写渲染所需的dom结构:app

// index.html
<div id="root"></div>

咱们将会把渲染所需的结构放入容器中dom

2、编写渲染所须要的方法

第二步骤,生成counter.js脚本文件,编写渲染所需的方法和存储数据的对象state,咱们一般会用到这几个方法:异步

  • render (渲染所需的dom结构)
  • getElement (获取到真实的dom,方便添加事件和方法)
  • mounted (初始化完成的方法,接收绑定的元素id做为参数)

咱们编写一个类来初始化咱们的方法函数

//counter.js
class Counter {
  constructor(){
    this.domEl = null;
    this.state = {index:0}
  }
  getElement(){
    let _dom = document.createElement("div");
    _dom.innerHTML = this.render();
    return _dom.children[0]
  }
  render(){
    return `<button>${this.state.index}</button>`
  }
  mounted(id){
    this.domEl = this.getElement();
    document.getElementById(id).appendChild(this.domEl)
  }
}

这样,经过初始化实例,并将id传入mounted方法,就能够在页面中看到渲染出的按钮标签了

new Counter().mounted("root");

3、编写组件所须要的方法

接着咱们还须要编写一些可复用的方法和属性,例如setStateupdate方法,方便数据更新和页面渲染,同时咱们也能够抽离getElementmounted方法还有domEl属性到通用的类中,方便后续调用,这些咱们能够编写Component类来实现:

//component.js
class Component {
  constructor() {
    this.domEl = null
  }
  setState(state){ // 新增setState方法,更新state数据
    Object.assign(this.state,state);
    this.update();
  }
  getElement() {
    let _dom = document.createElement('div')
    _dom.innerHTML = this.render()
    return _dom.children[0];
  }
  update(){ // 新增更新方法,渲染改变后的dom元素
    let oldDom = this.domEl;
    let newDom = this.getElement();
    this.domEl = newDom;
    oldDom.parentNode.replaceChild(newDom, oldDom)
  }
  mounted(id) {
    this.domEl = this.getElement()
    document.getElementById(id).appendChild(this.domEl)
  }
}

新增setStateupdate方法为了更新数据和dom,同时修改counter.js:

class Counter extends Component{
  constructor(){
    super();
    this.state = {index:0}
  }
  add(){
    this.setState({index:this.state.index+1});
  }
  render(){
    return `<button>${this.state.index}</button>`
  }
}

4、触发事件

咱们给Counter新增了add方法,但愿点击按钮后能够调用这个方法,为了给按钮绑定事件,我须要注册一个全局方法,方便在调用这个方法后,触发对应的事件,这里我新建了trigger.js,里面建立trigger函数:

//trigger.js
function trigger(event,method,...params) {
  let component = event.target.component
  component[method].apply(component, params);
}

同时咱们也须要需改render方法,添加trigger事件

//counter.js
render(){
    return `<button onclick="trigger(event,'add')">${this.state.index}</button>`
}

咱们往trigger方法里面传入了event事件和字符串add,但愿在trigger里面拿到对应的方法名并执行,这须要咱们在getElement的时候提早把类方法绑定到节点元素中:

//component.js
  getElement() {
    let _dom = document.createElement('div')
    _dom.innerHTML = this.render();
    let el = _dom.children[0];
    el.component = this; // 把本身绑定到component属性中
    return el;
  }

这样咱们就实现了,点击按钮增长数字的效果,同时,咱们也明白为何react里面 直接修改state 并不能从新渲染组件;

// 直接修改index,不会触发页面更新
this.state.index = 2;

固然上面的代码并无像react的setState方法拥有异步和批量更新的效果,咱们继续优化代码

5、批量任务管理

为了达到批量更新的效果,咱们新建batchingStrategy的脚本,往里面新增代码

// batchingStrategy.js

const batchingStrategy = {
  isBatchingUpdates: false, // 是否批量更新
  updaters: [], //存储更新函数的数组 
  batchedUpdates(){} // 执行更新方法
}

batchingStrategy对象用来管理咱们批量更新的任务和状态,同时咱们新建Updater类来处理是否须要缓存批量任务,新建updater.js并往里面新增代码:

//updater.js

class Updater {
  constructor(component) {
    this.component = component
    this.pendingStates = [] // 暂存须要更新的state
  }
  addState(particalState) {
    this.pendingStates.push(particalState);
    
    if (batchingStrategy.isBatchingUpdates) {
      batchingStrategy.updaters.push(this)
    } else {
      this.component.updateComponent()
    }
    
  }
}

咱们在Component里面实例化Update,并新增updateComponent方法和修改addSate方法

//component.js
    
   class Component {
    constructor(props) {
      this.props = props;
      this.domEl = null;
      this.$updater = new Updater(this);
    }
    addState(state) {
        this.$updater.addState(state)
    }
    updateComponent(){
      while (this.$updater.pendingStates.length) {
        this.state = Object.assign(
          this.state,
          this.$updater.pendingStates.shift()
        )
      }
      this.update()
    }
  }

同时,在触发事件trigger函数里面,将方法执行前的isBatchingUpdates 置成 true,而且在方法执行完后,再重为false并触发batchedUpdates方法:

function trigger(event,method,...params) {
  batchingStrategy.isBatchingUpdates = true;

  let component = event.target.component
  component[method].apply(component, params);

  batchingStrategy.isBatchingUpdates = false
  batchingStrategy.batchedUpdates()

}

由于咱们在updater.js里面将component类放入批量任务管理器中的updaters数组中,因此batchedUpdates方法里面,咱们能够把updaters缓存的方法拿出来依次执行:

// batchingStrategy.js
batchedUpdates() {
    while (this.updaters.length) {
      this.updaters.shift().component.updateComponent()
    }
 }

以上就是setState更新的整个过程,咱们能够看下组件更新的流程图

setState.png

咱们再修改add方法里面的setState方法,能够看到它会集齐一批须要更新的组件而后一块儿更新:

//counter.js
add(params){
    this.setState({index:this.state.index+1}); //{index:0}
    this.setState({index:this.state.index+1}); //{index:0}
    setTimeout(() => {
      this.setState({index:this.state.index+1}); //{index:2}
      this.setState({index:this.state.index+1}); //{index:3}
    }, 0);
  }

6、事务

最后,咱们能够给咱们的代码添加上事务(transaction),优化咱们的代码,简单说明一下transaction对象:

  • transaction 就是将须要执行的方法使用 wrapper 封装起来,再经过 transaction 提供的 perform 方法执行
  • perform 以前,先执行全部 wrapper 中的 initialize 方法;perform 完成以后(即 method 执行后)再执行全部的 close 方法
  • 一组 initializeclose 方法称为一个 wrapper
/**
 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>
 */

咱们生成transaction.js脚本并编写对应的方法:

//transaction.js
class Transaction {
  constructor(wrappers) {
    this.wrappers = wrappers
  }
  perform(func) {
    this.wrappers.forEach((wrapper) => wrapper.initialize())
    func.call()
    this.wrappers.forEach((wrapper) => wrapper.close())
  }
}

const transact = new Transaction([
  {
    initialize() {
      batchingStrategy.isBatchingUpdates = true
    },
    close() {
      batchingStrategy.isBatchingUpdates = false
      batchingStrategy.batchedUpdates()
    },
  },
])

接着修改trigger方法

function trigger(event,method,...params) {
  let component = event.target.component;
  transact.perform(component[method].bind(component, params))
}

这样咱们的模拟的setState方法就大功告成了!

github 地址

相关文章
相关标签/搜索