同步 setState 的问题

而在现有 setState 逻辑实现中, 每调用一次 setState 就会执行 render 一次。因此在如下代码中, 每次点击增加按钮, 因为 click 方法里调用了 10 次 setState 函数, 页面也会被渲染 10 次。而我们希望的是每点击一次增加按钮只执行 render 函数一次。

export default class B extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
this.click = this.click.bind(this)
}
click() {
for (let i = 0; i < 10; i++) {
this.setState({ // 在先前的逻辑中, 没调用一次 setState 就会 render 一次
count: ++this.state.count
})
}
}
render() {
console.log(this.state.count)
return (
<div>
<button onClick={this.click}>增加</button>
<div>{this.state.count}</div>
</div>
)
}
}

异步调用 setState

查阅 setState 的 api, 其形式如下:

setState(updater, [callback])

它能接收两个参数, 其中第一个参数 updater 可以为对象或者为函数 ((prevState, props) => stateChange), 第二个参数为回调函数;

确定优化思路为: 将多次 setState 后跟着的值进行浅合并, 并借助事件循环等所有值合并好之后再进行渲染界面。

let componentArr = []
// 异步渲染
function asyncRender(updater, component, cb) {
if (componentArr.length === 0) {
defer(() => render()) // 利用事件循环, 延迟渲染函数的调用
}
if (cb) defer(cb) // 调用回调函数
if (_.isFunction(updater)) { // 处理 setState 后跟函数的情况
updater = updater(component.state, component.props)
}
// 浅合并逻辑
component.state = Object.assign({}, component.state, updater)
if (componentArr.includes(component)) {
component.state = Object.assign({}, component.state, updater)
} else {
componentArr.push(component)
}
}
function render() {
let component
while (component = componentArr.shift()) {
renderComponent(component) // rerender
}
}
// 事件循环, 关于 promise 的事件循环和 setTimeout 的事件循环后续会单独写篇文章。
const defer = function(fn) {
return Promise.resolve().then(() => fn())
}

此时, 每点击一次增加按钮 render 函数只执行一次了。