2

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

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

State 的更新会被合并

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

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

如果要解决这个问题,可以在setState中传入回调函数:

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的执行结果在作用域和异步函数的区别。

需求

模拟一个计数器按钮,每点击一次按钮,数字累加。

一、初始化

编辑index.html,编写渲染所需的element:

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

二、渲染

编辑counter.js,完成下面函数编写:

  • render (渲染所需dom)
  • getElement (获取真实的dom,添加事件和方法)
  • mounted (初始化完成回调,接收绑定元素id作为参数)
class Counter {
  constructor(){
    this.domEl = null;
    this.state = {index:0}
  }
  getElement(){
    const 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)
  }
}

渲染到 root:

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

三、更新

Component 负责新增setStateupdate更新和重新渲染节点:

class Component {
  ...
  setState(state){ // 新增setState方法,更新state数据
    Object.assign(this.state,state);
    this.update();
  }
  update(){ // 新增更新方法,渲染改变后的dom元素
    let oldDom = this.domEl;
    let newDom = this.getElement();
    this.domEl = newDom;
    oldDom.parentNode.replaceChild(newDom, oldDom)
  }
  ...
}

继承 Component,新增 add 方法触发更新:

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>`
  }
}

四、事件

给按钮绑定事件:

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

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

为了让 trigger 获得元素 target,获取元素前做绑定操作:

  getElement() {
    const dom = document.createElement('div')
    dom.innerHTML = this.render();
+  const el = dom.children[0];
+   el.component = this; // 把自己绑定到component属性中
+   return el;
  }

事件添加完成,点击按钮可以实现数字累加。

五、批量任务管理

任务管理器:

const batchingStrategy = {
  isBatchingUpdates: false, // 是否批量更新
  updaters: [], //存储更新函数的数组 
  batchedUpdates(){} // 执行更新方法
}
batchingStrategy对象用来管理批量更新的任务和状态。

缓存批量任务更新器:

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()
    }
    
  }
}

实例化Update,新增updateComponentaddState:

class Component {
    constructor(props) {
      this.props = props;
      this.domEl = null;
      this.$updater = new Updater(this);
    }
    setState(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 // 更新start
  • 缓存更新方法
  • isBatchingUpdates = false // 更新end
  • 执行缓存区更新方法
function trigger(event,method,...params) {
  batchingStrategy.isBatchingUpdates = true;

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

  batchingStrategy.isBatchingUpdates = false
  batchingStrategy.batchedUpdates()
}

...
//依次执行
batchedUpdates() {
    while (this.updaters.length) {
      this.updaters.shift().component.updateComponent()
    }
 }

更新的流程图

setState.png

现在add方法执行结果如下:

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);
  }

六、事务

事务(transaction

目的:优化代码结构

原理:initCallbacks => customCallbacks => endCallbacks[]

  • 依次执行初始化函数数组 type initialize = Function[]
  • 执行用户自定义函数 customCallbacks()
  • 依次执行结束函数数组 type close = Function[]
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()
    },
  },
]);

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

源码地址


chenwl
117 声望5 粉丝

平坦的路面上曲折前行