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