setState是react開發中很重要的一個方法,在react的官方文檔中介紹了setState正確使用的三件事:html
官方文檔中講到,出於性能的考慮,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})); }
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>
第一步,生成index.html
模板文件,編寫渲染所需的dom結構:app
// index.html <div id="root"></div>
咱們將會把渲染所需的結構放入容器中dom
第二步驟,生成counter.js
腳本文件,編寫渲染所需的方法和存儲數據的對象state
,咱們一般會用到這幾個方法:異步
咱們編寫一個類來初始化咱們的方法函數
//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");
接着咱們還須要編寫一些可複用的方法和屬性,例如setState
和update
方法,方便數據更新和頁面渲染,同時咱們也能夠抽離getElement
和mounted
方法還有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) } }
新增setState
和update
方法爲了更新數據和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>` } }
咱們給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方法擁有異步和批量更新的效果,咱們繼續優化代碼
爲了達到批量更新的效果,咱們新建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更新的整個過程,咱們能夠看下組件更新的流程圖
咱們再修改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); }
最後,咱們能夠給咱們的代碼添加上事務(transaction
),優化咱們的代碼,簡單說明一下transaction對象:
wrapper
封裝起來,再經過 transaction 提供的 perform 方法執行perform
以前,先執行全部 wrapper
中的 initialize
方法;perform
完成以後(即 method 執行後)再執行全部的 close
方法initialize
及 close
方法稱爲一個 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 地址