在上一篇文章中,咱們實現了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