從項目的整體架構來看,要選擇適合項目背景的極速。如果項目背景不適合使用狀態管理器,那就沒有一定的必要性去使用,比如微信小程序等,可以從以下幾個維度來看
什么是渲染劫持,渲染劫持的概念是控制組件從另一個組件輸出的能力,當然這個概念一般和react中的高階組件(HOC)放在一起解釋比較有明了。
高階組件可以在render函數中做非常多的操作,從而控制原組件的渲染輸出,只要改變了原組件的渲染,我們都將它稱之為一種渲染劫持。
實際上,在高階組件中,組合渲染和條件渲染都是渲染劫持的一種,通過反向繼承,不僅可以實現以上兩點,還可以增強由原組件 render 函數產生的 React元素。
實際的操作中通過操作 state、props 都可以實現渲染劫持
依賴于 i18next 的方案,對于龐大的業務項目有個很蛋疼的問題,那就是 json 文件的維護。每次產品迭代都需要增加新的配置,那么這份配置由誰來維護,怎么維護,都會有很多問題,而且如果你的項目要支持幾十個國家的語言,那么這幾十份文件又怎么維護。
所以現在大廠比較常用的方案是,使用 AST,每次開發完新版本,通過 AST 去掃描所有的代碼,找出代碼中的中文,以中文為 key,調用智能翻譯服務,去幫項目自動生成 json 文件。這樣,再也不需要人為去維護 json 文件,一切都依賴工具進行自動化。目前已經有大廠開源,比如滴滴的 di18n,阿里的 kiwi
我認為 react 的拆分前提是代碼目錄設計規范,模塊定義規范,代碼設計規范,符合程序設計的一般原則,例如高內聚、低耦合等等。
在我們的react項目中:
官網例子:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state={ hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能夠顯示降級后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同樣可以將錯誤日志上報給服務器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定義降級后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
使用
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
但是錯誤邊界不會捕獲:
try{}catch(err){}
///異步代碼(例如 setTimeout 或 requestAnimationFrame 回調函數)
///服務端渲染
///它自身拋出來的錯誤(并非它的子組件)
保證react的單向數據流的設計模式,使狀態更可預測。如果允許自組件修改,那么一個父組件將狀態傳遞給好幾個子組件,這幾個子組件隨意修改,就完全不可預測,不知道在什么地方修改了狀態,所以我們必須像純函數一樣保護 props 不被修改
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData]=useState({ hits: [] });
useEffect(async ()=> {
const result=await axios(
'https://api/url/to/data',
);
setData(result.data);
});
return (
<ul>
{data.hits.map(item=> (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
不要在循環,條件或嵌套函數中調用 Hook, 確??偸窃谀愕?React 函數的最頂層調用他們。
不要在普通的 JavaScript 函數中調用 Hook。你可以:
React15 的 StackReconciler 方案由于遞歸不可中斷問題,如果 Diff 時間過長(JS計算時間),會造成頁面 UI 的無響應(比如輸入框)的表現,vdom 無法應用到 dom 中。
為了解決這個問題,React16 實現了新的基于 requestIdleCallback 的調度器(因為 requestIdleCallback 兼容性和穩定性問題,自己實現了 polyfill),通過任務優先級的思想,在高優先級任務進入的時候,中斷 reconciler。
為了適配這種新的調度器,推出了 FiberReconciler,將原來的樹形結構(vdom)轉換成 Fiber 鏈表的形式(child/sibling/return),整個 Fiber 的遍歷是基于循環而非遞歸,可以隨時中斷。
更加核心的是,基于 Fiber 的鏈表結構,對于后續(React 17 lane 架構)的異步渲染和 (可能存在的)worker 計算都有非常好的應用基礎
參考官網
官網回答:
Hook 解決了我們五年來編寫和維護成千上萬的組件時遇到的各種各樣看起來不相關的問題。無論你正在學習 React,或每天使用,或者更愿嘗試另一個和 React 有相似組件模型的框架,你都可能對這些問題似曾相識。
React 沒有提供將可復用性行為“附加”到組件的途徑(例如,把組件連接到 store)。如果你使用過 React 一段時間,你也許會熟悉一些解決此類問題的方案,比如 render props 和 高階組件。但是這類方案需要重新組織你的組件結構,這可能會很麻煩,使你的代碼難以理解。如果你在 React DevTools 中觀察過 React 應用,你會發現由 providers,consumers,高階組件,render props 等其他抽象層組成的組件會形成“嵌套地獄”。盡管我們可以在 DevTools 過濾掉它們,但這說明了一個更深層次的問題:React 需要為共享狀態邏輯提供更好的原生途徑。
你可以使用 Hook 從組件中提取狀態邏輯,使得這些邏輯可以單獨測試并復用。Hook 使你在無需修改組件結構的情況下復用狀態邏輯。 這使得在組件間或社區內共享 Hook 變得更便捷。
我們經常維護一些組件,組件起初很簡單,但是逐漸會被狀態邏輯和副作用充斥。每個生命周期常常包含一些不相關的邏輯。例如,組件常常在 componentDidMount 和 componentDidUpdate 中獲取數據。但是,同一個 componentDidMount 中可能也包含很多其它的邏輯,如設置事件監聽,而之后需在 componentWillUnmount 中清除。相互關聯且需要對照修改的代碼被進行了拆分,而完全不相關的代碼卻在同一個方法中組合在一起。如此很容易產生 bug,并且導致邏輯不一致。
在多數情況下,不可能將組件拆分為更小的粒度,因為狀態邏輯無處不在。這也給測試帶來了一定挑戰。同時,這也是很多人將 React 與狀態管理庫結合使用的原因之一。但是,這往往會引入了很多抽象概念,需要你在不同的文件之間來回切換,使得復用變得更加困難。
為了解決這個問題,Hook 將組件中相互關聯的部分拆分成更小的函數(比如設置訂閱或請求數據),而并非強制按照生命周期劃分。你還可以使用 reducer 來管理組件的內部狀態,使其更加可預測。
除了代碼復用和代碼管理會遇到困難外,我們還發現 class 是學習 React 的一大屏障。你必須去理解 JavaScript 中 this 的工作方式,這與其他語言存在巨大差異。還不能忘記綁定事件處理器。沒有穩定的語法提案,這些代碼非常冗余。大家可以很好地理解 props,state 和自頂向下的數據流,但對 class 卻一籌莫展。即便在有經驗的 React 開發者之間,對于函數組件與 class 組件的差異也存在分歧,甚至還要區分兩種組件的使用場景。
另外,React 已經發布五年了,我們希望它能在下一個五年也與時俱進。就像 Svelte,Angular,Glimmer等其它的庫展示的那樣,組件預編譯會帶來巨大的潛力。尤其是在它不局限于模板的時候。最近,我們一直在使用 Prepack 來試驗 component folding,也取得了初步成效。但是我們發現使用 class 組件會無意中鼓勵開發者使用一些讓優化措施無效的方案。class 也給目前的工具帶來了一些問題。例如,class 不能很好的壓縮,并且會使熱重載出現不穩定的情況。因此,我們想提供一個使代碼更易于優化的 API。
為了解決這些問題,Hook 使你在非 class 的情況下可以使用更多的 React 特性。 從概念上講,React 組件一直更像是函數。而 Hook 則擁抱了函數,同時也沒有犧牲 React 的精神原則。Hook 提供了問題的解決方案,無需學習復雜的函數式或響應式編程技術
React 官網是這么簡介的。JavaScript library for building user interfaces.專注 view 層 的特點決定了它不是一個全能框架,相比 angular 這種全能框架,React 功能較簡單,單一。比如說沒有前端路由,沒有狀態管理,沒有一站式開發文檔等。
react 組件是根據 state (或者 props)去渲染頁面的,類似于一個函數,輸入 state,輸出 view。不過這不是完整意義上的 MDV(Model Driven View),沒有完備的 model 層。順便提一句,感覺現在的組件化和 MDV 在前端開發中正火熱,大勢所趨...
從我們最開始寫 React 開始,就了解這條特點了。state 流向是自組件從外到內,從上到下的,而且傳遞下來的 props 是只讀的,如果你想更改 props,只能上層組件傳下一個包裝好的 setState 方法。不像 angular 有 ng-model, vue 有 v-model, 提供了雙向綁定的指令。React 中的約定就是這樣,你可能覺得這很繁瑣,不過 state 的流向卻更清晰了,單向數據流在大型 spa 總是要討好一些的。
這些特點決定了,React 本身是沒有提供強大的狀態管理功能的,原生大概是三種方式。
它沒有提供生命周期概念,不像 class 組件繼承 React.component,可以讓你使用生命周期以及特意強調相關概念
使用字典樹持久化數據結構,更新時可優化對象生成邏輯,降低成本
dangerouslySetInnerHTML
react 基于虛擬 DOM 和高效 Diff算法的完美配合,實現了對 DOM最小粒度的更新,大多數情況下,React對 DOM的渲染效率足以我們的業務日常
復雜業務場景下,性能問題依然會困擾我們。此時需要采取一些措施來提升運行性能,避免不必要的渲染則是業務中常見的優化手段之一
在實際開發過程中,前端性能問題是一個必須考慮的問題,隨著業務的復雜,遇到性能問題的概率也在增高
除此之外,建議將頁面進行更小的顆粒化,如果一個過大,當狀態發生修改的時候,就會導致整個大組件的渲染,而對組件進行拆分后,粒度變小了,也能夠減少子組件不必要的渲染
高階函數(Higher-order function),至少滿足下列一個條件的函數
在React中,高階組件即接受一個或多個組件作為參數并且返回一個組件,本質也就是一個函數,并不是一個組件
const EnhancedComponent=highOrderComponent(WrappedComponent);
上述代碼中,該函數接受一個組件 WrappedComponent 作為參數,返回加工過的新組件 EnhancedComponent
高階組件的這種實現方式,本質上是一個裝飾者設計模式
Refs 在計算機中稱為彈性文件系統(英語:Resilient File System,簡稱ReFS)
React 中的 Refs提供了一種方式,允許我們訪問 DOM節點或在 render方法中創建的 React元素
本質為ReactDOM.render()返回的組件實例,如果是渲染組件則返回的是組件實例,如果渲染dom則返回的是具體的dom節點
class
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef=React.createRef();
}
render() {
return <div ref="myref" />;
}
}
hooks
function App(props) {
const myref=useRef()
return (
<>
<div ref={myref}></div>
</>
)
}
作者:野生程序猿江辰
鏈接:https://juejin.cn/post/7280439887962144820
開發者,在后端僅提供原始數據集的情況下,如何讓所有搜索邏輯都在前端完成?不僅如此,還要能夠實現預測用戶輸入、忽略錯別字輸入、快速自動補齊等智能功能?本文的作者就深入 JavaScript 技術特性,談一談 React、性能優化以及多線程那些事兒。
第一篇 問題闡述
我有一個看似很簡單的任務:“有一個從后端檢索的數據集,其中包含13,000個數據項,每一項都是冗長羅嗦的名稱(科學組織)。使用這些數據創建一個帶自動補齊功能的搜索欄?!?/p>
你也不覺得很難,對不對?
難點1:
不能使用后端。后端只能提供原始的數據集,所有搜索邏輯都必須在前端完成。
難點2:
開發人員(我):“這些組織名稱這么長,需要花點心思。如果我們只是運行簡單的字符串匹配,而且用戶可能會輸錯或出現拼寫錯誤,那么就很難搜索到任何結果?!?/p>
客戶:“你說的對,我們必須加入一些智能的功能,預測用戶的輸入,忽略輸入的錯別字。”
注意:一般情況下,我不建議你在未經項目經理同意下,提示客戶沒有提到的復雜功能!這種情況稱為特征蔓延(feature creep)。在上述例子中,只是恰巧我一個人負責這個合同,我有足夠的精力,所以我認為這是一個有趣的挑戰。
難點3:
這是最大的難點。我選擇的智能(也稱為“模糊”)搜索引擎非常慢……
隨著搜索詞的長度加長,這個搜索算法庫的搜索時間會迅速增加。此外,看看下面這個龐大的列表,里面的數據項極其冗長,用戶需要輸入一個很長的搜索詞才能出現自動補齊提示。別無他法,搜索引擎跟不上用戶的打字速度,這個UI可能會廢掉。
我不知道是不是因為我選擇的這個搜索算法庫太過糟糕,我也不知道是不是因為所有“模糊”搜索算法都是這樣的情形。但幸運的是,我沒有費心去尋找其他選擇。
盡管上述難點大多數是我自己強加上去的,但我依然決定堅持到底,努力優化這種實現。雖然我知道這可能不是最佳策略,但是這個項目的情況允許我這么做,結果將說明一切,最重要的是,對我來說這是一次絕佳的學習體驗和成長的機會。
第二篇 問題剖析
在第一個實現中,我使用了UI的react-autocomplete和react-virtualized庫。
render () { return ( <Autocomplete value={this.state.searchTerm} items={this.getSearchResults()} renderMenu={this.reactVirtualizedList} renderItem={this.renderItem} getItemValue={ item=> item.name } onChange={(e, value)=> this.setState({searchTerm: value})} /> ) }
Autocomplete組件需要傳遞以下幾項:value屬性,需要傳入輸入框中的searchTeam;items屬性,傳入搜索結果;以及renderMenu函數,該函數將搜索結果列表傳遞給react-vertualized。
react-virtualized能夠很好地處理大型列表(它只會渲染列表顯示在屏幕上的一小部分,只有滾動列表時才會更新)。考慮到我們需要渲染的組件并不多,我認為應該不會有太嚴重的性能問題。
更新操作的聲明周期也很簡單:
getSearchResults=()=> { const {searchTerm}=this.state; return searchTerm ? this.searchEngine.search(searchTerm) : [] // searchEngine.search is the expensive search algorithm };
我們來看看結果如何:
哎呀……很糟。在按住刪除鍵時的的確確能感覺到UI的停頓,因為鍵盤觸發delete事件太快了。
不過至少模糊搜索好用了:'anerican'正確地解釋成了'American'。但隨著搜索關鍵字的加長,兩個元素(輸入框和搜索結果)的渲染過程完全跟不上用戶輸入的速度,延遲非常大。
盡管我們的搜索算法的確很慢,但如此大的延遲并不是由于單次搜索時間太長導致的。這里還有另外一個現象需要理解:我們管它叫UI阻塞。理解這個現象需要深入了解Chrome的DevTools性能評測工具。
性能評測
可能你不熟悉這個工具,但是你要知道,熟練使用性能評測工具是深入理解JavaScript的最好方式。這不僅因為它能提供有用的信息幫你解決各種問題,而且按照時間顯示出JavaScript的執行過程能夠幫助你更好地理解好的UI、React、JavaScript事件循環、同步異步執行等概念。
在下面每一節,我都會給出性能分析過程,以及從這些過程中推斷出的有趣的結論。這些數據可能會讓你眼花繚亂,但我會盡力給出合理的解釋!
首先評測一下在Autocomplete中按下兩個鍵時的情況:
X軸:時間,Y軸:按照類型排列的事件(用戶輸入、主線程函數調用)
理解基本的序列非常重要:
首先是用戶的鍵盤輸入(Key Characer)。這些輸入在JavaScript的主線程上觸發了Event(keypress)事件,該事件又觸發了我們的onChange處理函數,該函數會調用setState(圖上看不見,因為它太小了,但它的位置在整個棧的最開頭附近)。這一系列動作標志著重新計算的開始,計算setState對組件會產生何種影響。這一過程稱為更新,或者叫做重新渲染,它會調用幾個React生命周期方法,其中就包括render。這一切都發生在一個執行棧(有時稱為“調用棧”或簡稱為“棧”),在圖中由每個Event (keypress)下方豎直排列的框表示。每個框都是執行棧中的一次函數調用。
這里不要被render這個詞誤導了。React的渲染并不僅僅是在屏幕上繪制。渲染只是React用來計算被更新的元素應當如何顯示的過程。如果仔細查看第二個Event (keypress),就會發現在Event (keypress)框的外面有個小得幾乎看不見的綠條。放大一些就能看到這是對瀏覽器繪制的調用:
這才是UI更新被真正繪制到屏幕上,并在顯示器上顯示新幀的過程。而且,第一個Event (keypress)之后沒有繪制,只有第二個后面才有。
這說明,瀏覽器繪制(外觀上的更新)并不一定會在Event (keypress)事件發生并且React完成更新之后發生。
原因是JavaScript的事件循環和JavaScript對于任務隊列的優先級處理。在React結束計算并將更新寫入DOM之后(稱為提交階段,發生在每個執行棧末尾的地方),你可能會以為瀏覽器應該開始繪制,將DOM更新顯示在屏幕上。但是在繪制之前,瀏覽器會檢查JavaScript事件隊列中是否還有其他任務,有的任務會比繪制更優先執行。
當JavaScript線程忙于渲染第一個keypress時,產生第二個用戶輸入(如鍵盤按下事件)就會出現這種情況(你可以看到第二個Key Character輸入發生在第一個keypress執行棧依然在運行時)。這就是前面提到的UI阻塞。第二個Event (keypress)阻塞了UI更新第一個keypress。
不幸的是,這會導致巨大的延遲。因為渲染本身非常慢(部分原因是因為渲染中包含了昂貴的搜索算法),如果用戶輸入非???,那么很大可能會在前一個執行棧結束之前輸入新的字符。這會產生新的Event (keypress),并且它的優先級比瀏覽器繪制要高。所以繪制會不斷被用戶的輸入拖延。
不僅如此,甚至在用戶停止輸入后,隊列中依然滯留了許多keypress時間,React需要依次對每個keypress進行計算。所以,即使在輸入結束后,也不得不等待這些針對早已不需要的searchTeams的搜索!
注意最后一個Key Character發生后,還有4個Event (keypress)滯留,瀏覽器需要處理完所有事件才能重繪。
改進方法
為了解決這個問題,重要的是要理解好的UI的要素是什么。實際上,對于每次鍵盤輸入,用戶期待的視覺反饋包括兩個獨立的要素:
理解用戶的期望才能找到解決方案。盡管Google搜索快得像閃電一樣,但用戶無法立即收到搜索結果的事情也屢見不鮮。一些UI甚至會在請求搜索結果時顯示加載進度條。
重要的是要理解,對于反應迅速的UI而言,第一種反饋(按下的鍵顯示在輸入框中)扮演了非常重要的角色。如果UI無法做到這一點,就會有嚴重的問題。
查看性能評測是解決問題的第一個提示。仔細觀察這些長長的執行棧和render中包含的昂貴的search方法,我們會發現,更新輸入框和搜索結果的一切操作都發生在同一個執行棧內。所以,兩者的UI更新都會被搜索算法阻塞。
但是,輸入框的更新不需要等待結果!它只需要知道用戶按下了哪個鍵,而不需要知道搜索結果是什么。如果我們有辦法控制事件執行的順序,輸入框就有機會先更新UI,再去渲染搜索結果,這樣就能減少一些延遲。因此,我們的第一個優化措施就是將輸入框的渲染從搜索結果的渲染中分離出來。
注意:熟悉Dan Abramov在React JSConf 2018上的演講的人應該能回憶起這個場景。在他的幻燈片中,他設計了一個昂貴的更新操作,隨著輸入值的增加,屏幕上需要渲染的組件也越來越多。這里我們遇到的困難非常相似,只不過是隨著搜索關鍵字長度的增加,單個搜索函數的復雜度會增加而已。在Dan的演講中,他演示了時間切片(Time Slicing),這個React團隊在開發中的功能也許可以解決這個問題!我們的嘗試會以相似的方案解決問題:找到一個方法來改變渲染的順序,防止昂貴的計算再次阻塞主線程的UI更新。
第三篇 異步渲染(componentDidUpdate)
注意:本篇討論的優化最后以失敗告終了,但我認為值得講一講,因為它能幫助我們更好地理解React的componentDidUpdate生命周期。如果你非常熟悉React,或者只想看看怎樣改善性能問題,那么可以直接跳到下一篇。
拆分組件
由于我們想把昂貴的搜索結果更新從輸入框更新中拆分出來,所以我們應該自己設計一個組件,并放棄使用react-autocomplete庫提供的一站式解決方案Autocomplete:
//autocomplete.js render () { return ( <div> <input onChange={ e=> this.setState({searchTerm: e.target.value})} value={this.state.searchTerm}/> <SearchResults searchEngine={this.props.searchEngine} searchTerm={this.state.searchTerm}/> </div> ) }
然后在SearchResults中,我們需要異步觸發searchEngine.search(searchTerm),而不應該在更新searchTerm的同一個渲染中進行。
我最初的想法是利用SearchResults的componentDidUpdate,讓searchEngine異步工作,因為聽上去這個方法似乎是在更新之后異步執行的。
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { this.setState({ searchResults: searchEngine.search(searchTerm) }) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
我們將昂貴的searchEngine移動到了componentDidUpdate中,這樣就不用在render方法中調用,而是等到更新之后再執行。我希望在輸入框更新之后的另一個執行棧中執行render,并兩者之間執行一次繪制。我想象的新的更新生命周期如下:
很不幸,認為componentDidUpdate會在瀏覽器更新之后運行是一個常見的誤解。我們來看看這個方法的性能評測:
看到問題了嗎?componentDidUpdate跟最初的keypress事件是在同一個執行棧上執行的
componentDidUpdate并不會在繪制結束后執行,因此執行棧跟上一篇一樣昂貴,我們只不過是將昂貴的search方法移動到了不同位置而已。盡管這個解決方案并不能改善性能,但我們可以借此機會理解React的生命周期componentDidUpdate的具體行為。
雖然componentDidUpdate不會在另一個執行棧上運行,但它確實是在React更新完組件狀態并將更新后的DOM值提交之后才執行的。盡管這些更新后的DOM值還沒有被瀏覽器繪制,它們依然反映了更新后的UI應有的樣子。所以,任何componentDidupdate內執行的DOM查詢都能訪問到更新后的值。所以,組件確實更新了,只是瀏覽器中看不見而已。
所以,如果想要做DOM計算,一般都應該在componentDidUpdate中進行。在這里很方便根據布局改變進行更新,比如根據新的布局方式計算元素的位置或大小,然后更新狀態等。
如果componentDidUpdate每次觸發改變布局的更新時都要等待實際的瀏覽器繪制,那么用戶體驗會非常糟糕,因為用戶可能會在布局改變時看到兩次屏幕閃爍。
注(React Hooks):這個差異也有助于理解新的useEffect和useLayoutEffect鉤子。useEffect就是我們在這里嘗試實現的效果。它會讓代碼在另一個執行棧中運行,從而在執行之前瀏覽器可以進行繪制。而useLayoutEffect更像是componentDidUpdate,允許你在DOM更新之后、瀏覽器繪制之前執行代碼。
第四篇 異步渲染(setTimeout)
上一篇我們拆分了組件:
//autocomplete.js render () { return ( <div> <input onChange={ e=> this.setState({searchTerm: e.target.value})} value={this.state.searchTerm}/> <SearchResults searchEngine={this.props.searchEngine} searchTerm={this.state.searchTerm}/> </div> ) }
但我們沒能讓昂貴的searchEngine在另一個執行棧上運行。那么,還有什么辦法能實現這一點呢?
有兩個常見的方法可以設置異步調用:Promise和setTimeout。
Promise
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { /* stick the update with the expensive search method into a promise callback: */ Promise.resolve().then(()=> { this.setState({ searchResults: searchEngine.search(searchTerm) }) }) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
我們來看看性能評測:
(anonymous)是Promise.then()回調函數
又失敗了!
理論上Promsie的回調函數是異步的,因為它們不會同步執行,但實際上還是在同一個執行棧中運行的。
仔細看看性能評測就會發現,回調函數被放在了Run Microtasks下,因為Promise的回調函數被當作了微任務。瀏覽器通常會在完成正常的棧之后檢查并運行微任務。
更多信息:Jake Archibald有一篇非常好的演講(https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DcCOL7MC4Pl0),解釋了JavaScript事件循環在微任務方面的處理,還深入討論了許多我涉及到的話題。
盡管了解這一點很好,但并沒有解決問題。我們需要新的執行棧,這樣瀏覽器才有機會在新的執行棧開始之前進行繪制。
setTimeout
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { /* stick the update with the expensive search method into a setTimeout callback: */ const setTimeoutCallback=()=> { this.setState({ searchResults: searchEngine.search(searchTerm) }) } setTimeout(setTimeoutCallback) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
性能評測:
繪制很難看到,它太小了,但藍線的位置的確發生了繪制
哈哈!成功了!注意到這里有兩個執行棧:Event (keypress)和Timer Fired (searchResults.js:49)。兩個棧之間發生了一次繪制(藍線的位置)。這正是我們想要的!來看看漂亮的UI!
按住刪除鍵時依然有明顯的延遲
有很大的改進,但依然很令人失望……這個頁面的確變好了,但依然能感覺到UI的延遲。我們來仔細看看性能測試。
我們需要一些技巧才能分析這個性能測試報告并找出延遲的原因。使用性能報告有幾個小技巧:
性能評測工具著重顯示了兩個特別有用的Key Input Interactions,每個都有不同的性能度量:
理想狀態下,Key Down交互應該非常短,因為如果JavaScript主線程沒有被阻塞的話,事件應該在鍵盤按下之后立即出發。所以,看到長長的Key Down就意味著發生了UI阻塞問題。
在本例中,長長的Key Down的原因是它們觸發時,主線程還在被昂貴的setTimeoutCallback阻塞,比如最后的Key Down。很不幸,Key Down發生時,運行昂貴的search的setTimeoutCallback剛剛開始,也就是說,Key Down只有等待search的計算結束才能觸發事件。這個評測中的search方法大約花了330毫秒,也就是1/3秒。從優秀的UI角度而言,1/3秒的主線程阻塞實在太長了。
特別引起我注意的是最后一個Key Character。盡管它關聯了Key Down的結束并且觸發了Event (keypress),瀏覽器并沒有繪制新的searchTerm,而是執行了另一個setTimeoutCallback。這就是Key Character交互花了兩倍時間的原因。
這一點著實讓我大跌眼鏡。將search移動到setTimeoutCallback的目的,就是讓瀏覽器能夠在調用setTimeoutCallback之前進行繪制。但是在最后的Event (keypress)之前依然沒有繪制(藍線的位置)。
結論是,我們不能依賴于瀏覽器的隊列機制。顯然瀏覽器并不一定將繪制排在超時回調之前。如果超時回調需要占用主線程330毫秒,那么也會阻礙主線程,導致延遲。
第五篇 多線程
在上一篇中,我們拆分了組件,并成功地使用setTimeout將昂貴的search移動到了另一個執行棧中,因此瀏覽器無需等待 search完成,就可以繪制輸入框的更新:
//autocomplete.js render () { return ( <div> <input onChange={ e=> this.setState({searchTerm: e.target.value})} value={this.state.searchTerm}/> <SearchResults searchEngine={this.props.searchEngine} searchTerm={this.state.searchTerm}/> </div> ) } //searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { const setTimeoutCallback=()=> { this.setState({ searchResults: searchEngine.search(searchTerm) }) } setTimeout(setTimeoutCallback) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
不幸的是,這依然沒有解決每個searchTerm導致search阻塞主線程330毫秒并導致UI延遲的問題。
JavaScript的單線程實在太糟糕了……突然我想到了一個方法。
最近我在閱讀漸進式Web應用,其中有人使用Service Worker來實現一些過程,比如在另一個線程中進行緩存。于是我開始學習Service Worker。這是一個全新的API,我需要花點時間來學習。
但在學習之前我想先通過實驗來驗證一下增加額外的線程是否真的能夠提高性能。
用服務器端來模擬第二個線程
我以前就做過搜索和自動補齊提示的功能,但這次特別困難的原因是需要完全在前端實現。以前我做過用API來獲取搜索結果。其中一種思路是,API和運行API的服務器實際上相當于前端應用利用的另一個線程。
于是,我想到可以做一個簡單的node服務器讓searchTerm訪問,從而實現在另一個線程中運行昂貴的搜索。這個功能非常容易實現,因為這個項目中已經設置過開發用的服務器了。所以我只需添加一個新的路徑:
app.route('/prep-staging/testSearch') .post(bodyParser.json(), (req, res)=> { const {data, searchTerm}=req.body const engine=new SearchEngine(data) const searchResults=engine.search(searchTerm) res.send({searchResult}) })
然后將SearchResults中的setTimeout改成fetch:
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine, data}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { /* ping the search route with the searchTerm and update state with the results when they return: */ fetch(`testSearch`, { method: 'POST', body: JSON.stringify({data, searchTerm}), headers: { 'content-type': 'application/json' } }) .then(r=> r.json()) .then(resp=> this.setState({searchResults: resp.searchResults})) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
現在是見證奇跡的時刻!
注意看刪除!
太棒了!輸入框的更新幾乎非常完美。而另一方面,搜索結果的更新依然非常緩慢,但沒關系!別忘了我們的首要任務就是讓用戶在輸入時盡快獲得反饋。我們來看看性能測試報告:
注意主線程中間漂亮的空白,它不再是之前層層疊疊的樣子了。性能優化中經常會遇到這種情況。主線程中的空白越多越好!
從頂端可以看到testSearch花費的時間。最長的一個實際上花了800毫秒,讓我很吃驚,但仔細想想就會發現,并不是search花了這么長時間,而是我們的Node服務器也是單線程的。它在第一個搜索完成之前無法開始另一個搜索。由于輸入比搜索快得多,所以搜索會進入隊列然后被延遲。搜索函數實際花費的時間是前一個搜索完成之后的部分,大約315毫秒。
總的來說,將昂貴的任務放到服務器線程中,就可以將堆疊的棧移動到服務器上。所以,盡管依然有改進的空間,但主線程看起來非常好,UI的響應速度也更快了!
我們已經證明這個思路是正確的,現在來實現吧!
做了一點研究后我發現,Server Worker并不是正確的選擇,因為它不兼容Internet Explorer。幸運的是,它還有個近親,叫做Web Worker,能兼容所有主流服務器,API更簡單,而且能完成我們需要的功能!
第六篇 Web Worker
Web worker能夠在JavaScript的主線程之外的另一個線程上運行代碼,每個Web worker都由一個腳本文件啟動。啟動方式非常簡單:
//searchResults.js export default class SearchResults extends React.Component { constructor (props) { super(); this.state={ searchResults: [], } //initiate the webworker: this.webWorker=new Worker('...path to webWorker.js') //pass it the 13,000 item search data to initialize the searchEngine with: this.webWorker.postMessage({data: props.data}) //assign the handler that will accept the searchResults when it sends them back: this.webWorker.onmessage=this.handleResults } componentDidUpdate(prevProps) { const {searchTerm}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { //change our async search request to a .postMessage, the messaging API of webWorkers: this.webWorker.postMessage({searchTerm}) } } handleResults=(e)=> { const {searchResults}=e.data this.setState({ searchResults }) } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> } }
下面是webWorker.js腳本,在SearchResults的構造函數中進行初始化:
//webWorker.js self.importScripts('...the search engine script, provides the SearchEngine constructor'); let searchEngine; let cache={} //thought I would add a simple cache... Wait till you see those deletes now :) function initiateSearchEngine (data) { //initiate the search engine with the 13,000 item data set searchEngine=new SearchEngine(data); //reset the cache on initiate just in case cache={}; } function search (searchTerm) { const cachedResult=cache[searchTerm] if(cachedResult) { self.postMessage(cachedResult) return } const message={ searchResults: searchEngine.search(searchTerm) }; cache[searchTerm]=message; //self.postMessage is the api for sending messages to main thread self.postMessage(message) } /*self.onmessage is where we define the handler for messages recieved from the main thread*/ self.onmessage=function(e) { const {data, searchTerm}=e.data; /*We can determine how to respond to the .postMessage from SearchResults.js based on which data properties it has:*/ if(data) { initiateSearchEngine(data) } else if(searchTerm) { search(searchTerm) } }
可以看到,我還加了些額外的代碼。這里我加了緩存,這樣之前搜索過的searchTerms就可以立即返回結果了。如此一來,最耗性能的用戶交互(常按刪除鍵)的效率就提高了。
我們來看看運行情況:
太棒了……非常快!這看起來很不錯啊,實話說,做到這個樣子就可以直接發布了!
但是,現在就發布多沒勁啊……
從用戶體驗的角度來說,這個UI已經非常好了。如果仔細觀察,其實依然能看到搜索結果的延遲,但幾乎察覺不到……不過幸運的是,從性能測試報告中可以看到低效率的地方:
請無視灰色的條,我也不知道它們是怎么來的。
報告中最左側的線程是我們關注的線程。Main線程已經折疊了,因為里面只有大量的空白,意味著主線程的性能非常好,不會成為任何主要的性能瓶頸。
可以看到,Worker線程里堆滿了search調用。從最后一個Key Character輸入(藍線位置)之后就會看到其后果。Worker線程中的最后一個search實際上推遲了3個search才返回。測量一下會發現,延遲大約有850毫秒。而且大部分都是不必要的,因為那三個search都沒用,我們不再需要它們返回的結果了。
你也許在想:“這已經很好了!再優化下去性價比不高??!”
我不這樣認為。首先,不要低估嘗試新事物和探索帶來的價值。如果你從來沒做過探索,就很可能無法評價是否值得,因為你不知道你能有哪些收獲,以及你將投入多少時間和努力。所以,我認為這種探索帶來的經驗和知識是無價的。隨著知識的積累,你能更好地評價投入的時間是否值得,以及是否應該考慮這些問題。你可以做出更好的決定!
其次,別忘了這并不是過早優化。這些優化都是根據性能評價做出的,我們可以測量出效果。不管怎樣評價,如果能改善850毫秒的延遲,那都是非常重大的改進。
最后(但不是唯一),別忘了移動端!雖然我不會在本文中介紹,但我在研究這個問題時,我也跟蹤了移動端的性能,現在的條件下依然有能察覺得到的性能延遲。
不管怎么說,我們來解決這個問題!
第七篇 確保searchTerm
前面的性能評測揭示的最明顯的問題就是,即使對于無用的searchTerm也會運行昂貴的搜索。所以目前的解決方案之一就是在執行昂貴的搜索之前確保searchTerm是最新的。只要在webWorker腳本中加入confirmSearchTerm就可以非常容易地實現:
//webWorker.js self.importScripts('...the search engine script, provides the SearchEngine constructor'); let searchEngine; let cache={} function initiateSearchEngine (data) { searchEngine=new SearchEngine(data); cache={}; } function search (searchTerm) { const cachedResult=cache[searchTerm] if(cachedResult) { self.postMessage(cachedResult) return } const message={ searchResults: searchEngine.search(searchTerm) }; cache[searchTerm]=message; self.postMessage(message) } function confirmSearchTerm (searchTerm) { self.postMessage({confirmSearchTerm: searchTerm}) } self.onmessage=function(e) { const {data, searchTerm, confirmed}=e.data; if(data) { initiateSearchEngine(data) } else if(searchTerm) { /*check if the searchTerm is confirmed, if not, send a confirmSearchTerm message to compare the searchTerm with the latest value on the main thread */ confirmed ? search(searchTerm) : confirmSearchTerm(searchTerm) } }
這里還給SearchResults handleResults加了個額外的條件,監聽confirmSearchTerm的請求:
//searchResults.js export default class SearchResults extends React.Component { constructor (props) { super(); this.state={ searchResults: [], } this.webWorker=new Worker('...path to webWorker.js') this.webWorker.postMessage({data: props.data}) this.webWorker.onmessage=this.handleResults } componentDidUpdate(prevProps) { const {searchTerm}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { this.webWorker.postMessage({searchTerm}) } } handleResults=(e)=> { const {searchResults, confirmSearchTerm}=e.data; /* check if confirmSearchTerm property was sent, if so compare it to the latest searchTerm and send back a confirmed searchTerm message */ if (confirmSearchTerm && confirmSearchTerm===this.props.searchTerm) { this.webWorker.postMessage({ searchTerm: this.props.searchTerm, confirmed: true }) } else if (searchResults) { this.setState({ searchResults }) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> } }
我們來看看性能評測,看看有沒有改進:
很難看出結果,因為它們運行得太快了,但在每次Worker的執行棧之前都會執行confirmSearchTerm(見藍線)。
說明的確管用了:Worker在每次運行昂貴的search方法之前都會確認搜索是否必要。而且可以看到,頂部的橙色部分有4個Key Character輸入,但只運行了三個搜索。這里我們成功地去掉了一個不必要的搜索,節約了最多330毫秒。但之前我們看到,多個額外的搜索會進入隊列然后再不必要地運行,而現在我們完全避免了這個問題。所以,節約的時間非常顯著,特別是在移動端上。
但仔細觀察就會發現我們依然在浪費時間:
最后一個搜索使用了最新的searchTerm,但依然要至少等待當前的搜索完成后才能開始。浪費了84毫秒(藍色高亮部分)!據我所知,執行棧一旦開始就無法取消。那么我們是不是無計可施了呢?
第八篇 Web Worker陣列
如果多加一個線程的效果很好,那么加4個會怎樣?
實話實說,現在這些只是出于興趣……但說真的,最后這次優化確實在移動端上帶來了人眼能察覺到的改善……
無論怎樣,現在我打算用Web Worker陣列!
為了出這份報告,我用人類最快的速度輸入的!
圖中可以看到,頂端每個橙色的Key Character都有一個ww worker線程,能立即開始搜索(橙色條)。不再需要等待前一個搜索結束。而且每個ww在結束后就能用于下一次搜索。
這是因為我們設置了workerArray.js這個web worker作為分發器。圖中看不到,因為它執行得太快了,但對于每個Key Character,workerArray都會執行一個微笑的執行棧,用來處理主線程傳來的搜索請求消息,然后分發給第一個可用的ww worker。
我們成功地解決了“搜索堆”的問題。可以認為用增加車道的方式解決了交通擁堵。
為什么用4個搜索worker呢?因為我在測試時的輸入速度從來沒能到過需要第五個worker的速度(增加worker帶來的改進非常微?。?/p>
結果發現,從Key Character輸入到search執行之間沒有任何延遲。除非找到另一個更有效的搜索算法,否則我們已經成功地移除了所有性能瓶頸。
別忘了,我并沒有說這是最佳的解決方案。是否每臺手機都能處理4個web worker?性能改善是否值得付出這些額外的代碼復雜度?是否還有其他安全方面的考量,導致代碼更加復雜?
這些問題都非常重要,正是這些問題會最終引出這樣的解決方案。
但至少現在,這個Web worker陣列非常優秀!我們來看看實際效果:
感謝你耐心地閱讀完所有的篇節!
如果說這一系列優化有什么感想的話,那就是:
如果你有興趣,可以看看下面Web worker陣列的代碼。
課外作業
下面的代碼中有個小問題。需要一個非常微小的修改才能使它更強壯,最多兩行代碼。你能找到問題所在并修復嗎?
//searchResults.js export default class SearchResults extends React.Component { constructor (props) { super(); this.state={ searchResults: [], } //initiate the worker array: this.workerArray=new WorkerArrayController({ data: props.data, handleResults: this.handleResults, arraySize: 4 }); } componentDidUpdate(prevProps) { const {searchTerm}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { this.workerArray.search({searchTerm}) } } handleResults=(e)=> { const {searchResults}=e.data this.setState({ searchResults }) } componentWillUnmount () { this.workerArray.terminate(); } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> } }
SearchResults組件初始化WorkerArrayController。
//workerArrayController.js export default class WorkerArrayController { constructor ({data, handleResults, arraySize}) { this.workerArray=new Worker('... path to workerArray.js'); let i=1; this.webWorkers={}; while (i <=arraySize) { const workerName=`ww${i}`; this.webWorkers[workerName]=new Worker(`...path to ww1.js`); /* Creates a MessageChannel for each worker and passes that channel's ports to both workerArray dispatcher and the worker so they can communicate with each other */ const channel=new MessageChannel(); this.workerArray.postMessage({workerName}, [channel.port1]); this.webWorkers[workerName].postMessage({data}, [channel.port2]); i++; } this.workerArray.onmessage=handleResults; } search=(searchTerm)=> { this.workerArray.postMessage({searchTerm}); } terminate() { this.workerArray.terminate(); for (const workerName in this.webWorkers) { this.webWorkers[workerName].terminate(); } } }
WorkerArrayController用4個ww初始化workerArray web worker,并傳遞MessageChannel端口給它們,這樣它們能夠互相通信。
//workerArray.js const ports={}; let cache={}; let queue; function initiatePort (workerName, port) { ports[workerName]=port; const webWorker=ports[workerName]; webWorker.inUse=false; webWorker.onmessage=function handleResults (e) { const {searchTerm, searchResults}=e.data; const message={searchTerm, searchResults}; /* If all workers happen to be inUse, the message gets saved to the the queue and passed to the first worker that finishes */ if(queue) { webWorker.postMessage(queue); webWorker.inUse=true; queue=null; } else { webWorker.inUse=false; } cache[searchTerm]=message; self.postMessage(message); } } function dispatchSearchRequest (searchTerm) { const cachedResult=cache[searchTerm]; if(cachedResult) { self.postMessage(cachedResult); return } const message={searchTerm}; for (const workerName in ports) { const webWorker=ports[workerName]; if(!webWorker.inUse) { webWorker.postMessage(message); webWorker.inUse=true; return } } queue=message; } self.onmessage=function (e) { const {workerName, searchTerm}=e.data; if(workerName) { initiatePort(workerName, e.ports[0]); } else if(searchTerm) { dispatchSearchRequest(searchTerm); } }
workerArray初始化端口對象用于通信,并跟蹤每個ww worker。它還初始化了緩存和隊列,萬一所有端口都被占用的情況下用來跟蹤最新的searchTerm請求。
//ww1.js self.importScripts('...the search engine script, provides the SearchEngine constructor'); let searchEngine; let port; function initiate (data, port) { searchEngine=new SearchEngine(data); port=port; port.onmessage=search; } /* search is attached to the port as the message handler so it runs when communicating with the workerArray only */ function search (e) { const {searchTerm}=e.data; const message={ searchResults: searchEngine.search(searchTerm) }; port.postMessage(message) } /* self.onmessage is the handler that responds to messages from the main thread, which only fires during initiation */ self.onmessage=function(e) { const {data}=e.data; initiate(data, e.ports[0]); }
原文:Secrets of JavaScript: A tale of React, performance optimization and multi-threading
本文為 CSDN 翻譯
eact是facebook出的一款針對view視圖層的library庫。react遵循單向數據流的機制。目前我們學的是react17.x的版本
https://react.docschina.org/
<Outlet></Outlet>
*請認真填寫需求信息,我們會在24小時內與您取得聯系。