React總結(jié)篇之五_React組件的性能優(yōu)化-創(chuàng)新互聯(lián)

一、單個(gè)React組件的性能優(yōu)化
React利用Virtual DOM來提高渲染性能,雖然這能將每次DOM操作量減少到最小,計(jì)算和比較Virtual DOM依然是一個(gè)復(fù)雜的計(jì)算過程。如果能夠在計(jì)算Virtual DOM之前就能判斷渲染結(jié)果不會有變化,那樣可以干脆不要進(jìn)行Virtual DOM計(jì)算和比較,速度就會更快。

成都創(chuàng)新互聯(lián)長期為近1000家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為隴川企業(yè)提供專業(yè)的成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì),隴川網(wǎng)站改版等技術(shù)服務(wù)。擁有10多年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
  1. 發(fā)現(xiàn)浪費(fèi)的渲染時(shí)間
    在Chrome瀏覽器中安裝React Perf擴(kuò)展,步驟省略(屬于操作部分)

  2. 性能優(yōu)化的時(shí)機(jī)
    “我們應(yīng)該忘記忽略很小的性能優(yōu)化,可以說97%的情況下,過早的優(yōu)化是萬惡之源,而我們應(yīng)該關(guān)心對性能影響最關(guān)鍵的那另外3%的代碼” --高德納
    對于合并多個(gè)字符串,怎樣合并,使用什么方法合并不大可能對整個(gè)應(yīng)用造成關(guān)鍵的性能影響,這就是高納德所說的97%的情況,而選擇用什么樣的方式去定義組件的接口,如何定義state到prop的轉(zhuǎn)變,使用什么樣的算法來比對Virtual DOM,這些決定對性能和架構(gòu)的影響是巨大的,就是那關(guān)鍵的3%。

  3. React-Redux的shouldComponentUpdate的實(shí)現(xiàn)
    使用React-Redux,一個(gè)典型的React組件代碼文件最后一個(gè)語句代碼是這樣的:
    export default connect(mapStateToProps)(mapDispatchToProps)(Foo)
    以上,connect過程中實(shí)際上產(chǎn)生了一個(gè)無名的React組件類,這個(gè)類定制了shouldComponentUpdate的實(shí)現(xiàn),實(shí)現(xiàn)邏輯是比對這次傳遞給內(nèi)層傻瓜組件的props和上次的props,如果相同那就沒必要重新渲染了,可以返回false,否則就要返回true。
    但是,我們需要了解一下shouldComponentUpdate的實(shí)現(xiàn)方式,shouldComponentUpdate在比對prop和上次渲染所用的prop方面,依然用的是盡量簡單的方法,做的是所謂的“淺層比較”。簡單來說就是用JavaScript的===操作符來比較,如果prop的類型是字符串或者數(shù)字,只要值相同,那么“淺層比較”也會認(rèn)為二者相同,但是,如果prop的類型是復(fù)雜對象,那么“淺層比較”的方式只看這兩個(gè)prop是不是同一個(gè)對象的引用,如果不是,哪怕這兩個(gè)對象中的內(nèi)容完全一樣,也會被認(rèn)為是兩個(gè)不同的prop。
    比如,在JSX中使用組件Foo的時(shí)候給名為style的prop賦值,代碼如下:
    <Foo style={{color:"red"}} />
    像上面這樣的使用方法,F(xiàn)oo組件利用React-Redux提供的shouldComponentUpdate函數(shù)實(shí)現(xiàn),每一次渲染都會認(rèn)為style這個(gè)prop發(fā)生了變化,因?yàn)槊看味紩a(chǎn)生一個(gè)新的對象給style,而在“淺層比較”中,只比較第一層,不會去比較對象里面是不是相等。那為什么不用深層比較呢?因?yàn)橐粋€(gè)對象到底有多少層無法預(yù)料,如果遞歸對每個(gè)字段都進(jìn)行“深層比較”,不光代碼更復(fù)雜,也可能會造成性能問題。
    上面的例子應(yīng)該改成下面這樣:
    const fooStyle = {color:"red"} //確保這個(gè)初始化只執(zhí)行一次,不要放在render中
    <Foo style={fooStyle} />
    同樣的情況也存在與函數(shù)類型的prop,React-Redux無從知道兩個(gè)不同的函數(shù)是不是做著同樣的事,要想讓它認(rèn)為兩個(gè)prop是相同的,就必須讓這兩個(gè)prop指向同樣一個(gè)函數(shù),如果每次傳給prop的都是一個(gè)新創(chuàng)建的函數(shù),那肯定就沒法讓prop指向同一個(gè)函數(shù)了。
    看TodoList傳遞給TodoItem的onToggle和onRemove,在JSX中代碼如下:
    onToggle = {()=>onToggleTodo(item.id)}
    onRemove = {()=>onRemoveTodo(item.id)}
    這里賦值給onClick的是一個(gè)匿名的函數(shù),而且是在賦值的時(shí)候產(chǎn)生的。也就是說,每次渲染一個(gè)TodoItem的時(shí)候,都會產(chǎn)生一個(gè)新的函數(shù),這就是問題所在。辦法就是不要讓TodoList每次都傳遞新的函數(shù)給TodoItem。有兩種解決方式。
    (1)第一種方式,TodoList保證傳遞給TodoItem的onToggle永遠(yuǎn)只能指向同一個(gè)函數(shù)對象,這是為了應(yīng)對TodoItem的shouldComponentUpdate的檢查,但是因?yàn)門odoItem可能有多個(gè)實(shí)例,所以這個(gè)函數(shù)要用某種方法區(qū)分什么TodoItem回調(diào)這個(gè)函數(shù),區(qū)分的辦法只能通過函數(shù)參數(shù)。
    在TodoList組件中,mapDispatchToProps產(chǎn)生的prop中onToggleTodo接受TodoItem的id作為參數(shù),恰好勝任這個(gè)工作,所以,可以在JSX中代碼改為下面這樣:
    <TodoItem
    key=em.id
    id=em.id
    text=em.text
    completed=em.completed
    onToggle={onToggleTodo}
    onRemove={onRemoveTodo}
    />
    注意,除了onToggle和onRemove的值變了,還增加了一個(gè)新的prop名為id,這是讓每個(gè)TodoItem知道自己的id,在回調(diào)onToggle和onRemove時(shí)可以區(qū)分不同的Todo-Item實(shí)例。
    TodoList的代碼簡化了,但是TodoItem組件也要做對應(yīng)改變,對應(yīng)TodoItem組件的mapDispatchToProps函數(shù)代碼如下:
    const mapDispatchToProps = (dispatch,ownProps) =>({
    onToggleItem : () => ownProps.onToggle(ownProps.id)
    });
    mapDispatchToProps這個(gè)函數(shù)有兩個(gè)參數(shù)dispatch和ownProps,ownProps也就是父組件渲染當(dāng)前組件時(shí)傳遞過來的props,通過訪問ownProps.id就能夠得到父組件傳遞過來的名為id的prop值。
    上面的mapDispatchToProps這個(gè)函數(shù)給TodoItem組件增加了名為onToggleItem的prop,調(diào)用onToggle,傳遞當(dāng)前實(shí)例的id作為參數(shù),在TodoItem的JSX中就應(yīng)該使用onToggleItem,而不是直接使用TodoList提供的onToggle。
    (2)第二種方式,干脆讓TodoList不要給TodoItem傳遞任何函數(shù)類型prop,點(diǎn)擊事件完全由TodoItem組件自己搞定。
    在TodoList組件的JSX中,渲染TodoItem組件的代碼如下:
    <TodoItem
    key = em.id
    id = em.id
    text = em.text
    completed = em.completed
    />
    可以看到不需要onToggle和onRemove這些函數(shù)類型的prop,但依然有名為id的prop。
    在TodoItem組件中,需要自己通過react-redux派發(fā)action,需要改變的代碼如下:
    const mapDispatchToprops = (dispatch,ownProps) = >{
    const {id} = ownProps.id;
    return {
    onToggle : () => dispatch(toggleTodo(id)),
    onRemove : () => dispatch(removeTodo(id))
    }
    };
    對比這兩種方式,看一看到無論如何TodoItem都要使用react-redux,都需要定義產(chǎn)生定制prop的mapDispatchToProps,都需要TodoList傳入一個(gè)id,區(qū)別只在于actions是由父組件導(dǎo)入還是組件自己導(dǎo)入。
    相比而言,沒有多大必要讓action在TodoList導(dǎo)入然后傳遞一個(gè)函數(shù)給TodoItem,第二種讓TodoItem處理自己的一切事物,更符合高內(nèi)聚的要求。

二、多個(gè)React組件的性能優(yōu)化
和單個(gè)組件的生命周期一樣,React組件也要考慮3個(gè)階段:裝載階段、更新階段、卸載階段。其中,裝載階段基本沒什么可以優(yōu)化的空間,因?yàn)檫@部分工作沒有什么可以省略的。而卸載階段,只有一個(gè)生命周期函數(shù)componentWillUnmount,這個(gè)函數(shù)做的事情只是清理componentDidMount添加的事件處理監(jiān)聽等收尾工作,做的事情要比裝載過程少很多,所以也沒什么可以優(yōu)化的空間。所以值得關(guān)注的過程,只剩下更新過程。

  1. React的調(diào)和過程
    React在更新階段,很巧妙的對比原有的Virtual DOM和新生成的Virtual DOM(存在于內(nèi)存中),找出兩者的不同,根據(jù)不同修改DOM樹,這樣只需做最小的必要改動。
    React在更新中找不同的過程,就叫做調(diào)和(Reconciliation)。
    React實(shí)際采用的算法的時(shí)間復(fù)雜度是O(N)。React的Reconciliation算法并不復(fù)雜,當(dāng)React要對比兩個(gè)Virtual DOM的樹形結(jié)構(gòu)的時(shí)候,從根節(jié)點(diǎn)開始遞歸往下對比,在樹形結(jié)構(gòu)上,每個(gè)節(jié)點(diǎn)都可以看做這個(gè)節(jié)點(diǎn)以下子樹部分的根節(jié)點(diǎn),所以其實(shí)這個(gè)對比算法可以從Virtual DOM上的任何一個(gè)節(jié)點(diǎn)開始執(zhí)行。
    React首先檢查兩個(gè)根節(jié)點(diǎn)的類型是否相同,根據(jù)相同或者不同有不同處理方式。
    (1)節(jié)點(diǎn)類型不同的情況
    這時(shí)可以直接認(rèn)為原來的樹形結(jié)構(gòu)已經(jīng)沒用,需要重新構(gòu)建新的DOM樹,原有樹形上的React組件會經(jīng)歷“卸載”的生命周期。這時(shí),componentWillUnmount的方法會被調(diào)用,取而代之的組件則會經(jīng)歷裝載過程的生命周期,組件的componentWillMount、render和componentDidMount方法會被依次調(diào)用。
    (2)節(jié)點(diǎn)類型相同的情況
    這時(shí)React就會認(rèn)為原來的根節(jié)點(diǎn)只需要更新,不必將其卸載,也不會引發(fā)根節(jié)點(diǎn)的重新裝載。
    這時(shí),有必要區(qū)分一下節(jié)點(diǎn)的類型,節(jié)點(diǎn)的類型可以分為兩類:一類是DOM元素類型,對應(yīng)的就是HTML直接支持的元素類型,比如<div />,<span />和<p />;另一類是React組件,也就是利用React庫定制的類型。

    • 對于DOM元素類型,React會保留節(jié)點(diǎn)對應(yīng)的DOM元素,只對樹形結(jié)構(gòu)根節(jié)點(diǎn)上的屬性和內(nèi)容做一下對比,然后只更新修改的部分。
    • 對于React組件類型,React會根據(jù)新節(jié)點(diǎn)的props去更新原來根節(jié)點(diǎn)的props實(shí)例,引發(fā)這個(gè)組件實(shí)例的更新過程,也就是按照順序引發(fā)下列函數(shù):
      shouldComponentUpdate
      componentWillReceiveProps
      componentWillUpdate
      render
      componentDidUpdate
      在處理完根節(jié)點(diǎn)的對比之后,React的算法會對根節(jié)點(diǎn)的每個(gè)子節(jié)點(diǎn)重復(fù)一樣的動作,這時(shí)候每個(gè)子節(jié)點(diǎn)就會成為它所覆蓋部分的根節(jié)點(diǎn),處理方式和它的父節(jié)點(diǎn)完全一樣。
      (3)多個(gè)子組件的情況
      當(dāng)一個(gè)組件包含多個(gè)子組件,React的處理方式也非常的簡單直接。
      React總結(jié)篇之五_React組件的性能優(yōu)化
      React發(fā)現(xiàn)多了一個(gè)TodoItem,會創(chuàng)建一個(gè)新的TodoItem組件實(shí)例,這個(gè)TodoItem組件實(shí)例需要經(jīng)歷裝載過程,對于前兩個(gè)TodoItem實(shí)例,React會引發(fā)它們的更新過程。
      上面的例子是TodoItem序列后面增加了一個(gè)新的TodoItem實(shí)例,接下來在TodoItem序列前面增加一個(gè)TodoItem實(shí)例,代碼如下:
      React總結(jié)篇之五_React組件的性能優(yōu)化
      像上面新的TodoItem實(shí)例插入在第一位的例子中,React會首先認(rèn)為把text為First的TodoItem組件實(shí)例的text改成了Zero,text為Second的TodoItem組件實(shí)例的text改成了First,在后面多出了一個(gè)TodoItem組件實(shí)例,text內(nèi)容為Second。這樣操作的后果就是,現(xiàn)存的兩個(gè)TodoItem實(shí)例的text屬性被改變了,強(qiáng)迫它們完成了一個(gè)更新過程。React提供了方法來克服這種浪費(fèi),但需要開發(fā)人員在寫代碼的時(shí)候提供一點(diǎn)幫助,這就是key的作用。
  2. key的用法
    默認(rèn)情況下,在React的眼里,確定每一個(gè)組件在組件序列中的唯一標(biāo)識就是它的位置,所以它也完全不懂哪些子組件實(shí)際上并沒有改變,為了讓React更加“聰明”,就需要開發(fā)者提供一點(diǎn)幫助。
    如果在代碼中明確的告訴React每個(gè)組件的唯一標(biāo)識,就可以幫助React在處理這個(gè)問題時(shí)聰明很多,告訴React每個(gè)組件“×××號”的途徑就是key屬性。假如讓待辦事項(xiàng)列表用JSX標(biāo)識的代碼如下:
    <UI>
    <TodoItem key={1} text="First" completed={false} />
    <TodoItem key={2} text="Second" completed={false} />
    <UI />
    前面代碼的區(qū)別是每個(gè)TodoItem增加了名為key的prop,而且每個(gè)key是這個(gè)TodoItem實(shí)例的唯一id。現(xiàn)在第一位增加一個(gè)TodoItem實(shí)例,并給一個(gè)唯一的key值0,React的處理方式會不一樣。
    <UI>
    <TodoItem key={0} text="Zero" completed={false} />
    <TodoItem key={1} text="First" completed={false} />
    <TodoItem key={2} text="Second" completed={false} />
    <UI />
    React根據(jù)key值,可以知道現(xiàn)在的第二個(gè)和第三個(gè)TodoItem實(shí)例其實(shí)就是之前的第一個(gè)和第二個(gè)實(shí)例,所以React就會把新創(chuàng)建的TodoItem實(shí)例插在第一位,對于原有的兩個(gè)TodoItem實(shí)例只用原有的props來啟動更新過程,這樣shouldComponentUpdate就會發(fā)生作用,避免無謂的更新操作。
    在一列子組件中,每個(gè)子組件的key值必須唯一,并且key值還需要是穩(wěn)定不變的,因此數(shù)組的下標(biāo)不能作為key使用。
    注意:雖然key是一個(gè)prop,但是接受key的組件并不能讀取到key的值,因?yàn)閗ey和ref是React保留的兩個(gè)特殊的prop,并沒有預(yù)期讓組件直接訪問。

三、用reselect提高數(shù)據(jù)獲取性能

  1. 兩階段選擇過程
    reselect庫的工作原理:只要相關(guān)狀態(tài)沒有改變,那就直接使用上一次的緩存結(jié)果。
    reselect庫被用來創(chuàng)造“選擇器”,即接受state作為參數(shù)的函數(shù),這個(gè)選擇器函數(shù)返回的數(shù)據(jù)就是我們某個(gè)mapStateToProps需要的結(jié)果。
    reselect認(rèn)為一個(gè)選擇器的工作可以分為兩個(gè)部分,把一個(gè)計(jì)算步驟分為兩個(gè)步驟:
    (1)從輸入?yún)?shù)state抽取第一層結(jié)果,將這第一層結(jié)果和之前抽取的第一層結(jié)果做比較,如果發(fā)現(xiàn)完全相同,就沒有必要進(jìn)行第二部分運(yùn)算了,選擇器直接把之前第二部分的運(yùn)算結(jié)果返回就好了。注意:這一部分做的比較,就是JavaScript的===操作符比較,如果第一層結(jié)果是對象的話,只有是同一對象才會被認(rèn)為是相同。
    (2)根據(jù)第一層結(jié)果計(jì)算出選擇器需要返回的最終結(jié)果。
    顯然,每次選擇器函數(shù)被調(diào)用時(shí),步驟一都會被執(zhí)行,但步驟一的結(jié)果被用來判斷是否可以使用緩存的結(jié)果,所以并不是每次都會調(diào)用步驟二的運(yùn)算。
    剩下的事情就是確定選擇器步驟一和步驟二分別進(jìn)行什么運(yùn)算。原則很簡單,步驟一運(yùn)算因?yàn)槊看芜x擇器都要使用,所以一定要快,運(yùn)算要非常簡單,最好就是一個(gè)映射運(yùn)算,通常就只是從state參數(shù)中得到某個(gè)字段的引用就足夠,把剩下來的重活累活都交給步驟二去做。
    在TodoList的具體例子中,todos和filter的值直接決定了應(yīng)該顯示什么樣的待辦事項(xiàng),所以,步驟一是獲取todos和filter的值,步驟二是根據(jù)這兩個(gè)值進(jìn)行計(jì)算。
    使用reselect需要安裝對應(yīng)的npm包:
    npm install --save reselect
    React總結(jié)篇之五_React組件的性能優(yōu)化
    reselect提供了創(chuàng)造選擇器的createSelector函數(shù),這是一個(gè)高階函數(shù),也就是接受函數(shù)為參數(shù)來產(chǎn)生一個(gè)新函數(shù)的函數(shù)。
    第一個(gè)參數(shù)是一個(gè)函數(shù)數(shù)組,每個(gè)元素代表了選擇器步驟一需要做的映射計(jì)算,這里我們提供了兩個(gè)函數(shù)getFilter和getTodos,代碼如下:
    const getFilter = (state)=>state.filter;
    const getTodos = (state) = >state.todos;
    createSelector函數(shù)的第二個(gè)參數(shù)代表步驟二的計(jì)算過程,參數(shù)為第一個(gè)參數(shù)的輸出結(jié)果。
    現(xiàn)在,可以在TodoList模塊中改用新定義的選擇器來獲取待辦事項(xiàng)數(shù)據(jù)了:
    import {selectVisibleTodos} from '../selector.js'
    const mapStateToProps = (state) =>{
    return {
    todos : selectVisibleTodos(state)
    }
    }
    Redux要求每個(gè)reducer不能修改state狀態(tài),如果要返回一個(gè)新的狀態(tài),就必須返回一個(gè)新的對象。這樣,如果state狀態(tài)樹上的某個(gè)節(jié)點(diǎn)沒有變化,那我們可以認(rèn)為這個(gè)節(jié)點(diǎn)下的數(shù)據(jù)沒有改變,應(yīng)用在reselect中,步驟一的運(yùn)算就可以確定使用緩存的數(shù)據(jù)結(jié)果。
    雖然reselect的createSelector創(chuàng)造的選擇器并不是一個(gè)純函數(shù),但是createSelector接受的所有函數(shù)參數(shù)都是純函數(shù),雖然選擇器有“記憶”這個(gè)副作用,但只要輸入?yún)?shù)state沒有變化,產(chǎn)生的結(jié)果也就沒有變化,表現(xiàn)得卻類似于一個(gè)純函數(shù)。

  2. 范式化狀態(tài)樹
    狀態(tài)樹的設(shè)計(jì)盡量范式化。所謂范式化,就是遵照關(guān)系型數(shù)據(jù)庫的設(shè)計(jì)原則,減少冗余數(shù)據(jù)。
    反范式化的設(shè)計(jì):
    {
    id : 1, //待辦事項(xiàng)id
    text:“待辦事項(xiàng)1”, //待辦事項(xiàng)文字內(nèi)容
    completed:false, //是否已完成
    type:{ //種類
    name:“緊急” , //種類的名稱
    color:“red” //種類的顯示顏色
    }
    }
    范式化的設(shè)計(jì):
    {
    id:1, //待辦事項(xiàng)id
    text:“待辦事項(xiàng)1” //待辦事項(xiàng)文字內(nèi)容
    completed:false, //是否已完成
    typeId:1, 待辦事項(xiàng)所屬的種類id
    }
    用一個(gè)typeId代表類型,然后在Redux store上和Todos平級的根節(jié)點(diǎn)位置創(chuàng)建一個(gè)types字段,內(nèi)容是一個(gè)數(shù)組,每個(gè)數(shù)組元素代表一個(gè)類型,一個(gè)種類的數(shù)據(jù)是類似下面的對象:
    {
    id:1, //種類id
    name:“緊急”, //種類的名稱
    color:“red” //種類的顯示顏色
    }
    當(dāng)TodoItem要渲染內(nèi)容時(shí),從Redux Store狀態(tài)樹的todos字段下獲取的數(shù)據(jù)是不夠的,因?yàn)橹挥衪ypeId。為了獲得對應(yīng)的種類名稱和顏色,需要做一個(gè)類似關(guān)系型數(shù)據(jù)庫的join操作,到狀態(tài)樹的type字段下去尋找對應(yīng)的typeId的種類數(shù)據(jù)。
    以上比較來看范式方式更合理。因?yàn)殡m然join數(shù)據(jù)需要花費(fèi)計(jì)算時(shí)間,但是用了reselect之后,大部分情況下都會命中緩存,實(shí)際上也就沒有花費(fèi)很多計(jì)算時(shí)間了。

另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。

分享名稱:React總結(jié)篇之五_React組件的性能優(yōu)化-創(chuàng)新互聯(lián)
網(wǎng)站鏈接:http://muchs.cn/article18/dcoegp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供商城網(wǎng)站、小程序開發(fā)、面包屑導(dǎo)航、定制開發(fā)、動態(tài)網(wǎng)站、外貿(mào)建站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

微信小程序開發(fā)