从零开始实现一个React(四):异步的setState
前言在上一篇文章中,我们实现了diff算法,性能有非常大的改进。但是文章末尾也指出了一个问题:按照目前的实现,每次调用setState都会触发更新,如果组件内执行这样一段代码: for ( let i = 0; i < 100; i++ ) { this.setState( { num: this.state.num + 1 } ); } 那么执行这段代码会导致这个组件被重新渲染100次,这对性能是一个非常大的负担。 真正的React是怎么做的React显然也遇到了这样的问题,所以针对setState做了一些特别的优化:React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新,举个栗子: class App extends Component { constructor() { super(); this.state = { num: 0 } } componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( { num: this.state.num + 1 } ); console.log( this.state.num ); // 会输出什么? } } render() { return ( <div className="App"> <h1>{ this.state.num }</h1> </div> ); } } 我们定义了一个
组件渲染的结果是1,并且在控制台中输出了100次0,说明每个循环中,拿到的state仍然是更新之前的。 这是React的优化手段,但是显然它也会在导致一些不符合直觉的问题(就如上面这个例子),所以针对这种情况,React给出了一种解决方案:setState接收的参数还可以是一个函数,在这个函数中可以拿先前的状态,并通过这个函数的返回值得到下一个状态。 我们可以通过这种方式来修正App组件: componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( prevState => { console.log( prevState.num ); return { num: prevState.num + 1 } } ); } }
这种用法是不是很像数组的
现在来看看App组件的渲染结果: 所以,这篇文章的目标也明确了,我们要实现以下两个功能:
合并setState回顾一下第二篇文章中对setState的实现: setState( stateChange ) { Object.assign( this.state,stateChange ); renderComponent( this ); } 这种实现,每次调用setState都会更新state并马上渲染一次。 setState队列为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后,清空这个队列并渲染组件。 队列是一种数据结构,它的特点是“先进先出”,可以通过js数组的push和shift方法模拟 const queue = []; function enqueueSetState( stateChange,component ) { queue.push( { stateChange,component } ); } 然后修改组件的setState方法 setState( stateChange ) { enqueueSetState( stateChange,this ); } 现在队列是有了,怎么清空队列并渲染组件呢? 清空队列我们定义一个flush方法,它的作用就是清空队列 function flush() { let item; // 遍历 while( item = setStateQueue.shift() ) { const { stateChange,component } = item; // 如果没有prevState,则将当前的state作为初始的prevState if ( !component.prevState ) { component.prevState = Object.assign( {},component.state ); } // 如果stateChange是一个方法,也就是setState的第二种形式 if ( typeof stateChange === 'function' ) { Object.assign( component.state,stateChange( component.prevState,component.props ) ); } else { // 如果stateChange是一个对象,则直接合并到setState中 Object.assign( component.state,stateChange ); } component.prevState = component.state; } } 这只是实现了state的更新,我们还没有渲染组件。渲染组件不能在遍历队列时进行,因为同一个组件可能会多次添加到队列中,我们需要另一个队列保存所有组件,不同之处是,这个队列内不会有重复的组件。 我们在enqueueSetState时,就可以做这件事 const queue = []; const renderQueue = []; function enqueueSetState( stateChange,component } ); // 如果renderQueue里没有当前组件,则添加到队列中 if ( !renderQueue.some( item => item === component ) ) { renderQueue.push( component ); } } 在flush方法中,我们还需要遍历renderQueue,来渲染每一个组件 function flush() { let item,component; while( item = queue.shift() ) { // ... } // 渲染每一个组件 while( component = renderQueue.shift() ) { renderComponent( component ); } } 延迟执行现在还有一件最重要的事情:什么时候执行flush方法。 一个比较好的做法是利用js的事件队列机制。 先来看这样一段代码: setTimeout( () => { console.log( 2 ); },0 ); Promise.resolve().then( () => console.log( 1 ) ); console.log( 3 ); 你可以打开浏览器的调试工具运行一下,它们打印的结果是: 3 1 2 具体的原理可以看阮一峰的这篇文章,这里就不再赘述了。 我们可以利用事件队列,让flush在所有同步任务后执行 function enqueueSetState( stateChange,component ) { // 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加 if ( queue.length === 0 ) { defer( flush ); } queue.push( { stateChange,component } ); if ( !renderQueue.some( item => item === component ) ) { renderQueue.push( component ); } } 定义defer方法,利用刚才题目中出现的Promise.resolve function defer( fn ) { return Promise.resolve().then( fn ); } 这样在一次“事件循环“中,最多只会执行一次flush了,在这个“事件循环”中,所有的setState都会被合并,并只渲染一次组件。 别的延迟执行方法除了用 16毫秒的间隔在一秒内大概可以执行60次,也就是60帧,人眼每秒只能捕获60幅画面 另外也可以用 function defer( fn ) { return requestAnimationFrame( fn ); } 试试效果就试试渲染上文中用React渲染的那两个例子: class App extends Component { constructor() { super(); this.state = { num: 0 } } componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( { num: this.state.num + 1 } ); console.log( this.state.num ); } } render() { return ( <div className="App"> <h1>{ this.state.num }</h1> </div> ); } } 效果和React完全一样 componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( prevState => { console.log( prevState.num ); return { num: prevState.num + 1 } } ); } } 结果也完全一样: 后话在这篇文章中,我们又实现了一个很重要的优化:合并短时间内的多次setState,异步更新state。 这篇文章的所有代码都在这里:https://github.com/hujiulong/... 从零开始实现React系列React是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。 整个系列大概会有四篇左右,我每周会更新一到两篇,我会第一时间在github上更新,有问题需要探讨也请在github上回复我~ 博客地址: https://github.com/hujiulong/... 上一篇文章从零开始实现一个React(三):diff算法 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |