在上一篇文章中,咱们实现了diff算法,性能有很是大的改进。可是文章末尾也指出了一个问题:按照目前的实现,每次调用setState都会触发更新,若是组件内执行这样一段代码:html
for ( let i = 0; i < 100; i++ ) { this.setState( { num: this.state.num + 1 } ); }
那么执行这段代码会致使这个组件被从新渲染100次,这对性能是一个很是大的负担。前端
React显然也遇到了这样的问题,因此针对setState作了一些特别的优化:React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会当即更新,举个栗子: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> ); } }
咱们定义了一个App
组件,在组件挂载后,会循环100次,每次让this.state.num
增长1,咱们用真正的React来渲染这个组件,看看结果:git
组件渲染的结果是1,而且在控制台中输出了100次0,说明每一个循环中,拿到的state仍然是更新以前的。github
这是React的优化手段,可是显然它也会在致使一些不符合直觉的问题(就如上面这个例子),因此针对这种状况,React给出了一种解决方案:setState接收的参数还能够是一个函数,在这个函数中能够拿先前的状态,并经过这个函数的返回值获得下一个状态。算法
咱们能够经过这种方式来修正App组件:数组
componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( prevState => { console.log( prevState.num ); return { num: prevState.num + 1 } } ); } }
这种用法是否是很像数组的
reduce
方法?
如今来看看App组件的渲染结果:
如今终于能获得咱们想要的结果了。浏览器
因此,这篇文章的目标也明确了,咱们要实现如下两个功能:数据结构
回顾一下第二篇文章中对setState的实现:框架
setState( stateChange ) { Object.assign( this.state, stateChange ); renderComponent( this ); }
这种实现,每次调用setState都会更新state并立刻渲染一次。
为了合并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 ) { queue.push( { 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方法。
咱们须要合并一段时间内全部的setState,也就是在一段时间后才执行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都会被合并,并只渲染一次组件。
除了用Promise.resolve().then( fn )
,咱们也能够用上文中提到的setTimeout( fn, 0 )
,setTimeout的时间也能够是别的值,例如16毫秒。
16毫秒的间隔在一秒内大概能够执行60次,也就是60帧,人眼每秒只能捕获60幅画面
另外也能够用requestAnimationFrame
或者requestIdleCallback
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彻底同样
一样,用第二种方式调用setState:
componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( prevState => { console.log( prevState.num ); return { num: prevState.num + 1 } } ); } }
结果也彻底同样:
在这篇文章中,咱们又实现了一个很重要的优化:合并短期内的屡次setState,异步更新state。
到这里咱们已经实现了React的大部分核心功能和优化手段了,因此这篇文章也是这个系列的最后一篇了。
这篇文章的全部代码都在这里:https://github.com/hujiulong/...
React是前端最受欢迎的框架之一,解读其源码的文章很是多,可是我想从另外一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程当中去探索为何有虚拟DOM、diff、为何setState这样设计等问题。
整个系列大概会有四篇左右,我每周会更新一到两篇,我会第一时间在github上更新,有问题须要探讨也请在github上回复我~
博客地址: https://github.com/hujiulong/...
关注点star,订阅点watch