模擬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 地址

相關文章
相關標籤/搜索