整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          前端面試題2023年前端面試真題之React篇

          前端面試題2023年前端面試真題之React篇

          么時候使用狀態管理器?

          從項目的整體架構來看,要選擇適合項目背景的極速。如果項目背景不適合使用狀態管理器,那就沒有一定的必要性去使用,比如微信小程序等,可以從以下幾個維度來看

          用戶的使用方式復雜

          • 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
          • 多個用戶之間可以協作
          • 與服務器大量交互,或者使用了WebSocket
          • View要從多個來源獲取數據

          從組件角度看

          • 某個組件的狀態,需要共享
          • 某個狀態需要在任何地方都可以拿到
          • 一個組件需要改變全局狀態
          • 一個組件需要改變另一個組件的狀態

          什么渲染劫持?

          什么是渲染劫持,渲染劫持的概念是控制組件從另一個組件輸出的能力,當然這個概念一般和react中的高階組件(HOC)放在一起解釋比較有明了。

          高階組件可以在render函數中做非常多的操作,從而控制原組件的渲染輸出,只要改變了原組件的渲染,我們都將它稱之為一種渲染劫持。

          實際上,在高階組件中,組合渲染和條件渲染都是渲染劫持的一種,通過反向繼承,不僅可以實現以上兩點,還可以增強由原組件 render 函數產生的 React元素。

          實際的操作中通過操作 state、props 都可以實現渲染劫持

          怎么實現React組件的國際化呢?

          依賴于 i18next 的方案,對于龐大的業務項目有個很蛋疼的問題,那就是 json 文件的維護。每次產品迭代都需要增加新的配置,那么這份配置由誰來維護,怎么維護,都會有很多問題,而且如果你的項目要支持幾十個國家的語言,那么這幾十份文件又怎么維護。

          所以現在大廠比較常用的方案是,使用 AST,每次開發完新版本,通過 AST 去掃描所有的代碼,找出代碼中的中文,以中文為 key,調用智能翻譯服務,去幫項目自動生成 json 文件。這樣,再也不需要人為去維護 json 文件,一切都依賴工具進行自動化。目前已經有大廠開源,比如滴滴的 di18n,阿里的 kiwi

          React如何進行代碼拆分?拆分的原則是什么?

          我認為 react 的拆分前提是代碼目錄設計規范,模塊定義規范,代碼設計規范,符合程序設計的一般原則,例如高內聚、低耦合等等。

          在我們的react項目中:

          • 在 api 層面我們單獨封裝,對外暴露 http 請求的結果。
          • 數據層我們使用的 mobx 封裝處理異步請求和業務邏輯處理。
          • 試圖層,盡量使用 mobx 層面的傳遞過來的數據,修改邏輯。
          • 靜態類型的資源單獨放置
          • 公共組件、高階組件、插件單獨放置
          • 工具類文件單獨放置

          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是只讀的?

          保證react的單向數據流的設計模式,使狀態更可預測。如果允許自組件修改,那么一個父組件將狀態傳遞給好幾個子組件,這幾個子組件隨意修改,就完全不可預測,不知道在什么地方修改了狀態,所以我們必須像純函數一樣保護 props 不被修改

          怎樣使用Hooks獲取服務端數據?

          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;
          


          使用Hooks要遵守哪些原則?

          1. 只在最頂層使用 Hook

          不要在循環,條件或嵌套函數中調用 Hook, 確??偸窃谀愕?React 函數的最頂層調用他們。

          1. 只在 React 函數中調用 Hook

          不要在普通的 JavaScript 函數中調用 Hook。你可以:

          • ? 在 React 的函數組件中調用 Hook
          • ? 在自定義 Hook 中調用其他 Hook

          React Fiber它的目的是解決什么問題?

          React15 的 StackReconciler 方案由于遞歸不可中斷問題,如果 Diff 時間過長(JS計算時間),會造成頁面 UI 的無響應(比如輸入框)的表現,vdom 無法應用到 dom 中。

          為了解決這個問題,React16 實現了新的基于 requestIdleCallback 的調度器(因為 requestIdleCallback 兼容性和穩定性問題,自己實現了 polyfill),通過任務優先級的思想,在高優先級任務進入的時候,中斷 reconciler。

          為了適配這種新的調度器,推出了 FiberReconciler,將原來的樹形結構(vdom)轉換成 Fiber 鏈表的形式(child/sibling/return),整個 Fiber 的遍歷是基于循環而非遞歸,可以隨時中斷。

          更加核心的是,基于 Fiber 的鏈表結構,對于后續(React 17 lane 架構)的異步渲染和 (可能存在的)worker 計算都有非常好的應用基礎

          說出幾點你認為的React最佳實踐

          參考官網

          React為什么要搞一個Hooks?

          官網回答:

          動機

          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

          除了代碼復用和代碼管理會遇到困難外,我們還發現 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 提供了問題的解決方案,無需學習復雜的函數式或響應式編程技術

          狀態管理解決了什么問題?

          專注 view 層

          React 官網是這么簡介的。JavaScript library for building user interfaces.專注 view 層 的特點決定了它不是一個全能框架,相比 angular 這種全能框架,React 功能較簡單,單一。比如說沒有前端路由,沒有狀態管理,沒有一站式開發文檔等。

          f(state)=view

          react 組件是根據 state (或者 props)去渲染頁面的,類似于一個函數,輸入 state,輸出 view。不過這不是完整意義上的 MDV(Model Driven View),沒有完備的 model 層。順便提一句,感覺現在的組件化和 MDV 在前端開發中正火熱,大勢所趨...

          state 自上而下流向、Props 只讀

          從我們最開始寫 React 開始,就了解這條特點了。state 流向是自組件從外到內,從上到下的,而且傳遞下來的 props 是只讀的,如果你想更改 props,只能上層組件傳下一個包裝好的 setState 方法。不像 angular 有 ng-model, vue 有 v-model, 提供了雙向綁定的指令。React 中的約定就是這樣,你可能覺得這很繁瑣,不過 state 的流向卻更清晰了,單向數據流在大型 spa 總是要討好一些的。

          這些特點決定了,React 本身是沒有提供強大的狀態管理功能的,原生大概是三種方式。

          函數式組件有沒有生命周期?

          它沒有提供生命周期概念,不像 class 組件繼承 React.component,可以讓你使用生命周期以及特意強調相關概念

          immutable的原理是什么?

          使用字典樹持久化數據結構,更新時可優化對象生成邏輯,降低成本

          怎么防止HTML被轉義?

          dangerouslySetInnerHTML

          說說你是如何提高組件的渲染效率的

          是什么

          react 基于虛擬 DOM 和高效 Diff算法的完美配合,實現了對 DOM最小粒度的更新,大多數情況下,React對 DOM的渲染效率足以我們的業務日常

          復雜業務場景下,性能問題依然會困擾我們。此時需要采取一些措施來提升運行性能,避免不必要的渲染則是業務中常見的優化手段之一

          如何做

          類組件:

          • 繼承PureComponent
          • 使用shouldComponentUpdate優化

          函數組件:

          • memo模擬PureComponent
          • 使用useMemo緩存變量
          • 使用useCallback緩存函數
          • 循環添加key, key最好用數組項的唯一值,不推薦用 index

          總結

          在實際開發過程中,前端性能問題是一個必須考慮的問題,隨著業務的復雜,遇到性能問題的概率也在增高

          除此之外,建議將頁面進行更小的顆粒化,如果一個過大,當狀態發生修改的時候,就會導致整個大組件的渲染,而對組件進行拆分后,粒度變小了,也能夠減少子組件不必要的渲染

          說說對高階組件(HOC)的理解?

          高階函數(Higher-order function),至少滿足下列一個條件的函數

          • 接受一個或多個函數作為輸入
          • 輸出一個函數

          在React中,高階組件即接受一個或多個組件作為參數并且返回一個組件,本質也就是一個函數,并不是一個組件

          const EnhancedComponent=highOrderComponent(WrappedComponent);
          


          上述代碼中,該函數接受一個組件 WrappedComponent 作為參數,返回加工過的新組件 EnhancedComponent

          高階組件的這種實現方式,本質上是一個裝飾者設計模式

          說說對React refs 的理解?

          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能夠很好地處理大型列表(它只會渲染列表顯示在屏幕上的一小部分,只有滾動列表時才會更新)。考慮到我們需要渲染的組件并不多,我認為應該不會有太嚴重的性能問題。

          更新操作的聲明周期也很簡單:

          • 用戶輸入'a'
          • Autocomplete的onChange處理函數觸發一次渲染,此時this.state.searchTeam='a'
          • 該渲染會調用getSearchResults方法,在這個方法中,搜索引擎使用'a'作為搜索關鍵字計算搜索結果,然后將結果傳遞給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的要素是什么。實際上,對于每次鍵盤輸入,用戶期待的視覺反饋包括兩個獨立的要素:

          • 用戶輸入的鍵顯示在輸入框中;
          • 搜索結果根據新的searchTeam進行更新。

          理解用戶的期望才能找到解決方案。盡管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,并兩者之間執行一次繪制。我想象的新的更新生命周期如下:

          • 輸入框使用新的searchTerm渲染;
          • React把帶有新的searchTerm的componentDidUpdate searchEngine任務放入隊列;
          • 瀏覽器繪制輸入框的更新,然后處理隊列中的searchEngine任務。理論上,瀏覽器不會把這個任務放在繪制任務之前,因為它不涉及用戶交互事件;
          • componentDidUpdate在輸入框繪制之后執行,計算搜索結果并生成新的更新。

          很不幸,認為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:從鍵盤按下到Event (keypress)處理函數被調用的時間間隔;
          • Key Character:從鍵盤按下到瀏覽器繪制更新的時間間隔。

          理想狀態下,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/

          react基礎語法和常見api

          • jsx語法
          • 就是在js種直接使用html標簽來寫。
          • const str = '你好??!'
            const people = [<p>蘋果嘉兒</p>,<p>云寶</p>,<p>紫悅</p>,<p>珍奇</p>]
            function Demo() {
            return <>
            <h1>我是一個組件</h1>
            <p>{str}</p>
            <hr/>
            {
            people}
            <hr/>
            {
            people.length>1?'人數多于一個':'人數少于一個'}
            </>
            }
          • function定義組件
          • 組件傳參
          • 父傳子使用props
          • 子傳父使用方法調用
          • hooks
          • 是react16.8之后新增得功能,可以在function定義得組件中設置局部狀態和模擬組件的生命周期。是目前官方推薦的組件定義方式。
            • useState
            • 可以在function定義的組件中設置局部狀態數據,當狀態數據改變的時候組件會重新更新
            • function App() {
              const [count, setCount] = React.useState(1) //
              // useState返回一個數組,接收一個參數作為初始狀態數據
              // 一個組件中可以定義多次useState,但是它不能放在循環或者條件語句中
              // 第一項表示變量名
              // 第二項表示改變數據的方法
              // 當count值變了,這個function中的代碼會重新執行
              ....
              }
            • useEffect
            • 副作用,接收兩個參數
            • 參數一:回調函數,回調函數可以返回一個function,這個返回值在組件銷毀的時候執行
            • 參數二:依賴數組
            • 當依賴數組中的數據改變的時候,回調函數會執行
            • 關于參數二有以下幾種場景
            • 參數二不存在,沒有參數二。當組件的屬性或者狀態數據改變的時候,參數一的回調函數都會執行
            • 參數二是一個空數組,就是數組沒有內容。只在組件初始化的時候執行一次,用來調接口取數據
            • 參數二是一個正常的數據。當數組中的任意一項發生了改變,那么回調函數就會執行
            • useRef
            • 作用是獲取組件中的dom元素
            • useContext
            • 上下文,在react中實現跨組件之間傳參.
            • useCallback
            • 對function做緩存,接收兩個參數,參數一是一個function,參數二是一個依賴數組。當依賴數組中的數據改變的時候function會重新執行
            • useMemo
            • 對值做緩存,接收兩個參數,參數一是一個返回一個值的function,參數二是一個依賴數組。當依賴數組中的數據改變的時候被緩存的值會重新計算
            • memo
            • 對組件做緩存,主要是做性能優化。當組件接收到的屬性沒有改變的時候組件不會重新渲染
          • class定義組件
          • class定義組件時react中另外一種定義組件的方式。可以在組件內部設置局部狀態和生命周期。當狀態或者屬性數據改變的時候組件會重新渲染
          • class定義的組件和function定義的組件是可以混合使用的
          • class Counter extends React.Component {
            // 每一個組件都需要有一個render方法,這個方法需要一個返回值,返回一個html節點作為當前組件展示的元素內容
            render() {
            return <h1>我是一個計數器</h1>
            }
            }
            • 組件狀態
            • state表示局部狀態
            • 如果要改變狀態數據,需要調用setState方法。setState方法接收兩個參數,一般參數二都不用。改變state數據是異步操作的,在參數二的回調函數中可以獲取state改變之后的最新數據
            • ...
              this.setState({
              count: 18
              },
              function() {
              // 這里可以獲取state的最新數據
              console.log(this.state)
              })
              ...
            • this指向問題
            • 改變this指向有三種寫法:
            • 在constructor中直接綁定(建議使用第一種)
            • 在render的時候進行綁定
            • 使用箭頭函數(如果需要傳參使用第三種)
            • // 在class定義的組件中有一個很麻煩的問題,就是this指向問題
              class Counter extends React.Component {
              // 創建的時候執行
              constructor(props) {
              super(props);
              // 通過this.state可以為組件設置狀態數據
              this.state = {
              count: 1,
              name: "counter計數器",
              };
              this.clickHandle = this.clickHandle.bind(this); // 推薦寫法
              }
              clickHandle() {
              // console.log(this);
              // 在class定義的組件中如果我們要改變state數據,需要調用setState方法
              // setState是異步的,如果想獲取最新的state數據,需要在參數二的回調函數中獲取值
              this.setState(
              {
              count: this.state.count + 1,
              },
              function () {
              console.group("回調函數中獲取");
              console.log(this.state.count);
              console.groupEnd();
              }
              );
              console.log(this.state.count);
              }
              clickHandle2() {
              this.setState({
              count: this.state.count + 2,
              });
              }
              clickHandle3() {
              this.setState({
              count: this.state.count + 3,
              });
              }
              // 在屬性或者數據改變的時候會調用
              render() {
              console.log("render執行了");
              return (
              <>
              <h1>
              {
              this.state.name}-{this.state.count}
              </h1>
              <button onClick={this.clickHandle}>加+</button>
              <button onClick={this.clickHandle2.bind(this)}>加+2</button>
              <button onClick={() => this.clickHandle3()}>加+3</button>
              </>
              );
              }
              }
              const App = () => (
              <>
              <h1>我是組件</h1>
              <Counter />
              </>
              );
              ReactDOM.render(<App />, document.querySelector("#app"));
            • PureComponent
            • 對組件做性能優化,組件屬性只做淺比較。當屬性沒有改變的時候組件不會重新渲染。相當于function定義組件中的memo方法
            • contextType
            • 通過為組件設置屬性contextType可以在組件內部獲取context上下文中提供的數據
            • const { createContext, Component } = React;
              const context = createContext();
              class AppProvider extends Component {
              constructor(props) {
              super(props);
              }
              render() {
              return (
              <context.Provider value={{ name: "class定義的組件" }}>
              {
              this.props.children}
              </context.Provider>
              );
              }
              }
              class Home extends Component {
              constructor(props) {
              super(props);
              }
              render() {
              console.log(this.context);
              return (
              <>
              <h1>首頁</h1>
              <p>{this.context.name}</p>
              </>
              );
              }
              }
              Home.contextType = context; // 通過為組件設置一個contextType屬性把context上下文中的value數據映射到組件內部
              const App = () => (
              <>
              <h1>我是組件</h1>
              <Home />
              </>
              );
              ReactDOM.render(
              <AppProvider>
              <App />
              </AppProvider>,
              document.querySelector("#app")
              );
            • 組件生命周期
            • class組件的生命周期16.3之前和之后有差異
            • 16.3之前的鉤子函數:
              • constructor(不算生命周期鉤子函數,應該是class的構造函數,可以在這里面設置state數據和獲取props屬性)
              • componentWillMount,組件掛載之前執行
              • componentDidMount【很重要】,class定義的組件在這里獲取數據
              • componentWillReceiveProps,將要接收新的屬性
              • shouldComponentUpdate,組件是否需要更新。主要用來做性能優化的時候需要操作的生命周期函數。返回bool值,true后續的更新階段會執行;否則不更新
              • componentWillUpdate,將要更新
              • componentDidUpdate,更新完成
              • componentWillUnmount,卸載
            • 16.3之后的刪除了will開頭的三個函數(componentWillMount、componentWillReceiveProps、componentWillUpdate)。新增了幾個鉤子函數,作為了解聽過就行
            • function定義的組件中useEffect對應class定義組件中的三個生命周期鉤子函數:componentDidMount、componentDidUpdate、componentWillUnmount
            • 和function定義的組件的useEffect對應:
            • // 參數二為空數組,對應componentDidMount
              useEffect(()=>{
              return () => {
              // 對應componentWillUnmount
              }
              }, [])
              // 對應componentDidUpdate
              useEffect(()=>{

              }, [
              xx])
            • 嵌套組件的生命周期問題:如果組件出現嵌套,那么在執行父組件的更新或者掛載完成之前會完成所有子組件的掛載和更新。最后才會執行外圍的掛載和更新完成
            • class組件中使用ref
            • 用來獲取組件中的dom元素
            • function和class定義組件的區別
            • function定義的組件中不存在this指向問題,function定義的組件又叫無狀態組件但是在react16.8之后新增了hooks,可以在function定義的組件中模擬局部狀態和生命周期
            • class定義的組件有自己的局部狀態和生命周期
            • 目前官方推薦使用function的方式定義組件
            • 備注:hooks在2019年的時候已經開始推薦并普及使用,antd是react中一個非常知名的ui組件庫,它在4.0之后的版本中已經開始全面使用function+hooks的寫法
          • useReducer
          • 是react中的一個hooks,可以讓我們使用類似redux中的數據流機制進行狀態數據的管理
          • reducer是一個function,這個function接收兩個參數(state,action),返回一個新的state數據
            • state表示狀態數據
            • action表示行為,必須包含一個type屬性
          • 高階組件

          react腳手架

          • create-react-app
          • create-react-app(CRA)是官方的腳手架,這個腳手架只提供基礎的一些插件,是基于webpack的。
          • npx create-react-app first-react-app # 創建一個react項目
            cd first-react-app
            npm start # 啟動項目
          • 如果電腦上安裝的有yarn這個模塊管理工具,那么cra默認使用的是yarn做模塊管理
          • yarn
          • 它是facebook推出的一款包管理工具,和npm是一樣的功能
          • https://yarn.bootcss.com/
          • npm i yarn -g # 安裝yarn
            # 使用
            yarn add xx # 安裝模塊
            yarn remove xx # 刪除模塊
            yarn # 找到package.json文件,根據依賴項安裝所有的依賴
          • vite
          • 它是另外的一種創建項目的方式,使用的不是webpack。下一代前端開發與構建工具
          • https://cn.vitejs.dev/
          • npm init vite@latest
            # or
            yarn create vite
          • 使用sass
          • npm i sass -D # 安裝sass插件就行
          • 路由
          • https://reactrouter.com/
          • 安裝
          • npm i react-router-dom # 安裝路由插件
          • 使用
            • HashRouter或者BrowserRouter
            • 路由組件的根節點,一個項目中只能有一個
            • Route
            • 路由組件,每一個路徑對應顯示的內容在這里標準
              • path
              • 表示路由的路徑,就是url中訪問的地址,訪問指定路徑的時候展示對應的組件
              • exact
              • 表示絕對匹配
            • Route對應展示的組件有三種寫法
              • 直接寫成子標簽children的形式
              • <Route path="/list">
                <List/>
                </Route>
              • 推薦用法,在List組件的屬性中不能直接獲取路由對象信息,但可以通過withRouter方法實現
              • 寫成屬性component
              • <Route path="/list" component={List} />
              • 可以直接在List組件的屬性中獲取路由對象信息
              • 通過render方法展示
              • <Route path="/list" render={()=><List/>} />
              • 在List組件的屬性中不能直接獲取路由對象信息,但可以通過withRouter方法實現
            • Link
            • 生成跳轉鏈接
            • Switch
            • 只匹配一個
            • withRouter
            • 作用是把路由對象的屬性信息映射到組件的屬性上
            • import React from "react";
              import { withRouter } from "react-router-dom";

              function Products(props) {
              console.group("products");
              console.log(props);
              console.groupEnd();
              return (
              <div>
              <h2>商品列表</h2>
              </div>
              );
              }

              export default withRouter(Products);
            • 路由傳參
              • params
              • 需要在路由中設置占位符。如果沒有占位符,那么刷新之后會消失
              • search
              • 就是直接在url中傳遞,通過?進行分割。刷新之后也不會消失。建議使用search
              • state
              • 直接傳遞。刷新之后直接消失
              • useLocation
              • 獲取location數據,可以用來獲取參數
              • useHistory
              • 獲取歷史記錄信息,可以使用push、go等方法做跳轉
              • useParams
              • 獲取parmas參數
          • antd
          • ant.design是螞蟻金服出的一款ui組件庫。
          • https://ant-design.gitee.io/index-cn
          • npm i antd # 安裝依賴
          • 管理后臺項目
          • redux
          • https://redux.js.org/
          • redux是一個全局狀態管理插件。它是單向數據流的。
          • 單向數據流:數據是單向流動的,分為三部分(state,action,view)。在view視圖中通過dispatch派發一個action改變數據,數據改變之后view視圖重新渲染
            • action
            • 行為,每一個action都建議有一個type屬性,type不能重復。因為在dispatch派發的時候所有的reducer都會收到這個action,根據type不一樣來做不同的處理
            • state
            • 存儲數據
            • reducer
            • 改變數據的地方,reducer是一個function。接收兩個參數
            • 參數一 表示state的初始值
            • 參數二是一個action
            • 安裝使用
            • npm i redux # 安裝插件,是核心模塊
              npm i react-redux # 插件的作用是實現redux和react的連接
            • redux中的方法
              • createStore 作用是創建一個數據存儲
              • combineReducers 合并多個reducer為一個
            • react-redux中的方法
              • Provider
              • 是一個組件需要放在項目的最外層,用來把store數據傳遞給整個項目
              • connect
              • 實現數據以及dispatch方法和組件的關聯,把數據和dispatch方法映射到組件的屬性上
              • useSelector
              • 獲取state數據
              • useDispatch
              • 獲取dispatch方法
            • redux-thunk
            • 插件的作用是用來處理異步action。所有的action必須返回一個plain object,不能是一個function。為了解決異步問題,可以借助插件實現
            • redux-thunk,它的作用是判斷action的數據類型,如果是function,那么會把dispatch當參數傳遞給function;如果是一個對象,那么就直接執行
            • redux-toolkits
            • 它目前是redux官方推薦的寫法
            • npm install @reduxjs/toolkit react-redux # 安裝依賴
          • umi
          • https://umijs.org/
          • umi是一個企業級的開發框架,內置了react、react-router、dva、antd等插件,可以實現開箱即用
            • dva
            • 是一個redux的封裝,內置了常見的redux插件:redux-thunk、redux-saga、redux-devtools等等,可以讓我們很方便的來實現redux的使用
          • ts和antd pro
            • antd pro
            • https://pro.ant.design/zh-CN/docs/getting-started
            • # 使用 npm
              npx create-umi myapp
              # 使用 yarn
              yarn create umi myapp
            • ts
            • TypeScript是js的一個超集。就是在js語言的基礎上加入了數據類型的概念,所有的ts語句最終都會被編譯成js進行運行
            • https://www.tslang.cn/ ts的官網
            • 在ts中當出現報錯警告的時候,直接定義數據類型為any
            • http://json2ts.com/ 作用是把json數據轉換為ts數據結構,用來在項目中對接口返回的數據做處理
          • 其他ui框架
            • ionic
            • 這是一個移動端開發框架,是用來做手機頁面的
            • https://ionicframework.com/
            • 可以讓我們的做的頁面具有原生app一模一樣的效果
            • npm install -g @ionic/cli
              ionic start --type=react
            • framework7
            • https://framework7.io/
            • maturial
            • https://mui.com/zh/ 這個是一定要知道的。是google推出的,目前比較流行的flutter就是使用的這種設計風格
          • next.js
          • 它是一個react的服務器端渲染框架,可以做多頁面應用程序開發。解決了單頁面應用程序存在的所有弊端。
          • npx create-next-app first-next-app-pre # 創建一個next項目Reach route61.基礎更改<Route>特性變更
            path:與當前頁面對應的URL匹配
            element新增,用于決定路由匹配時,渲染組件組件,代替,v5的compoent和render
            <Routers>代替了Switch>
            <Outlet></Outlet>嵌套路由更簡單
            useNavigate代替useHitory
            移除Navlink的activeClass和activeStyle
            鉤子useRouter代替react-router-config
            index用于嵌套路由,僅匹配父路由路徑時,設置渲染
            2.路由重定向 Navigate使用
            <Route path="*" element={<Navigate to="/film"/>}/>
            3.組件嵌套
           <Outlet></Outlet> 

          主站蜘蛛池模板: 国产色精品vr一区区三区| 亚洲一区二区三区免费在线观看| 亚洲av无码一区二区乱子伦as| 国产av熟女一区二区三区| 无码精品一区二区三区在线| 亚洲一区中文字幕| 中文字幕无码免费久久9一区9| 伊人久久大香线蕉AV一区二区| 国产一区二区影院| 成人毛片无码一区二区| 国产成人精品视频一区二区不卡| 一区二区三区在线免费观看视频 | 国产精品一区二区久久精品涩爱| 一区二区三区精品视频| 日韩一区二区视频在线观看| 国产精品亚洲产品一区二区三区| 日本片免费观看一区二区| 亚洲无线码在线一区观看| 一级毛片完整版免费播放一区| 亚洲综合无码一区二区痴汉| 精品欧洲AV无码一区二区男男| 国产福利一区二区三区在线视频| 亚洲电影一区二区| 国产精品夜色一区二区三区| 精品国产一区二区三区久久影院| 国产在线精品一区二区不卡| 久久久久人妻精品一区三寸蜜桃| 亚洲一区二区三区免费| 日本精品高清一区二区2021| 亚洲av高清在线观看一区二区 | 波多野结衣一区在线| 国产精品无码亚洲一区二区三区| 51视频国产精品一区二区| 亚洲AV成人一区二区三区在线看| 精品国产乱子伦一区二区三区 | 一本大道在线无码一区| 蜜桃传媒一区二区亚洲AV| 亚洲AV无码一区二区大桥未久| 日美欧韩一区二去三区| 精品无码人妻一区二区三区不卡 | 秋霞日韩一区二区三区在线观看|