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