摘要:一個比較好的做法是利用的事件隊列機制。整個系列大概會有四篇左右,我每周會更新一到兩篇,我會第一時間在上更新,有問題需要探討也請在上回復(fù)我博客地址關(guān)注點,訂閱點上一篇文章從零開始實現(xiàn)一個三算法
前言
在上一篇文章中,我們實現(xiàn)了diff算法,性能有非常大的改進。但是文章末尾也指出了一個問題:按照目前的實現(xiàn),每次調(diào)用setState都會觸發(fā)更新,如果組件內(nèi)執(zhí)行這樣一段代碼:
for ( let i = 0; i < 100; i++ ) { this.setState( { num: this.state.num + 1 } ); }
那么執(zhí)行這段代碼會導(dǎo)致這個組件被重新渲染100次,這對性能是一個非常大的負擔。
真正的React是怎么做的React顯然也遇到了這樣的問題,所以針對setState做了一些特別的優(yōu)化:React會將多個setState的調(diào)用合并成一個來執(zhí)行,這意味著當調(diào)用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 (); } }{ this.state.num }
我們定義了一個App組件,在組件掛載后,會循環(huán)100次,每次讓this.state.num增加1,我們用真正的React來渲染這個組件,看看結(jié)果:
組件渲染的結(jié)果是1,并且在控制臺中輸出了100次0,說明每個循環(huán)中,拿到的state仍然是更新之前的。
這是React的優(yōu)化手段,但是顯然它也會在導(dǎo)致一些不符合直覺的問題(就如上面這個例子),所以針對這種情況,React給出了一種解決方案:setState接收的參數(shù)還可以是一個函數(shù),在這個函數(shù)中可以拿先前的狀態(tài),并通過這個函數(shù)的返回值得到下一個狀態(tài)。
我們可以通過這種方式來修正App組件:
componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( prevState => { console.log( prevState.num ); return { num: prevState.num + 1 } } ); } }
這種用法是不是很像數(shù)組的reduce方法?
現(xiàn)在來看看App組件的渲染結(jié)果:
現(xiàn)在終于能得到我們想要的結(jié)果了。
所以,這篇文章的目標也明確了,我們要實現(xiàn)以下兩個功能:
異步更新state,將短時間內(nèi)的多個setState合并成一個
為了解決異步更新導(dǎo)致的問題,增加另一種形式的setState:接受一個函數(shù)作為參數(shù),在函數(shù)中可以得到前一個狀態(tài)并返回下一個狀態(tài)
合并setState回顧一下第二篇文章中對setState的實現(xiàn):
setState( stateChange ) { Object.assign( this.state, stateChange ); renderComponent( this ); }
這種實現(xiàn),每次調(diào)用setState都會更新state并馬上渲染一次。
setState隊列為了合并setState,我們需要一個隊列來保存每次setState的數(shù)據(jù),然后在一段時間后,清空這個隊列并渲染組件。
隊列是一種數(shù)據(jù)結(jié)構(gòu),它的特點是“先進先出”,可以通過js數(shù)組的push和shift方法模擬
const queue = []; function enqueueSetState( stateChange, component ) { queue.push( { stateChange, component } ); }
然后修改組件的setState方法
setState( stateChange ) { enqueueSetState( stateChange, this ); }
現(xiàn)在隊列是有了,怎么清空隊列并渲染組件呢?
清空隊列我們定義一個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; } }
這只是實現(xiàn)了state的更新,我們還沒有渲染組件。渲染組件不能在遍歷隊列時進行,因為同一個組件可能會多次添加到隊列中,我們需要另一個隊列保存所有組件,不同之處是,這個隊列內(nèi)不會有重復(fù)的組件。
我們在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 ); } }延遲執(zhí)行
現(xiàn)在還有一件最重要的事情:什么時候執(zhí)行flush方法。
我們需要合并一段時間內(nèi)所有的setState,也就是在一段時間后才執(zhí)行flush方法來清空隊列,關(guān)鍵是這個“一段時間“怎么決定。
一個比較好的做法是利用js的事件隊列機制。
先來看這樣一段代碼:
setTimeout( () => { console.log( 2 ); }, 0 ); Promise.resolve().then( () => console.log( 1 ) ); console.log( 3 );
你可以打開瀏覽器的調(diào)試工具運行一下,它們打印的結(jié)果是:
3 1 2
具體的原理可以看阮一峰的這篇文章,這里就不再贅述了。
我們可以利用事件隊列,讓flush在所有同步任務(wù)后執(zhí)行
function enqueueSetState( stateChange, component ) { // 如果queue的長度是0,也就是在上次flush執(zhí)行之后第一次往隊列里添加 if ( queue.length === 0 ) { defer( flush ); } queue.push( { stateChange, component } ); if ( !renderQueue.some( item => item === component ) ) { renderQueue.push( component ); } }
定義defer方法,利用剛才題目中出現(xiàn)的Promise.resolve
function defer( fn ) { return Promise.resolve().then( fn ); }
這樣在一次“事件循環(huán)“中,最多只會執(zhí)行一次flush了,在這個“事件循環(huán)”中,所有的setState都會被合并,并只渲染一次組件。
別的延遲執(zhí)行方法除了用Promise.resolve().then( fn ),我們也可以用上文中提到的setTimeout( fn, 0 ),setTimeout的時間也可以是別的值,例如16毫秒。
16毫秒的間隔在一秒內(nèi)大概可以執(zhí)行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 (); } }{ this.state.num }
效果和React完全一樣
同樣,用第二種方式調(diào)用setState:
componentDidMount() { for ( let i = 0; i < 100; i++ ) { this.setState( prevState => { console.log( prevState.num ); return { num: prevState.num + 1 } } ); } }
結(jié)果也完全一樣:
在這篇文章中,我們又實現(xiàn)了一個很重要的優(yōu)化:合并短時間內(nèi)的多次setState,異步更新state。
到這里我們已經(jīng)實現(xiàn)了React的大部分核心功能和優(yōu)化手段了,所以這篇文章也是這個系列的最后一篇了。
這篇文章的所有代碼都在這里:https://github.com/hujiulong/...
從零開始實現(xiàn)React系列React是前端最受歡迎的框架之一,解讀其源碼的文章非常多,但是我想從另一個角度去解讀React:從零開始實現(xiàn)一個React,從API層面實現(xiàn)React的大部分功能,在這個過程中去探索為什么有虛擬DOM、diff、為什么setState這樣設(shè)計等問題。
整個系列大概會有四篇左右,我每周會更新一到兩篇,我會第一時間在github上更新,有問題需要探討也請在github上回復(fù)我~
博客地址: https://github.com/hujiulong/...上一篇文章
關(guān)注點star,訂閱點watch
從零開始實現(xiàn)一個React(三):diff算法
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/94331.html
摘要:單頁博客應(yīng)用編寫總結(jié)很久之前就想寫一個博客應(yīng)用在一開始想要直接用和模板直接寫但是暑假一開始的時候不小心入了的坑所以就一不做二不休直接用寫那既然用了不寫個單頁應(yīng)用也過意不去了不前前后后寫了將近兩個星期現(xiàn)在看來這其實是一個很容易的應(yīng)用但是鑒于 React-Express單頁博客應(yīng)用編寫總結(jié) 很久之前就想寫一個博客應(yīng)用.在一開始想要直接用express和ejs模板直接寫, 但是暑假一開始的時...
摘要:無疑是一個非常值得學習其原理的框架,它設(shè)計簡單,沒有引入任何新的概念,一個組件就是一個方法或一個類。 這是我?guī)讉€月前寫的文章,在前端面試中原理相關(guān)的問題是問的最多的,所以重新推薦下這幾篇文章 深入學習一個框架最直接的方式,就是弄明白框架的原理。React無疑是一個非常值得學習其原理的框架,它設(shè)計簡單,沒有引入任何新的概念,一個組件就是一個方法或一個類。 但是要完整弄明白React的源碼...
摘要:在這篇文章中,我們就要實現(xiàn)的組件功能。這篇文章的代碼從零開始實現(xiàn)系列是前端最受歡迎的框架之一,解讀其源碼的文章非常多,但是我想從另一個角度去解讀從零開始實現(xiàn)一個,從層面實現(xiàn)的大部分功能,在這個過程中去探索為什么有虛擬為什么這樣設(shè)計等問題。 前言 在上一篇文章JSX和虛擬DOM中,我們實現(xiàn)了基礎(chǔ)的JSX渲染功能,但是React的意義在于組件化。在這篇文章中,我們就要實現(xiàn)React的組件功...
摘要:想要自己實現(xiàn)一個簡易版框架,并不是非常難。為了防止出現(xiàn)這種情況,我們需要改變整體的策略。上面這段話,說的就是版本和架構(gòu)的區(qū)別。 showImg(https://segmentfault.com/img/bVbwfRh); 想要自己實現(xiàn)一個React簡易版框架,并不是非常難。但是你需要先了解下面這些知識點如果你能閱讀以下的文章,那么會更輕松的閱讀本文章: 優(yōu)化你的超大型React應(yīng)用 ...
閱讀 1211·2023-04-25 20:31
閱讀 3718·2021-10-14 09:42
閱讀 1485·2021-09-22 16:06
閱讀 2636·2021-09-10 10:50
閱讀 3524·2021-09-07 10:19
閱讀 1772·2019-08-30 15:53
閱讀 1170·2019-08-29 15:13
閱讀 2818·2019-08-29 13:20