2023面試真題之框架篇

語言: CN / TW / HK

highlight: a11y-dark theme: Chinese-red


閱讀使人充實,會談使人敏捷,寫作使人精確

大家好,我是柒八九

今天,我們繼續2023前端面試真題系列。我們來談談關於前端框架的相關知識點。

如果,想了解該系列的文章,可以參考我們已經發布的文章。如下是往期文章。

文章list

  1. 2023前端面試真題之JS篇
  2. 2023面試真題之CSS篇
  3. 2023面試真題之瀏覽器篇
  4. 22023面試真題之網路篇

你能所學到的知識點

  1. React Diff 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  2. setState同步非同步問題 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  3. React 18新特性 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  4. React 生命週期 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  5. Hook的相關知識點 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  6. ref能否拿到函式元件的例項 推薦閱讀指數⭐️⭐️⭐️
  7. useCallbck vs useMemo的區別 推薦閱讀指數⭐️⭐️⭐️
  8. React.memo 推薦閱讀指數⭐️⭐️⭐️⭐️
  9. 類元件和函式元件的區別 推薦閱讀指數⭐️⭐️⭐️⭐️
  10. componentWillUnmount在瀏覽器重新整理後,會執行嗎 推薦閱讀指數⭐️⭐️⭐️
  11. React 元件優化 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  12. React-Router實現原理 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  13. XXR 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  14. WebComponents 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  15. Lit 推薦閱讀指數⭐️⭐️⭐️⭐️
  16. npm 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  17. yarn 推薦閱讀指數⭐️⭐️⭐️⭐️
  18. pnpm 推薦閱讀指數⭐️⭐️⭐️⭐️
  19. yarn PnP 推薦閱讀指數⭐️⭐️⭐️⭐️
  20. npm install 發生了啥 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  21. 使用 history 模式的前端路由時靜態資源伺服器配置詳解 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  22. webpack 優化 推薦閱讀指數⭐️⭐️⭐️⭐️
  23. Redux內部實現 推薦閱讀指數⭐️⭐️⭐️⭐️ 24.Vue和 React的區別 推薦閱讀指數⭐️⭐️⭐️⭐️
  24. Webpack有哪些常用的loader和plugin 推薦閱讀指數⭐️⭐️⭐️⭐️
  25. Babel 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  26. Fiber 實現時間切片的原理 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
  27. devServer進行跨域處理 推薦閱讀指數⭐️⭐️⭐️
  28. React-Hook 實現原理 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️

好了,天不早了,乾點正事哇。

React Diff

React中,diff演算法需要與虛擬DOM配合才能發揮出真正的威力。React會使用diff演算法計算出虛擬DOM真正發生變化的部分,並且只會針對該部分進行dom操作,從而避免了對頁面進行大面積的更新渲染,減小效能的開銷

React diff演算法

傳統的diff演算法中複雜度會達到O(n^3)React中定義了三種策略,在對比時,根據策略只需遍歷一次樹就可以完成對比,將複雜度降到了O(n)

  1. tree diff:在兩個樹對比時,只會比較同一層級的節點,會忽略掉跨層級的操作

  2. component diff:在對比兩個元件時,首先會判斷它們兩個的型別是否相同

    • 如果不是,則將該元件判斷為 dirty component,從而替換整個元件下的所有子節點
  3. element diff:對於同一層級的一組節點,會使用具有唯一性的key來區分是否需要建立,刪除,或者是移動。

Element Diff

當節點處於同一層級時,React diff 提供了三種節點操作,分別為: 1. INSERT_MARKUP(插入) - 新的 component 型別不在老集合裡, 即是全新的節點,需要對新節點執行插入操作 2. MOVE_EXISTING(移動) - 在老集合有新 component 型別,且 element 是可更新的型別,這種情況下 prevChild=nextChild,就需要做移動操作,可以複用以前的 DOM 節點。 3. REMOVE_NODE(刪除) - 老 component 型別,在新集合裡也有,但對應的 element 不同則不能直接複用和更新,需要執行刪除操作, - 或者老 component 不在新集合裡的,也需要執行刪除操作

存在如下結構:

新老集合進行 diff 差異化對比,通過 key 發現新老集合中的節點都是相同的節點,因此無需進行節點刪除和建立,只需要將老集合中節點的位置進行移動,更新為新集合中節點的位置,此時 React 給出的 diff 結果為:B、D 不做任何操作,A、C 進行移動操作

  • 首先對新集合的節點進行迴圈遍歷,for (name in nextChildren)
  • 通過唯一 key 可以判斷新老集合中是否存在相同的節點if (prevChild === nextChild)
  • 如果存在相同節點,則進行移動操作
  • 但在移動前需要將當前節點在老集合中的位置lastIndex 進行比較,
    • if (child._mountIndex < lastIndex),則進行節點移動操作,否則不執行該操作。
  • lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置(即最大的位置)
  • 如果新集合中當前訪問的節點lastIndex 大,說明當前訪問節點在老集合中就比上一個節點位置靠後,則該節點不會影響其他節點的位置,因此不用新增到差異佇列中,即不執行移動操作
  • 只有當訪問的節點比 lastIndex 小時,才需要進行移動操作

當完成新集合中所有節點 diff 時,最後還需要對老集合進行迴圈遍歷,判斷是否存在新集合中沒有但老集合中仍存在的節點,發現存在這樣的節點 x,因此刪除節點 x,到此 diff 全部完成。


setState同步非同步問題

18.x之前版本

如果直接在setState後面獲取state的值是獲取不到的。 - 在React內部機制能檢測到的地方setState就是非同步的; - 在React檢測不到的地方,例如 原生事件addEventListener,setInterval,setTimeoutsetState就是同步更新的

setState並不是單純的非同步或同步,這其實與呼叫時的環境相關

  • 合成事件生命週期鉤子(除componentDidUpdate) 中,setState是"非同步"的;
  • 原生事件setTimeout 中,setState是同步的,可以馬上獲取更新後的值;

批量更新

多個順序的setState不是同步地一個一個執行滴,會一個一個加入佇列,然後最後一起執行。在 合成事件生命週期鉤子 中,setState更新佇列時,儲存的是 合併狀態(Object.assign)。因此前面設定的 key 值會被後面所覆蓋,最終只會執行一次更新

非同步現象原因

setState 的“非同步”並不是說內部由非同步程式碼實現,其實本身執行的過程和程式碼都是同步的,只是合成事件和生命鉤子函式的呼叫順序在更新之前,導致在合成事件鉤子函式中沒法立馬拿到更新後的值,形成了所謂的“非同步”,當然可以通過第二個引數setState(partialState, callback)中的callback拿到更新後的結果。

setState 並非真非同步,只是看上去像非同步。在原始碼中,通過 isBatchingUpdates 來判斷

setState呼叫流程: 1. 呼叫this.setState(newState) 2. 將新狀態newState存入pending佇列 3. 判斷是否處於batch UpdateisBatchingUpdates是否為true) - isBatchingUpdates=true,儲存元件於dirtyComponents中,走非同步更新流程,合併操作,延遲更新; - isBatchingUpdates=false,走同步過程遍歷所有的dirtyComponents,呼叫updateComponent,更新pending state or props

為什麼直接修改this.state無效

setState本質是通過一個佇列機制實現state更新的。 執行setState時,會將需要更新的state合併後放入狀態佇列,而不會立刻更新state,佇列機制可以批量更新state

如果不通過setState而直接修改this.state,那麼這個state不會放入狀態佇列中,下次呼叫setState時對狀態佇列進行合併時,會忽略之前直接被修改state,這樣我們就無法合併了,而且實際也沒有把你想要的state更新上去

React18

v18 之前只在事件處理函式中實現了批處理,在 v18 中所有更新都將自動批處理,包括 promise鏈setTimeout等非同步程式碼以及原生事件處理函式


React 18新特性

Reactv16v18 主打的特性包括三個變化:

  • v16: Async Mode (非同步模式)
  • v17: Concurrent Mode (併發模式)
  • v18: Concurrent Render (併發更新)

ReactFiber 樹的更新流程分為兩個階段 render 階段和 commit 階段。 1. 元件的 render 函式執行時稱為 render(本次更新需要做哪些變更),純 js 計算; 2. 而將 render 的結果渲染到頁面的過程稱為 commit (變更到真實的宿主環境中,在瀏覽器中就是操作DOM)。

Sync 模式下,render 階段是一次性執行完成;而在 Concurrent 模式下 render 階段可以被拆解,每個時間片內執行一部分,直到執行完畢。由於 commit 階段有 DOM 的更新,不可能讓 DOM 更新到一半中斷,必須一次性執行完畢。

React 併發新特性

併發渲染機制concurrent rendering的目的:根據使用者的裝置效能網速對渲染過程進行適當的調整, 保證 React 應用在長時間的渲染過程中依舊保持可互動性,避免頁面出現卡頓或無響應的情況,從而提升使用者體驗

  1. 新 root API
    • 通過 createRoot Api 手動建立 root 節點。
  2. 自動批處理優化 Automatic batching
    • React 將多個狀態更新分組到一個重新渲染中以獲得更好的效能。(將多次 setstate 事件合併)
    • v18 之前只在事件處理函式中實現了批處理,在 v18 中所有更新都將自動批處理,包括 promise鏈setTimeout等非同步程式碼以及原生事件處理函式
    • 想退出自動批處理立即更新的話,可以使用 ReactDOM.flushSync() 進行包裹
  3. startTransition
    • 可以用來降低渲染優先順序。分別用來包裹計算量大的 functionvalue,降低優先順序,減少重複渲染次數
    • startTransition 可以指定 UI 的渲染優先順序,哪些需要實時更新,哪些需要延遲更新
    • hook 版本的 useTransition,接受傳入一個毫秒的引數用來修改最遲更新時間,返回一個過渡期的pending 狀態和startTransition函式。
  4. useDefferdValue
    • 通過 useDefferdValue 允許變數延時更新,同時接受一個可選的延遲更新的最大值。React 將嘗試儘快更新延遲值,如果在給定的 timeoutMs 期限內未能完成,它將強制更新
    • const defferValue = useDeferredValue(value, { timeoutMs: 1000 })
    • useDefferdValue 能夠很好的展現併發渲染時優先順序調整的特性,可以用於延遲計算邏輯比較複雜的狀態,讓其他元件優先渲染,等待這個狀態更新完畢之後再渲染。

React 生命週期

生命週期 React 的 生命週期主要有兩個比較大的版本,分別是 1. v16.0 2. v16.4兩個版本

的生命週期。

v16.0前

總共分為四大階段: 1. {初始化| Intialization} 2. {掛載| Mounting} 3. {更新| Update} 4. {解除安裝| Unmounting}


Intialization(初始化)

在初始化階段,會用到 constructor() 這個建構函式,如:

javascript constructor(props) { super(props); } - super的作用 - 用來呼叫基類的構造方法( constructor() ), - 也將父元件的props注入給子元件,供子元件讀取 - 初始化操作,定義this.state的初始內容 - 只會執行一次


Mounting(掛載)(3個)

  1. componentWillMount在元件掛載到DOM前呼叫
    • 這裡面的呼叫的this.setState不會引起元件的重新渲染,也可以把寫在這邊的內容提到constructor(),所以在專案中很少。
    • 只會呼叫一次
  2. render: 渲染
    • 只要propsstate發生改變(無論值是否有變化,兩者的重傳遞和重賦值,都可以引起元件重新render),都會重新渲染render
    • return是必須的,是一個React元素,不負責元件實際渲染工作,由React自身根據此元素去渲染出DOM
    • render純函式,不能執行this.setState
  3. componentDidMount元件掛載到DOM後呼叫
    • 呼叫一次

Update(更新)(5個)

  1. componentWillReceiveProps(nextProps):調用於props引起的元件更新過程中
    • nextProps:父元件傳給當前元件新的props
    • 可以用nextPropsthis.props來查明重傳props是否發生改變(原因:不能保證父元件重傳的props有變化)
    • 只要props發生變化就會,引起呼叫
  2. shouldComponentUpdate(nextProps, nextState)用於效能優化
    • nextProps:當前元件的this.props
    • nextState:當前元件的this.state
    • 通過比較nextPropsnextState,來判斷當前元件是否有必要繼續執行更新過程。
    • 返回false:表示停止更新,用於減少元件的不必要渲染,優化效能
    • 返回true:繼續執行更新
    • componentWillReceiveProps()中執行了this.setState,更新了state,但render(如shouldComponentUpdatecomponentWillUpdate),this.state依然指向更新前的state,不然nextState及當前元件的this.state的對比就一直是true
  3. componentWillUpdate(nextProps, nextState)元件更新前呼叫
    • render方法前執行
    • 由於元件更新就會呼叫,所以一般很少使用
  4. render:重新渲染

  5. componentDidUpdate(prevProps, prevState)元件更新後被呼叫

    • prevProps:元件更新前的props
    • prevState:元件更新前的state
    • 可以操作元件更新的DOM

Unmounting(解除安裝)(1個)

componentWillUnmount元件被解除安裝前呼叫

可以在這裡執行一些清理工作,比如清除元件中使用的定時器,清除componentDidMount手動建立的DOM元素等,以避免引起記憶體洩漏


React v16.4

v16.0的生命週期相比 - 新增了 -- (兩個getXX) 1. getDerivedStateFromProps 2. getSnapshotBeforeUpdate - 取消了 -- (三個componmentWillXX) 1. componentWillMount、 2. componentWillReceiveProps、 3. componentWillUpdate

getDerivedStateFromProps

getDerivedStateFromProps(prevProps, prevState):元件建立和更新時呼叫的方法

  • prevProps:元件更新前的props
  • prevState:元件更新前的state

React v16.3中,在建立和更新時,只能是由父元件引發才會呼叫這個函式,在React v16.4改為無論是Mounting還是Updating,全部都會呼叫。

是一個靜態函式,也就是這個函式不能通過this訪問到class的屬性。

如果props傳入的內容不需要影響到你的state,那麼就需要返回一個null,這個返回值是必須的,所以儘量將其寫到函式的末尾。

在元件建立時和更新時的render方法之前呼叫,它應該 - 返回一個物件來更新狀態 - 或者返回null來不更新任何內容

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps,prevState):Updating時的函式,在render之後呼叫

  • prevProps:元件更新前的props
  • prevState:元件更新前的state

可以讀取,但無法使用DOM的時候,在元件可以在可能更改之前從DOM捕獲一些資訊(例如滾動位置)

返回的任何值都將作為引數傳遞給componentDidUpdate()


Note

17.0的版本,官方徹底廢除 - componentWillMount、 - componentWillReceiveProps、 - componentWillUpdate


Hook的相關知識點

react-hooksReact 16.8的產物,給函式式元件賦上了生命週期

React v16.8中的hooks

useState

useState:定義變數,可以理解為他是類元件中的this.state 使用:

jsx const [state, setState] = useState(initialState);

  • state:目的是提供給 UI,作為渲染檢視的資料來源
  • setState:改變 state 的函式,可以理解為this.setState
  • initialState:初始預設值

useState 有點類似於PureComponent,會進行一個比較淺的比較,如果是物件的時候直接傳入並不會更新

解決傳入物件的問題

使用 useImmer 替代 useState。

immer.js 這個庫,是基於 proxy 攔截 gettersetter 的能力,讓我們可以很方便的通過修改物件本身,建立新的物件。

React 通過 Object.is 函式比較 props,也就是說對於引用一致的物件,react是不會重新整理檢視的,這也是為什麼我們不能直接修改呼叫 useState 得到的 state 來更新檢視,而是要通過 setState 重新整理檢視,通常,為了方便,我們會使用 es6spread 運算子構造新的物件(淺拷貝)。

對於巢狀層級多的物件,使用 spread 構造新的物件寫起來心智負擔很大,也不易於維護

常規的處理方式是對資料進行deepClone,但是這種處理方式針對結構簡單的資料來講還算OK,但是遇到大資料的話,就不夠優雅了。

所以,我們可以直接使用 useImmer 這個語法糖來進一步簡化呼叫方式

```javascript const [state,setState] = useImmer({ a: 1, b: { c: [1,2] d: 2 }, });

setState(prev => { prev.b.c.push(3); })) ```


useEffect

useEffect副作用,你可以理解為是類元件的生命週期,也是我們最常用的鉤子

副作用(Side Effect):是function 做了和本身運算返回值無關的事,如請求資料、修改全域性變數,列印、資料獲取、設定訂閱以及手動更改 React 元件中的 DOM 都屬於副作用操作

  1. 不斷執行
    • useEffect不設立第二個引數時,無論什麼情況,都會執行
  2. 根據依賴值改變
    • 設定useEffect的第二個值

useContext

useContext上下文,類似於Context:其本意就是設定全域性共享資料,使所有元件可跨層級實現資料共享

useContent引數一般是由createContext的建立,通過 xxContext.Provider 包裹的元件,才能通過 useContext 獲取對應的值

存在的問題及解決方案

useContextReact 官方推薦的共享狀態的方式,然而在需要共享狀態的元件非常多的情況下,這有著嚴重的效能問題,例如有A/B元件, A 元件只更新 state.a,並沒有用到 state.b,B 元件更新 state.b 的時候 A 元件也會重新整理,在元件非常多的情況下,就卡死了,使用者體驗非常不好。

解決上述問題,可以使用 react-tracked 這個庫,它擁有和 useContext 差不多的 api,但基於 proxy 和元件內部的 useForceUpdate 做到了自動化的追蹤,可以精準更新每個元件,不會出現修改大的 state,所有元件都重新整理的情況


useReducer

useReducer:它類似於redux功能的api

javascript const [state, dispatch] = useReducer(reducer, initialArg, init); - state:更新後的state值 - dispatch:可以理解為和useStatesetState一樣的效果 - reducer:可以理解為reduxreducer - initialArg:初始值 - init:惰性初始化


useMemo

useMemo:與memo的理念上差不多,都是判斷是否滿足當前的限定條件來決定是否執行callback函式,而useMemo的第二個引數是一個陣列,通過這個陣列來判定是否執行回撥函式

當一個父元件中呼叫了一個子元件的時候,父元件的 state 發生變化,會導致父元件更新,而子元件雖然沒有發生改變,但也會進行更新。

只要父元件的狀態更新,無論有沒有對子元件進行操作,子元件都會進行更新useMemo就是為了防止這點而出現的。


useCallback

useCallbackuseMemo極其類似,唯一不同的是 - useMemo返回的是函式執行的結果 - 而useCallback返回的是函式 - 這個函式是父元件傳遞子元件的一個函式,防止做無關的重新整理, - 其次,這個子元件必須配合React.memo,否則不但不會提升效能,還有可能降低效能

存在的問題及解決方案

一個很常見的誤區是為了心理上的效能提升把函式通通使用 useCallback 包裹,在大多數情況下,javascript 建立一個函式的開銷是很小的,哪怕每次渲染都重新建立,也不會有太大的效能損耗,真正的效能損耗在於,很多時候 callback 函式是元件 props 的一部分,因為每次渲染的時候都會重新建立 callback 導致函式引用不同,所以觸發了元件的重渲染。然而一旦函式使用 useCallback 包裹,則要面對宣告依賴項的問題,對於一個內部捕獲了很多 state 的函式,寫依賴項非常容易寫錯,因此引發 bug。

所以,在大多數場景下,我們應該只在需要維持函式引用的情況下使用 useCallback

```jsx const [userText, setUserText] = useState(""); const handleUserKeyPress = useCallback(event => { // do something here }, []);

useEffect(() => { window.addEventListener("keydown", handleUserKeyPress); return () => { window.removeEventListener("keydown", handleUserKeyPress); }; }, [handleUserKeyPress]);

return (

{userText}
); ```

在元件解除安裝的時候移除 event listener callback,因此需要保持 event handler 的引用,所以這裡需要使用 useCallback 來保持引用不變。

使用 useCallback,我們又會面臨宣告依賴項的問題,這裡我們可以使用 ahook 中的 useMemoizedFn 的方式,既能保持引用,又不用宣告依賴項。

javascript const [state, setState] = useState(''); // func 地址永遠不會變化 const func = useMemoizedFn(() => { console.log(state); });


useRef

useRef: 可以獲取當前元素的所有屬性,並且返回一個可變的ref物件,並且這個物件只有current屬性,可設定initialValue

  1. 通過useRef獲取對應的React元素的屬性值
  2. 快取資料

useImperativeHandle

useImperativeHandle:可以讓你在使用 ref自定義暴露給父元件的例項值

javascript useImperativeHandle(ref, createHandle, [deps]) - refuseRef所建立的ref - createHandle處理的函式,返回值作為暴露給父元件的 ref 物件。 - deps依賴項,依賴項更改形成新的 ref 物件。

useImperativeHandleforwardRef配合使用

jsx function FancyInput(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput); 在父元件中,可以渲染<FancyInput ref={inputRef} />並可以通過父元件的inputRef對子元件中的input進行處理。

  • inputRef.current.focus()

useLayoutEffect

useLayoutEffect: 與useEffect基本一致,不同的地方時,useLayoutEffect同步

要注意的是useLayoutEffectDOM 更新之後,瀏覽器繪製之前,這樣做的好處是可以更加方便的修改 DOM,獲取 DOM 資訊,這樣瀏覽器只會繪製一次,所以useLayoutEffect在useEffect之前執行

如果是 useEffect 的話 ,useEffect 執行在瀏覽器繪製檢視之後,如果在此時改變DOM,有可能會導致瀏覽器再次迴流和重繪

除此之外useLayoutEffectcallback 中程式碼執行會阻塞瀏覽器繪製


useDebugValue

useDebugValue:可用於在 React 開發者工具中顯示自定義 hook 的標籤


React v18中的hooks

useSyncExternalStore

useSyncExternalStore:是一個推薦用於讀取和訂閱外部資料來源hook,其方式與選擇性的 hydration 和時間切片等併發渲染功能相容

javascript const state = useSyncExternalStore( subscribe, getSnapshot[, getServerSnapshot] ) - subscribe: 訂閱函式,用於註冊一個回撥函式,當儲存值發生更改時被呼叫。此外, useSyncExternalStore 會通過帶有記憶性的 getSnapshot 來判別資料是否發生變化,如果發生變化,那麼會強制更新資料。 - getSnapshot: 返回當前儲存值的函式。必須返回快取的值。如果 getSnapshot 連續多次呼叫,則必須返回相同的確切值,除非中間有儲存值更新。 - getServerSnapshot:返回服務端(hydration模式下)渲染期間使用的儲存值的函式


useTransition

useTransition: - 返回一個狀態值表示過渡任務的等待狀態, - 以及一個啟動該過渡任務的函式

過渡任務 在一些場景中,如:輸入框tab切換按鈕等,這些任務需要檢視上立刻做出響應,這些任務可以稱之為立即更新的任務

但有的時候,更新任務並不是那麼緊急,或者來說要去請求資料等,導致新的狀態不能立馬更新,需要用一個loading...的等待狀態,這類任務就是過度任務

javascript const [isPending, startTransition] = useTransition(); - isPending過渡狀態的標誌,為true時是等待狀態 - startTransition:可以將裡面的任務變成過渡任務


useDeferredValue

useDeferredValue:接受一個值,並返回該值的新副本,該副本將推遲到更緊急地更新之後。

如果當前渲染是一個緊急更新的結果,比如使用者輸入,React返回之前的值,然後在緊急渲染完成後渲染新的值

也就是說useDeferredValue可以讓狀態滯後派生

javascript const deferredValue = useDeferredValue(value); - value:可變的值,如useState建立的值 - deferredValue: 延時狀態

useTransition和useDeferredValue做個對比 - 相同點:useDeferredValueuseTransition 一樣,都是過渡更新任務 - 不同點:useTransition 給的是一個狀態,而useDeferredValue給的是一個


useInsertionEffect

useInsertionEffect:與 useLayoutEffect 一樣,但它在所有 DOM 突變之前同步觸發

在執行順序上 useInsertionEffect > useLayoutEffect > useEffect

seInsertionEffect 應僅限於 css-in-js 庫作者使用。
優先考慮使用 useEffect 或 useLayoutEffect 來替代。


useId

useId : 是一個用於生成橫跨服務端和客戶端的穩定的唯一 ID 的同時避免hydration不匹配的 hook。


ref能否拿到函式元件的例項

使用forwordRef

input單獨封裝成一個元件TextInputjsx const TextInput = React.forwardRef((props,ref) => { return <input ref={ref}></input> })TextInputWithFocusButton呼叫它

jsx function TextInputWithFocusButton() { // 關鍵程式碼 const inputEl = useRef(null); const onButtonClick = () => { // 關鍵程式碼,`current` 指向已掛載到 DOM 上的文字輸入元素 inputEl.current.focus(); }; return ( <> // 關鍵程式碼 <TextInput ref={inputEl}></TextInput> <button onClick={onButtonClick}>Focus the input</button> ); }

useImperativeHandle

有時候,我們可能不想將整個子元件暴露給父元件,而只是暴露出父元件需要的值或者方法,這樣可以讓程式碼更加明確。而useImperativeHandle Api就是幫助我們做這件事的。

```jsx const TextInput = forwardRef((props,ref) => { const inputRef = useRef(); // 關鍵程式碼 useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return })

function TextInputWithFocusButton() { // 關鍵程式碼 const inputEl = useRef(null); const onButtonClick = () => { // 關鍵程式碼,current 指向已掛載到 DOM 上的文字輸入元素 inputEl.current.focus(); }; return ( <> // 關鍵程式碼
); }

`` 也可以使用current.focus()來做input`聚焦。

這裡要注意的是,子元件TextInput中的useRef物件,只是用來獲取input元素的,大家不要和父元件的useRef混淆了。

useCallbck vs useMemo的區別

useMemo

jsx const memoizedValue = useMemo( () => computeExpensiveValue(a, b), [a, b] );

useMemo:與memo的理念上差不多,都是判斷是否滿足當前的限定條件來決定是否執行callback函式,而useMemo的第二個引數是一個陣列,通過這個陣列來判定是否執行回撥函式

當一個父元件中呼叫了一個子元件的時候,父元件的 state 發生變化,會導致父元件更新,而子元件雖然沒有發生改變,但也會進行更新。

只要父元件的狀態更新,無論有沒有對子元件進行操作,子元件都會進行更新useMemo就是為了防止這點而出現的。


useCallback

useCallback 可以理解為 useMemo 的語法糖 diff const memoizedCallback = useCallback( + () => { doSomething(a, b); + }, [a, b], );

useCallbackuseMemo極其類似,唯一不同的是

  • useMemo返回的是函式執行的結果
  • useCallback返回的是函式
  • 這個函式是父元件傳遞子元件的一個函式,防止做無關的重新整理,
  • 其次,這個子元件必須配合React.memo,否則不但不會提升效能,還有可能降低效能

React.memo

memo:結合了 pureComponent 純元件和 componentShouldUpdate()功能,會對傳入的 props 進行一次對比,然後根據第二個函式返回值來進一步判斷哪些props需要更新

要注意 memo 是一個高階元件,函式式元件和類元件都可以使用。

memo 接收兩個引數:

``` function MyComponent(props) {

} function areEqual(prevProps, nextProps) {

} export default React.memo(MyComponent, areEqual); ```

  1. 第一個引數:元件本身,也就是要優化的元件
  2. 第二個引數:(pre, next) => boolean,
    • pre:之前的資料
    • next:現在的資料
    • 返回一個布林值
    • 為 true 則不更新
    • false 更新

memo的注意事項

React.memoPureComponent 的區別:

  • 服務物件不同
    • PureComponent 服務於類元件
    • React.memo既可以服務於類元件,也可以服務與函式式元件,
    • useMemo 服務於函式式元件
  • 針對的物件不同:
    • PureComponent 針對的是propsstate
    • React.memo只能針對props來決定是否渲染

React.memo 的第二個引數的返回值與shouldComponentUpdate的返回值是相反的 - React.memo:返回 true 元件不渲染 , 返回 false 元件重新渲染。 - shouldComponentUpdate: 返回 true 元件渲染 , 返回 false 元件不渲染


類元件和函式元件的區別

相同點

元件是 React 可複用的最小程式碼片段,它們會返回要在頁面中渲染 React 元素,也正是基於這一點,所以React 中無論是函式元件,還是類元件,其實它們最終呈現的效果都是一致的

不同點

設計思想

  1. 類元件的根基是 OOP(面向物件程式設計),所以它會有繼承,有內部狀態管理
  2. 函式元件的根基是 FP(函數語言程式設計)

未來的發展趨勢

React 團隊從 Facebook 的實際業務場景觸發,通過探索時間切片併發模式,以及考慮效能的進一步優化和元件間更合理的程式碼拆分後,認為 類元件的模式並不能很好地適應未來的趨勢,它們給出了以下3個原因:

  1. this 的模糊性
  2. 業務邏輯耦合在生命週期中
  3. React 的元件程式碼缺乏標準的拆分方式

componentWillUnmount在瀏覽器重新整理後,會執行嗎

不會

如果想實現,在重新整理頁面時進行資料處理。使用beforeunload事件。

還有一個navigator.sendBeacon()


React 元件優化

  1. 父元件重新整理,而不波及子元件
  2. 元件自己控制自己是否重新整理
  3. 減少波及範圍,無關重新整理資料不存入state
  4. 合併 state,減少重複 setState 的操作

父元件重新整理,而不波及子元件

  1. 子元件自己判斷是否需要更新 ,典型的就是
    • PureComponent
    • shouldComponentUpdate
    • React.memo
  2. 父元件對子元件做個緩衝判斷

使用PureComponent注意點

  1. 父元件是函式元件,子元件用PureComponent時,匿名函式箭頭函式普通函式都會重新宣告
    • 可以使用useMemo或者 useCallback,利用他們緩衝一份函式,保證不會出現重複宣告就可以了。
  2. 類元件中不使用箭頭函式,匿名函式
    • class 元件中每一次重新整理都會重複呼叫render函式,那麼render函式中使用的匿名函式,箭頭函式就會造成重複重新整理的問題
    • 處理方式- 換成普通函式
  3. class 元件的render函式中呼叫bind 函式
    • bind操作放在constructor

shouldComponentUpdate

class 元件中 使用 shouldComponentUpdate 是主要的優化方式,它不僅僅可以判斷來自父元件的nextprops,還可以根據nextState和最新的nextContext來決定是否更新。

React.memo

React.memo的規則是如果想要複用最後一次渲染結果,就返回true,不想複用就返回false。所以它和shouldComponentUpdate的正好相反,false才會更新,true就返回緩衝。

jsx const Children = React.memo(function ({count}){ return ( <div> 只有父元件傳入的值是偶數的時候才會更新 {count} </div> ) },(prevProps, nextProps)=>{ if(nextProps.count % 2 === 0){ return false; }else{ return true; } })


使用 React.useMemo來實現對子元件的緩衝

子元件只關心count資料,當我們重新整理name資料的時候,並不會觸發重新整理 Children子元件,實現了我們對元件的緩衝控制。 jsx export default function Father (){ let [count,setCount] = React.useState(0); let [name,setName] = React.useState(0); const render = React.useMemo( ()=> <Children count = {count}/> ,[count] ) return ( <div> <button onClick={()=>setCount(++count)}> 點選重新整理count </button> <br/> <button onClick={()=>setName(++name)}> 點選重新整理name </button> <br/> {"count"+count} <br/> {"name"+name} <br/> {render} </div> ) }

減少波及範圍,無關重新整理資料不存入state中

  1. 無意義重複呼叫setState合併相關的state
  2. 頁面重新整理無關的資料,不存入state
  3. 通過存入useRef的資料中,避免父子元件的重複重新整理
  4. 合併 state,減少重複 setState 的操作
    • ReactDOM.unstable_batchedUpdates;
    • 多個setState會合並執行一次。

React-Router實現原理

react-router-dom和react-router和history庫三者什麼關係

  1. history 可以理解為react-router的核心,也是整個路由原理的核心,裡面集成了popState,history.pushState底層路由實現的原理方法
  2. react-router可以理解為是react-router-dom的核心,裡面封裝了RouterRouteSwitch等核心元件,實現了從路由的改變到元件的更新的核心功能
  3. react-router-dom,在react-router的核心基礎上,添加了用於跳轉的Link元件,和histoy模式下的BrowserRouterhash模式下的HashRouter元件等。
    • 所謂BrowserRouterHashRouter,也只不過用了history庫中createBrowserHistorycreateHashHistory方法

單頁面實現核心原理

單頁面應用路由實現原理是,切換url,監聽url變化,從而渲染不同的頁面元件

主要的方式有history模式和hash模式。

history模式原理

  1. 改變路由
    • history.pushState(state,title,path)
  2. 監聽路由
    • window.addEventListener('popstate',function(e){ /* 監聽改變 */})

hash模式原理

  1. 改變路由
    • 通過window.location.hash 屬性獲取和設定 hash
  2. 監聽路由
    • window.addEventListener('hashchange',function(e){ /* 監聽改變 */})


XXR

根據不同的構建、渲染過程有不同的優劣勢和適用情況。 - 現代 UI 庫加持下常用的 CSR、 - 具有更好 SEO 效果的 SSR (SPR)、 - 轉換思路主打構建時生成SSG、 - 大架構視野之上的 ISRDPR, - 還有更少聽到的 NSRESR

CSR(Client Side Rendering)

頁面託管伺服器只需要對頁面的訪問請求響應一個如下的空頁面

```html


`` 頁面中留出一個<span style="font-weight:800;color:red;font-size:18px">用於填充渲染內容的檢視節點</span> (div#root),並插入指向專案**編譯壓縮後**的 -JS Bundle檔案的script節點 - 指向CSS檔案的link.stylesheet` 節點等。

瀏覽器接收到這樣的文件響應之後,會根據文件內的連結載入指令碼與樣式資源,並完成以下幾方面主要工作:

  1. 執行指令碼
  2. 進行網路訪問以獲取線上資料
  3. 使用 DOM API 更新頁面結構
  4. 繫結互動事件
  5. 注入樣式

以此完成整個渲染過程。

CSR 模式有以下幾方面優點:

  • UI 庫支援
  • 前後端分離
  • 伺服器負擔輕

SSR (Server Side Rendering)

SSR 的概念,即與 CSR 相對地,在服務端完成大部分渲染工作,--- 伺服器在響應站點訪問請求的時候,就已經渲染好可供呈現的頁面

ReactVue 這樣的 UI 生態巨頭,其實都有一個關鍵的 Virtual DOM (or VDOM) 概念,先自己建模處理視圖表現與更新、再批量調 DOM API 完成檢視渲染更新。這就帶來了一種 SSR 方案:

VDOM自建模型,是一種抽象的巢狀資料結構,也就可以在 Node 環境(或者說一切服務端環境)下跑起來,把原來的檢視程式碼拿來在服務端跑,通過 VDOM 維護,再在最後拼接好字串作為頁面響應,生成文件作為響應頁面,此時的頁面內容已經基本生成完畢,把邏輯程式碼、樣式程式碼附上,則可以實現完整的、可呈現頁面的響應。

SSR優點

  • 呈現速度和使用者體驗佳
  • SEO 友好

SSR缺點

  1. 引入成本高
    • 將檢視渲染的工作交給了伺服器做,引入了新的概念和技術棧(如 Node)
  2. 響應時間長
    • SSR 在完成訪問響應的時候需要做更多的計算和生成工作
    • 關鍵指標 TTFB (Time To First Byte) 將變得更大
  3. 首屏互動不佳
    • 雖然 SSR 可以讓頁面請求響應後更快在瀏覽器上渲染出來
    • 但在首幀出現,需要客戶端載入啟用的邏輯程式碼(如事件繫結)還沒有初始化完畢的時候,其實是不可互動的狀態

SSR-React 原理

  1. VDOM
  2. 同構
  3. 雙端對比

VDOM

同構

雙端對比

renderToString()

renderToStaticMarkup()

javascript ReactDOMServer.renderToStaticMarkup(element) 僅僅是為了將元件渲染為html字串,不會帶有data-react-checksum屬性


SPR (Serverless Pre-Rendering)

無服務預渲染,這是 Serverless 話題之下的一項渲染技術。SPR 是指在 SSR 架構下通過預渲染與快取能力,將部分頁面轉化為靜態頁面,以避免其在伺服器接收到請求的時候頻繁被渲染的能力,同時一些框架還支援設定靜態資源過期時間,以確保這部分“靜態頁面”也能有一定的即時性。


SSG (Static Site Generation)

  • 它與 CSR 一樣,只需要頁面託管,不需要真正編寫並部署服務端,頁面資源在編譯完成部署之前就已經確定
  • 但它又與 SSR 一樣,屬於一種 Prerender 預渲染操作,即在使用者瀏覽器得到頁面響應之前,頁面內容和結構就已經渲染好了。
  • 當然形式和特徵來看,它更接近 SSR。

SSG 模式,把原本日益動態化、互動性增強的頁面,變成了大部分已經填充好,託管在頁面服務 / CDN 上的靜態頁面


NSR (Native Side Rendering)

Native 就是客戶端,萬物皆可分散式,可以理解為這就是一種分散式的 SSR,不過這裡的渲染工作交給了客戶端去做而不是遠端伺服器。在使用者即將訪問頁面的上級頁面預取頁面資料,由客戶端快取 HTML 結構,以達到使用者真正訪問時快速響應的效果

NSR 見於各種移動端 + WebviewHybrid 場景,是需要頁面與客戶端研發協作的一種優化手段。


ESR (Edge Side Rendering)

Edge 就是邊緣,類比前面的各種 XSRESR 就是將渲染工作交給邊緣伺服器節點,常見的就是 CDN 的邊緣節點。這個方案主打的是邊緣節點相比核心伺服器與使用者的距離優勢,利用了 CDN 分級快取的概念,渲染和內容填充也可以是分級進行並快取下來的。

ESR 之下靜態內容與動態內容是分流的, 1. 邊緣 CDN 節點可以將靜態頁面內容先響應給使用者 2. 然後再自己發起動態內容請求,得到核心伺服器響應之後再返回給使用者

是在大型網路架構下非常極致的一種優化,但這也就依賴更龐大的技術基建體系了。


ISR (Incremental Site Rendering)

增量式網站渲染,就是對待頁面內容小刀切,有更細的差異化渲染粒度,能漸進、分層地進行渲染。

常見的選擇是: - 對於重要頁面如首屏、訪問量較大的直接落地頁,進行預渲染並新增快取,保證最佳的訪問效能; - 對於次要頁面,則確保有兜底內容可以即時 fallback,再將其實時資料的渲染留到 CSR 層次完成,同時觸發非同步快取更新。

對於“非同步快取更新”,則需要提到一個常見的內容快取策略:Stale While Revalidate,CDN 對於資料請求始終首先響應快取內容,如果這份內容已經過期,則在響應之後再觸發非同步更新——這也是對於次要元素或頁面的快取處理方式。


WebComponents

Web Components 是一套不同的技術,允許您建立可重用的定製元素並且在您的 web 應用中使用它們

三要素

  1. Custom elements(自定義元素): 一組 JavaScript API,允許您定義 custom elements 及其行為,然後可以在您的使用者介面中按照需要使用它們。
    • 通過 class A extends HTMLElement {} 定義元件,
    • 通過 window.customElements.define('a-b', A) 掛載已定義元件。
  2. Shadow DOM(影子 DOM ):一組 JavaScript API,用於將封裝的“影子” DOM 樹附加到元素(與主文件 DOM 分開呈現)並控制其關聯的功能。
    • 通過這種方式,您可以保持元素的功能私有,這樣它們就可以被指令碼化樣式化,而不用擔心與文件的其他部分發生衝突。
    • 使用 const shadow = this.attachShadow({mode : 'open'})WebComponents 中開啟。
  3. HTML templates(HTML 模板)slottemplate 可以簡化生成 dom 元素的操作,不再需要 createElement 每一個節點。

雖然 WebComponents 有三個要素,但卻不是缺一不可的,WebComponents

  • 藉助 shadow dom 來實現樣式隔離
  • 藉助 templates簡化標籤的操作。

內部生命週期函式(4個)

  1. connectedCallback: 當 WebComponents 第一次被掛在到 dom 上是觸發的鉤子,並且只會觸發一次。
    • 類似 React 中的 useEffect(() => {}, [])componentDidMount
  2. disconnectedCallback: 當自定義元素與文件 DOM 斷開連線時被呼叫。
  3. adoptedCallback: 當自定義元素被移動到新文件時被呼叫。
  4. attributeChangedCallback: 當自定義元素的被監聽屬性變化時被呼叫

元件通訊

傳入複雜資料型別

  • 傳入一個 JSON 字串配飾attribute

    • JSON.stringify配置指定屬性
    • 在元件attributeChangedCallback中判斷對應屬性,然後用JSON.parse()獲取
  • 配置DOM的property屬性

  • xx.dataSource = [{ name: 'xxx', age: 19 }]
  • 但是,自定義元件中沒有辦法監聽到這個屬性的變化
  • 如果想實現,複雜的結構,不是通過配置,而是在定義元件時候,就確定

狀態的雙向繫結

```

// js (function () { const template = document.createElement('template') template.innerHTML = `

class WlInput extends HTMLElement { constructor() { super() const shadow = this.attachShadow({ mode: 'closed' }) const content = template.content.cloneNode(true) this._input = content.querySelector('#wlInput') this._input.value = this.getAttribute('value') shadow.appendChild(content) this._input.addEventListener("input", ev => { const target = ev.target; const value = target.value; this.value = value; this.dispatchEvent( new CustomEvent("change", { detail: value }) ); }); } get value() { return this.getAttribute("value"); } set value(value) { this.setAttribute("value", value); } } window.customElements.define('wl-input', WlInput) })() ``` 監聽了這個表單的input事件,並且在每次觸發input事件的時候觸發自定義的change` 事件,並且把輸入的引數回傳。


樣式設定

直接給自定義標籤新增樣式

```html

```

定義元素內部子元素設定樣式

分為兩種場景: 1. 在主 DOM 使用 JS 2. 在 Custom Elements 建構函式中使用 JS

在主 DOM 使用 JS 給 Shadow DOM 增加 style 標籤:

```html

```

在 Custom Elements 建構函式中使用 JS 增加 style 標籤:

```html

```

引入 CSS 檔案

使用 JS 建立 link 標籤,然後引入 CSS 檔案給自定義元素內部的子元素設定樣式 ```html

``` 樣式檔案

css .input-header{ padding:10px; background-color: yellow; font-size: 16px; font-weight: bold; }


Lit

Lit 的核心是一個元件基類,它提供響應式scoped 樣式和一個小巧、快速且富有表現力的宣告性模板系統,且支援 TypeScript 型別宣告。

Lit 在開發過程中不需要編譯或構建,幾乎可以在無工具的情況下使用。

我們知道 HTMLElement 是瀏覽器內建的類,LitElement 基類則是 HTMLElement 的子類,因此 Lit 元件繼承了所有標準 HTMLElement 屬性和方法。更具體來說,LitElement 繼承自 ReactiveElement,後者實現了響應式屬性,而後者又繼承自 HTMLElement

LitElement 框架則是基於 HTMLElement 類二次封裝了 LitElement 類。

js export class LitButton extends LitElement { /* ... */ } customElements.define('lit-button', LitButton);

渲染

元件具有 render 方法,該方法被呼叫以渲染元件的內容。

```javascript export class LitButton extends LitElement { / ... /

render() { // 使用模板字串,可以包含表示式 return html<div><slot name="btnText"></slot></div>; } }

`` 元件的render()方法返回單個TemplateResult` 物件


響應式 properties

DOM 中 propertyattribute 的區別: - attributeHTML 標籤上的特性,可以理解為標籤屬性,它的值只能夠是 String 型別,並且會自動新增同名 DOM 屬性作為 property 的初始值; - propertyDOM 中的屬性,是 JavaScript 裡的物件,有同名 attribiute 標籤屬性的 property 屬性值的改變也並不會同步引起 attribute 標籤屬性值的改變

Lit 元件接收標籤屬性 attribute 並將其狀態儲存為 JavaScriptclass 欄位屬性或 properties響應式 properties 是可以在更改時觸發響應式更新週期、重新渲染元件以及可選地讀取或重新寫入 attribute 的屬性。每一個 properties 屬性都可以配置它的選項物件

傳入複雜資料型別

對於複雜資料的處理,為什麼會存在這個問題,根本原因還是因為 attribute 標籤屬性值只能是 String 型別,其他型別需要進行序列化。在 LitElement 中,只需要在父元件模板的屬性值前使用.操作符,這樣子元件內部 properties 就可以正確序列化為目標型別。

優點

LitElementWeb Components 開發方面有著很多比原生的優勢,它具有以下特點:

  1. 簡單:在 Web Components 標準之上構建,Lit 添加了響應式、宣告性模板和一些周到的功能,減少了模板檔案
  2. 快速:更新速度很快,因為 Lit 會跟蹤 UI 的動態部分,並且只在底層狀態發生變化時更新那些部分——無需重建整個虛擬樹並將其與 DOM 的當前狀態進行比較
  3. 輕便:Lit 的壓縮後大小約為 5 KB,有助於保持較小的包大小並縮短載入時間
  4. 高擴充套件性:lit-html 基於標記的 template,它結合了 ES6 中的模板字串語法,使得它無需預編譯、預處理,就能獲得瀏覽器原生支援,並且擴充套件能力強
  5. 相容良好:對瀏覽器相容性非常好,對主流瀏覽器都能有非常好的支援。

npm

巢狀的 node_modules 結構

npm 在早期採用的是巢狀的 node_modules 結構直接依賴會平鋪在 node_modules 下,子依賴巢狀在直接依賴的 node_modules 中。

比如專案依賴了A 和 C,而 A 和 C 依賴了不同版本的 [email protected][email protected]node_modules 結構如下:

node_modules ├── [email protected] │ └── node_modules │ └── [email protected] └── [email protected] └── node_modules └── [email protected] 如果 D 也依賴 [email protected],會生成如下的巢狀結構:

node_modules ├── [email protected] │ └── node_modules │ └── [email protected] ├── [email protected] │ └── node_modules │ └── [email protected] └── [email protected] └── node_modules └── [email protected] 可以看到同版本的 B 分別被 A 和 D 安裝了兩次

依賴地獄 Dependency Hell

在真實場景下,依賴增多,冗餘的包也變多,node_modules 最終會堪比黑洞,很快就能把磁碟佔滿。而且依賴巢狀的深度也會十分可怕,這個就是依賴地獄。

扁平的 node_modules 結構

為了將巢狀的依賴儘量打平,避免過深的依賴樹和包冗餘,npm v3子依賴提升(hoist),採用扁平的 node_modules 結構,子依賴會儘量平鋪安裝在主依賴項所在的目錄中

node_modules ├── [email protected] ├── [email protected] └── [email protected] └── node_modules └── [email protected] 可以看到 A 的子依賴的 [email protected] 不再放在 A 的 node_modules 下了,而是與 A 同層級。

C 依賴的 [email protected] 因為版本號原因還是巢狀在 C 的 node_modules 下。

這樣不會造成大量包的重複安裝,依賴的層級也不會太深,解決了依賴地獄問題,但也形成了新的問題。

幽靈依賴 Phantom dependencies

幽靈依賴是指package.json 中未定義的依賴,但專案中依然可以正確地被引用到

比如上方的示例其實我們只安裝了 A 和 C:

```json { "dependencies": { "A": "^1.0.0", "C": "^1.0.0" } }

`` 由於B在安裝時被提升到了和A` 同樣的層級,所以在專案中引用 B 還是能正常工作的

幽靈依賴是由依賴的宣告丟失造成的,如果某天某個版本的 A 依賴不再依賴 B 或者 B 的版本發生了變化,那麼就會造成依賴缺失或相容性問題。

不確定性 Non-Determinism

不確定性是指:同樣的 package.json 檔案,install 依賴後可能不會得到同樣的 node_modules 目錄結構。

如果有 package.json 變更,本地需要刪除 node_modules 重新 install,否則可能會導致生產環境與開發環境 node_modules 結構不同,程式碼無法正常執行。

依賴分身 Doppelgangers

假設繼續再安裝依賴 [email protected]D 模組和依賴 @B2.0E 模組,此時:

AD 依賴 [email protected] CE 依賴 [email protected] 以下是提升 [email protected]node_modules 結構:

node_modules ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] │ └── node_modules │ └── [email protected] └── [email protected] └── node_modules └── [email protected] 可以看到 [email protected] 會被安裝兩次,實際上無論提升 [email protected] 還是 [email protected],都會存在重複版本的 B 被安裝,這兩個重複安裝的 B 就叫 doppelgangers


yarn

yarn 也採用扁平化 node_modules 結構

提升安裝速度

npm 中安裝依賴時,安裝任務是序列的,會按包順序逐個執行安裝,這意味著它會等待一個包完全安裝,然後再繼續下一個。

為了加快包安裝速度,yarn 採用了並行操作,在效能上有顯著的提高。而且在快取機制上yarn將每個包快取在磁碟上,在下一次安裝這個包時,可以脫離網路實現從磁碟離線安裝。

lockfile 解決不確定性

yarn 更大的貢獻是發明了 yarn.lock

在依賴安裝時,會根據 package.josn 生成一份 yarn.lock 檔案。

lockfile 裡記錄了依賴,以及依賴的子依賴,依賴的版本,獲取地址與驗證模組完整性的 hash。

即使是不同的安裝順序,相同的依賴關係在任何的環境和容器中,都能得到穩定的 node_modules 目錄結構,保證了依賴安裝的確定性。

所以 yarn 在出現時被定義為快速、安全、可靠的依賴管理。而 npm 在一年後的 v5 才釋出了 package-lock.json

與 npm 一樣的弊端

yarn 依然和 npm 一樣是扁平化的 node_modules 結構,沒有解決幽靈依賴依賴分身問題。


pnpm

內容定址儲存 CAS

與依賴提升和扁平化的 node_modules 不同,pnpm 引入了另一套依賴管理策略:內容定址儲存

該策略會將包安裝在系統的全域性 store 中,依賴的每個版本只會在系統中安裝一次。

在引用專案 node_modules 的依賴時,會通過硬連結符號連結在全域性 store 中找到這個檔案。為了實現此過程,node_modules 下會多出 .pnpm 目錄,而且是非扁平化結構。

  • 硬連結 Hard link:硬連結可以理解為原始檔的副本,專案裡安裝的其實是副本,它使得使用者可以通過路徑引用查詢到全域性 store 中的原始檔,而且這個副本根本不佔任何空間。同時,pnpm 會在全域性 store 裡儲存硬連結,不同的專案可以從全域性 store 尋找到同一個依賴,大大地節省了磁碟空間。

  • 符號連結 Symbolic link:也叫軟連線,可以理解為快捷方式,pnpm 可以通過它找到對應磁碟目錄下的依賴地址。

由於連結的優勢,pnpm 的安裝速度在大多數場景都比 npmyarn 快 2 倍,節省的磁碟空間也更多。


yarn Plug’n’Play

Plug’n’Play(Plug'n'Play = Plug and Play = PnP,即插即用)。

拋棄 node_modules

無論是 npm 還是 yarn,都具備快取的功能,大多數情況下安裝依賴時,其實是將快取中的相關包複製到專案目錄中 node_modules 裡。

yarn PnP 則不會進行拷貝這一步,而是在專案裡維護一張靜態對映表 pnp.cjs


npm install 發生了啥


使用 history 模式的前端路由時靜態資源伺服器配置詳解

我們一般都是打包以後放在靜態資源伺服器中的,我們訪問諸如 example.com/rootpath/ 這種形式的資源沒問題,是因為,index.html 檔案是真實的存在於 rootpath 資料夾中的,可以找到的,返回給前端的。

但是如果訪問子路由 example.com/rootpath/login 進行登入操作,但是 login/index.html 檔案並非真實存在的檔案,其實我們需要的檔案還是 rootpath 目錄中的 index.html

再者,如果我們需要 js 檔案,比如登陸的時候請求的地址是 example.com/rootpath/login/js/dist.js 其實我們想要的檔案,還是 rootpath/js/ 目錄中的 dist.js 檔案而已。

前端路由其實是一種假象,只是用來矇蔽使用者而已的,無論用什麼路由,訪問的都是同一套靜態資源

之所以展示的內容不同,只是因為程式碼裡,根據不同的路由,對要顯示的檢視做了處理而已

比如 - 要找 example.com/rootpath/login 靜態資源伺服器找不到,那就返回 example.com/rootpath/ 內容; - 要找 example.com/rootpath/login/css/style.css 找不到,那就照著 example.com/rootpath/css/style.css 這個路徑去找。

總之就是,請求的是子目錄,找不到,那就返回根目錄一級對應的資原始檔就好了。

在 nginx 中使用

如果你打包以後的前端靜態資原始檔,想要仍在 nginx 中使用,那首先將你打包好的靜態資源目錄扔進 www 目錄,比如你打包好的資源的目錄叫 rootpath ,那麼直接將 rootpath 整個目錄丟進 www 目錄即可。

然後開啟我們的 nginx 配置檔案 nginx.conf,插入以下配置:

location /rootpath/ { root html; index index.html index.htm; try_files $uri $uri/ /rootpath/index.html; } 1. root 的作用 - 就是指定一個根目錄。預設的是html目錄 2. try_files - 關鍵點1:按指定的file順序查詢存在的檔案,並使用第一個找到的檔案進行請求處理 - 關鍵點2:查詢路徑是按照給定的rootalias為根路徑來查詢的 - 關鍵點3:如果給出的file都沒有匹配到,則重新請求最後一個引數給定的uri,就是新的location匹配


webpack 優化

時間方向(8個)

  1. 開發環境 - EvalSourceMapDevToolPlugin排除第三方模組
    • devtool:false
    • EvalSourceMapDevToolPlugin,通過傳入 module: truecolumn:false,達到和預設 eval-cheap-module-source-map 一樣的質量
  2. 縮小loader的搜尋範圍:test、include、exclude
  3. Module.noParse
    • noParse: /jquery|lodash/,
  4. TypeScript 編譯優化
  5. Resolve.modules指定查詢模組的目錄範圍
  6. Resolve.alias
  7. Resolve.extensions指定查詢模組的檔案類型範圍
  8. HappyPack

資源大小(9個)

  1. 按需引入類庫模組 (工具類庫)
    • 使用babel-plugin-import對其處理
  2. 使用externals優化cdn靜態資源
  3. CSS抽離+剔除無用樣式 -MiniCssExtractPlugin + PurgeCSS
  4. CSS壓縮 - CssMinimizerWebpackPlugin
  5. TreeSharking
    • CSS 方向 - glob-all purify-css purifycss-webpack
    • JS方向 - babel-loader版本問題
  6. Code Spilt - optimization - splitChunks - chunks:all
  7. 魔法註釋 - webpackChunkName:’xxx‘
  8. Scope Hoisting - optimization - concatenateModules:true
    • 普通打包只是將一個模組最終放入一個單獨的函式中,如果模組很多,就意味著在輸出結果中會有很多的模組函式。concatenateModules 配置的作用,儘可能將所有模組合併到一起輸出到一個函式中,既提升了執行效率,又減少了程式碼的體積。
  9. 圖片壓縮 - image-webpack-loader - 只要在 file-loader 之後加入 image-webpack-loader 即可

共同方案

  1. IgnorePlugin

Redux內部實現

createStore

```javascript function createStore( reducer, preloadedState, enhancer ){ let state;

// 用於存放被 subscribe 訂閱的函式(監聽函式) let listeners = [];

// getState 是一個很簡單的函式 const getState = () => state;

return { dispatch, getState, subscribe, replaceReducer } } ```

dispatch

javascript function dispatch(action) { // 通過 reducer 返回新的 state // 這個 reducer 就是 createStore 函式的第一個引數 state = reducer(state, action); // 每一次狀態更新後,都需要呼叫 listeners 陣列中的每一個監聽函式 listeners.forEach(listener => listener()); return action; // 返回 action }

subscribe

javascript function subscribe(listener){ listeners.push(listener); // 函式取消訂閱函式 return () => { listeners = listeners.filter(fn => fn !== listener); } }

combineReducers

function combineReducers(reducers){ return (state = {},action) => { // 返回的是一個物件,reducer 就是返回的物件 return Object.keys(reducers).reduce( (accum,currentKey) => { accum[currentKey] = reducers[currentKey](state[currentKey],action); return accum; },{} // accum 初始值是空物件 ); } }

applyMiddleware

```javascript function applyMiddleware(...middlewares){ return function(createStore){ return function(reducer,initialState){ var store = createStore(reducer,initialState); var dispatch = store.dispatch; var chain = [];

  var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
  };

  chain = middlewares.map(
      middleware => middleware(middlewareAPI)
      );

  dispatch = compose(...chain)(store.dispatch);
  return { ...store, dispatch };
}

} } ``applyMiddleware` 函式是一個三級柯里化函式


Vue和 React的區別

共同點

  1. 資料驅動檢視
  2. 元件化
  3. 都使用 Virtual DOM

不同點

  1. 核心思想
    • Vue靈活易用的漸進式框架,進行資料攔截/代理,它對偵測資料的變化更敏感、更精確
    • React推崇函數語言程式設計(純元件),資料不可變以及單向資料流
  2. 元件寫法差異
    • React推薦的做法是JSX + inline style, 也就是把 HTMLCSS 全都寫進 JavaScript 中,即 all in js;
    • Vue 推薦的做法是 template單檔案元件格式html,css,JS 寫在同一個檔案
  3. diff演算法不同
    • 兩者流程思路上是類似的:不同的元件產生不同的 DOM 結構。當type不相同時,對應DOM操作就是直接銷燬老的DOM,建立新的DOM同一層次的一組子節點,可以通過唯一的 key 區分
    • Vue-Diff演算法採用了雙端比較的演算法,同時從新舊children的兩端開始進行比較,藉助key值找到可複用的節點,再進行相關操作。相比ReactDiff演算法,同樣情況下可以減少移動節點次數,減少不必要的效能損耗,更加的優雅。
  4. 響應式原理不同
    • Vue 依賴收集,自動優化,資料可變, 當資料改變時,自動找到引用元件重新渲染
    • React基於狀態機,手動優化,資料不可變,需要setState驅動新的state替換老的state。 當資料改變時,以元件為根目錄,預設全部重新渲染。

Webpack有哪些常用的loader和plugin

Webpack Loader vs Plugin

  • loader檔案載入器,能夠載入資原始檔,並對這些檔案進行一些處理,諸如編譯、壓縮等,最終一起打包到指定的檔案中
  • plugin 賦予了 webpack 各種靈活的功能,例如打包優化、資源管理、環境變數注入等,目的是解決 loader 無法實現的其他事

- loader 執行在打包檔案之前 - plugins 在整個編譯週期都起作用

常用loader

  • 樣式:style-loadercss-loaderless-loadersass-loaderMiniCssExtractPlugin + PurgeCSS + CssMinimizerWebpackPlugin
  • js: bable-loader/ts-loader
  • 圖片:url-loaderlimit)、file-loaderimage-webpack-loader
  • 程式碼校驗:eslint-loader

常用plugin

  1. HtmlWebpackPlugin:會在打包結束之後自動建立一個index.html, 並將打包好的JS自動引入到這個檔案中
  2. MiniCssExtractPlugin
  3. IgnorePlugin:用於忽略第三方包指定目錄,讓指定目錄不被打包進去
  4. terser-webpack-plugin:壓縮js程式碼
  5. SplitChunksPluginCode-Splitting實現的底層就是通過Split-Chunks-Plugin實現的,其作用就是程式碼分割。

Babel

Babel 是一個 JavaScript 編譯器!

Babel 的作用就是將原始碼轉換為目的碼

Babel的作用

主要用於將採用 ECMAScript 2015+ 語法編寫的程式碼轉換為 es5 語法,讓開發者無視使用者瀏覽器的差異性,並且能夠用新的 JS 語法特性進行開發。除此之外,Babel 能夠轉換 JSX 語法,並且能夠支援 TypeScript 轉換為 JavaScript

總結一下:Babel 的作用如下 1. 語法轉換 2. 通過 Polyfill 方式在目標環境中新增缺失的特性 3. 原始碼轉換

原理

Babel 的執行原理可以通過以下這張圖來概括。整體來看,可以分為三個過程,分別是: 1. 解析, 1. 詞法解析 2. 語法解析 2. 轉換, 3. 生成。


Babel7 的使用

Babel 支援多種形式的配置檔案,根據使用場景不同可以選擇不同的配置檔案。 - 如果配置中需要書寫 js 邏輯,可以選擇babel.config.js或者 .babelrc.js; - 如果只是需要一個簡單的 key-value 配置,那麼可以選擇.babelrc,甚至可以直接在 package.json 中配置。

所有 Babel 的包都發布在 npm 上,並且名稱以 @babel 為字首(自從版本 7.0 之後),接下來,我們一起看下 @babel/core@babel/cli 這兩個 npm 包。

  • @babel/core - 核心庫,封裝了 Babel 的核心能力
  • @babel/cli - 命令列工具, 提供了 babel 這個命令

Babel 構建在外掛之上的。預設情況下,Babel 不做任何處理,需要藉助外掛來完成語法的解析,轉換,輸出

外掛的配置形式常見有兩種,分別是 1. 字串格式 2. 陣列格式,並且可以傳遞引數

如果外掛名稱為 @babel/plugin-XXX,可以使用簡寫成@babel/XXX, - 例如 @babel/plugin-transform-arrow-functions 便可以簡寫成 @babel/transform-arrow-functions

外掛的執行順序是從前往後

``` // .babelrc / * 以下三個外掛的執行順序是: @babel/proposal-class-properties -> @babel/syntax-dynamic-import -> @babel/plugin-transform-arrow-functions / { "plugins": [ // 同 "@babel/plugin-proposal-class-properties" "@babel/proposal-class-properties", // 同 ["@babel/plugin-syntax-dynamic-import"] ["@babel/syntax-dynamic-import"], [ "@babel/plugin-transform-arrow-functions", { "loose": true } ] ] }

```

預設

預設是一組外掛的集合

與外掛類似,預設的配置形式也是字串陣列兩種,預設也可以將 @babel/preset-XXX 簡寫為 @babel/XXX

預設的執行順序是從後往前,並且外掛在預設之前執行

我們常見的預設有以下幾種:

  • @babel/preset-env: 可以無視瀏覽器環境的差異而盡情地使用 ES6+ 新語法和新特性;
  • 注:語法和特性不是一回事,語法上的迭代是讓我們書寫程式碼更加簡單和方便,如展開運算子、類,結構等,因此這些語法稱為語法糖;特性上的迭代是為了擴充套件語言的能力,如 MapPromise 等,
  • 事實上,Babel 對新語法和新特性的處理也是不一樣的,對於新語法,Babel 通過外掛直接轉換,而對於新特性,Babel 還需要藉助 polyfill 來處理和轉換
  • @babe/preset-react: 可以書寫 JSX 語法,將 JSX 語法轉換為 JS 語法;
  • @babel/preset-typescript:可以使用 TypeScript 編寫程式,將 TS 轉換為 JS
    • 注:該預設只是將 TS 轉為 JS,不做任何型別檢查
  • @babel/preset-flow:可以使用 Flow 來控制型別,將 Flow 轉換為 JS

json // .babelrc /* * 預設的執行順序為: @babel/preset-react -> @babel/preset-typescript -> @babel/preset-env */ { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": { "version": 3, "proposals": true // 使用尚在提議階段特性的 polyfill } } ], "@babel/preset-typescript", // 同 @babel/preset-react "@babel/react" ] } 對於 @babel/preset-env ,我們通常需要設定目標瀏覽器環境,可以在根目錄下的 .browserslistrc 檔案中設定,也可以在該預設的引數選項中通過 targets(優先順序最高) 或者在 package.json 中通過 browserslist 設定。

如果我們不設定的話,該預設預設會將所有的 ES6+ 的新語法全部做轉換,否則,該預設只會對目標瀏覽器環境不相容的新語法做轉換

推薦設定目標瀏覽器環境,這樣在中大型專案中可以明顯縮小編譯後的程式碼體積,因為有些新語法的轉換需要引入一些額外定義的 helper 函式的,比如 class

.babelrc

{ "presets": [ [ "@babel/preset-env", { "targets": "> 0.25%, not dead" } ] ] }

.browserslistrc

```

0.25% not dead ```

對於新特性,@babel/preset-env 也是能轉換的。但是需要通過 useBuiltIns 這個引數選項實現,值需要設定為 usage,這樣的話,只會轉換我們使用到的新語法和新特性,能夠有效減小編譯後的包體積,並且還要設定 corejs: { version: 3, proposals } 選項,因為轉換新特性需要用到 polyfill,而 corejs 就是一個 polyfill 包。如果不顯示指定 corejs 的版本的話,預設使用的是 version 2 ,而 version 2 已經停更,諸如一些更新的特性的 polyfill 只會更行與 version 3 裡,如 Array.prototype.flat()

``` // .babelrc "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": { "version": 3, "proposals": true // 使用尚在提議階段特性的 polyfill } } ] ]

```

雖然 @babel/env 可以幫我們做新語法和新特性的按需轉換,但是依然存在 2 個問題:

  1. corejs 引入的 polyfill全域性範圍的,不是模組作用域返回的,可能存在汙染全域性變數的風險;
  2. 對於某些新語法,如 class,會在編譯後的檔案中注入很多 helper 函式宣告,而不是從某個地方 require 進來的函式引用,從而增大編譯後的包體積;

runtime

runtimebabel7 提出來的概念,旨在解決如上提出的效能問題的。

實踐一下 @babel/plugin-transform-runtime 外掛配合 @babel/preset-env 使用

bash npm install --save-dev @babel/plugin-transform-runtime // @babel/runtime 是要安裝到生產依賴的,因為新特性的編譯需要從這個包裡引用 polyfill // 它就是一個封裝了 corejs 的 polyfill 包 npm install --save @babel/runtime

// .babelrc { "presets": [ "@babel/env" ], "plugins": [ [ "@babel/plugin-transform-runtime",{ "corejs": 3 } ] ], } 編譯後,可以明顯看到, - 引入的 polyfill 不再是全域性範圍內的了,而是模組作用域範圍內的; - 並且不再是往編譯檔案中直接注入 helper 函數了,而是通過引用的方式,

既解決了全域性變數汙染的問題,又減小了編譯後包的體積


Fiber 實現時間切片的原理

React15 架構缺點

React16之前的版本比對更新虛擬DOM的過程是採用迴圈遞迴方式來實現的,這種比對方式有一個問題,就是一旦任務開始進行就無法中斷,如果應用中陣列數量龐大,主執行緒被長期佔用,直到整顆虛擬DOM樹比對更新完成之後主執行緒才被釋放,主執行緒才能執行其他任務,這就會導致一些使用者互動或動畫等任務無法立即得到執行,頁面就會產生卡頓,非常的影響使用者體驗

主要原因就是遞迴無法中斷,執行重的任務耗時較長,javascript又是單執行緒的,無法同時執行其他任務,導致任務延遲頁面卡頓使用者體驗差。


Fiber架構

介面通過 vdom 描述,但是不是直接手寫 vdom,而是 jsx 編譯產生的 render function 之後以後生成的。這樣就可以加上 stateprops 和一些動態邏輯,動態產生 vdom

vdom 生成之後不再是直接渲染,而是先轉成 fiber,這個 vdomfiber 的過程叫做 reconcile

fiber 是一個連結串列結構,可以打斷,這樣就可以通過 requestIdleCallback 來空閒排程 reconcile,這樣不斷的迴圈,直到處理完所有的 vdomfiberreconcile,就開始 commit,也就是更新到 dom

reconcile 的過程會提前建立好 dom,還會標記出增刪改,那麼 commit 階段就很快了。

從之前遞迴渲染時做 diff 來確定增刪改以及建立 dom,提前到了可打斷的 reconcile 階段,讓 commit 變得非常快,這就是 fiber 架構的目的和意義。

併發&排程(Concurrency & Scheduler)

  • Concurrency 併發: 有能力優先處理更高優事務,同時對正在執行的中途任務可暫存,待高優完成後,再去執行。
  • Scheduler 協調排程: 暫存未執行任務,等待時機成熟後,再去安排執行剩下未完成任務

考慮到可中斷渲染,並可重回構造。React自行實現了一套體系叫做 React fiber 架構。

React Fiber 核心: 自行實現 虛擬棧幀

schedule 就是通過空閒排程每個 fiber 節點的 reconcilevdomfiber),全部 reconcile 完了就執行 commit

Fiber的資料結構有三層資訊: (採用連結串列結構) 1. 例項屬性 - 該Fiber的基本資訊,例如元件型別等。 2. 構建屬性 - 構建屬性 (returnchildsibling) 3. 工作屬性 - 資料的變更會導致UI層的變更 - 為了減少對DOM的直接操作,通過Reconcile進行diff查詢,並將需要變更節點,打上標籤,變更路徑保留在effectList - 待變更內容要有Scheduler優先順序處理 - 涉及到diff等查詢操作,是需要有個高效手段來處理前後變化,即雙快取機制

連結串列結構即可支援隨時隨時中斷的訴求

Scheduler 執行核心點

  1. 有個任務佇列 queue,該佇列存放可中斷的任務
  2. workLoop對佇列裡取第一個任務currentTask,進入迴圈開始執行。
    • 當該任務沒有時間 或 需要中斷 (渲染任務 或 其他高優任務插入等),則讓出主執行緒。
  3. requestAnimationFrame 計算一幀的空餘時間;
  4. 使用new MessageChannel () 執行巨集任務;

devServer進行跨域處理

```javascript module.exports = { devServer: { / 執行程式碼的目錄 / contentBase: resolve(__dirname, "dist"), / 監視 contentBase 目錄下的所有檔案,一旦檔案發生變化就會 reload (過載+重新整理瀏覽器)/ watchContentBase: true, / 監視檔案時 配合 watchContentBase / watchOptions: { / 忽略掉的檔案(不參與監視的檔案) / ignored: /node_modules/ }, / 啟動gzip壓縮 / compress: true, / 執行服務時自動開啟伺服器 / open: true, / 啟動HMR熱更新 / hot: true, / 啟動的埠號 / port: 5000, / 啟動的IP地址或域名 / host: "localhost", / 關閉伺服器啟動日誌 / clientLogLevel: "none", / 除了一些啟動的基本資訊,其他內容都不要列印 / quiet: true, / 如果出錯不要全屏提示 / overlay: false, / 伺服器代理 --> 解決開發環境跨域問題 / proxy: { / 一旦devServer(port:5000)伺服器接收到 ^/api/xxx 的請求,就會把請求轉發到另外一個伺服器(target)上 / "/api": { target: "http://localhost:3000", / 路徑重寫(代理時傳送到target的請求去掉/api字首) / pathRewrite: { "^/api": "" } } } }, }

```


React 實現原理

React-Hook為什麼不能放到條件語句中

每一次渲染都是完全獨立的

每次渲染具有獨立的狀態值(每次渲染都是完全獨立的)。也就是說,每個函式中的 state 變數只是一個簡單的常量,每次渲染時從鉤子中獲取到的常量,並沒有附著資料繫結之類的神奇魔法。

這也就是老生常談的 Capture Value 特性。可以看下面這段經典的計數器程式碼

```jsx function Counter() { const [count, setCount] = useState(0);

function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); }

return (

You clicked {count} times

); } `` 按如下步驟操作: - 1)點選Click me按鈕,把數字增加到 3; - 2)點選Show alert按鈕; - 3)在setTimeout觸發之前點選Click me`,把數字增加到 5。

結果是 Alert 顯示 3!

來簡單解釋一下:

  • 每次渲染相互獨立,因此每次渲染時元件中的狀態、事件處理函式等等都是獨立的,或者說只屬於所在的那一次渲染
  • 我們在 count 為 3 的時候觸發了 handleAlertClick 函式,這個函式所記住的 count 也為 3
  • 三秒種後,剛才函式的 setTimeout 結束,輸出當時記住的結果:3

深入useState本質

當元件初次渲染(掛載)時

1. 在初次渲染時,我們通過 useState 定義了多個狀態; 2. 每呼叫一次 useState ,都會在元件之外生成一條 Hook 記錄,同時包括狀態值(用 useState 給定的初始值初始化)和修改狀態的 Setter 函式; 3. 多次呼叫 useState 生成的 Hook 記錄形成了一條連結串列; 4. 觸發 onClick 回撥函式,呼叫 setS2 函式修改 s2 的狀態,不僅修改了 Hook 記錄中的狀態值,還即將觸發重渲染

元件重渲染時

初次渲染結束之後、重渲染之前Hook 記錄連結串列依然存在。當我們逐個呼叫 useState 的時候,useState 便返回了 Hook 連結串列中儲存的狀態,以及修改狀態的 Setter


深入useEffect本質

注意其中一些細節:

  • useStateuseEffect 在每次呼叫時都被新增到 Hook 連結串列中;
  • useEffect 還會額外地在一個佇列中新增一個等待執行的 Effect 函式;
  • 渲染完成後,依次呼叫 Effect 佇列中的每一個 Effect 函式。

React 官方文件 Rules of Hooks 中強調過一點:

Only call hooks at the top level. 只在最頂層使用 Hook。

具體地說,不要在迴圈、巢狀、條件語句中使用 Hook——

因為這些動態的語句很有可能會導致每次執行元件函式時呼叫 Hook 的順序不能完全一致,導致 Hook 連結串列記錄的資料失效


自定義Hook實現原理

元件初次渲染

App 元件中呼叫了 useCustomHook 鉤子。可以看到,即便我們切換到了自定義 Hook 中,Hook 連結串列的生成依舊沒有改變

元件重新渲染

即便程式碼的執行進入到自定義 Hook 中,依然可以從 Hook 連結串列中讀取到相應的資料,這個”配對“的過程總能成功

Rules of Hook。它規定只有在兩個地方能夠使用 React Hook:

  1. React 函式元件
  2. 自定義 Hook

第一點毋庸置疑,第二點通過剛才的兩個動畫你也可以輕鬆的得出一個結論:

自定義 Hook 本質上只是把呼叫內建 Hook 的過程封裝成一個個可以複用的函式,並不影響 Hook 連結串列的生成和讀取


useCallback

依賴陣列在判斷元素是否發生改變時使用了 Object.is 進行比較,因此當 deps 中某一元素為非原始型別時(例如函式、物件等),每次渲染都會發生改變,從而每次都會觸發 Effect,失去了 deps 本身的意義。

Effect 無限迴圈

來看一下這段”永不停止“的計數器:

```jsx function EndlessCounter() { const [count, setCount] = useState(0);

useEffect(() => { setTimeout(() => setCount(count + 1), 1000); });

return (

{count}

); } ``` 如果你去執行這段程式碼,會發現數字永遠在增長。我們來通過一段動畫來演示一下這個”無限迴圈“到底是怎麼回事: 元件陷入了:渲染 => 觸發 Effect => 修改狀態 => 觸發重渲染的無限迴圈

關於記憶化快取(Memoization)

Memoization,一般稱為記憶化快取(或者“記憶”),它背後的思想很簡單:假如我們有一個計算量很大的純函式(給定相同的輸入,一定會得到相同的輸出),那麼我們在第一次遇到特定輸入的時候,把它的輸出結果“記”(快取)下來,那麼下次碰到同樣的輸出,只需要從快取裡面拿出來直接返回就可以了,省去了計算的過程!

記憶化快取(Memoization)的兩個使用場景:

  1. 通過快取計算結果,節省費時的計算
  2. 保證相同輸入下返回值的引用相等

useCallback使用方法和原理解析

為了解決函式在多次渲染中的引用相等(Referential Equality)問題,React 引入了一個重要的 Hook—— useCallback。官方文件介紹的使用方法如下:

jsx const memoizedCallback = useCallback(callback, deps); 第一個引數 callback 就是需要記憶的函式,第二個引數是deps 引數,同樣也是一個依賴陣列。在 Memoization 的上下文中,這個 deps 的作用相當於快取中的鍵(Key),如果鍵沒有改變,那麼就直接返回快取中的函式,並且確保是引用相同的函式

元件初次渲染(deps 為空陣列的情況)

呼叫 useCallback 也是追加到 Hook 連結串列上,不過這裡著重強調了這個函式 f1 所指向的記憶體位置,從而明確告訴我們:這個 f1 始終是指向同一個函式。然後返回的 onClick 則是指向 Hook 中儲存的 f1

元件重新渲染

重渲染的時候,再次呼叫 useCallback 同樣返回給我們 f1 函式,並且這個函式還是指向同一塊記憶體,從而使得 onClick 函式和上次渲染時真正做到了引用相等


useCallback 和 useMemo 的關係

之前我們說Memoization 的兩大場景 1. 通過快取計算結果,節省費時的計算 2. 保證相同輸入下返回值的引用相等

useCallbackuesMemoMemoization角度來說 - useCallback 主要是為了解決函式的”引用相等“問題, - useMemo 則是一個”全能型選手“,能夠同時勝任引用相等和節約計算的任務。

實際上,useMemo 的功能是 useCallback 的超集。

useCallback 只能快取函式相比,useMemo 可以快取任何型別的值(當然也包括函式)。useMemo 的使用方法如下: jsx const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b] ); 其中第一個引數是一個函式,這個函式返回值的返回值(也就是上面 computeExpensiveValue 的結果)將返回給 memoizedValue

因此以下兩個鉤子的使用是完全等價的:

jsx useCallback(fn, deps); useMemo(() => fn, deps);


useReducer

使用 useState 的時候遇到過一個問題:通過 Setter 修改狀態的時候,怎麼讀取上一個狀態值,並在此基礎上修改呢?如果你看文件足夠細緻,應該會注意到 useState 有一個{函式式更新|Functional Update}的用法。

jsx function Counter({initialCount}) { const [count, setCount] = useState(initialCount); return ( <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> ); } 傳入 setCount 的是一個函式,它的引數是之前的狀態,返回的是新的狀態。熟悉 Redux 的朋友馬上就指出來了:這其實就是一個 Reducer 函式。

useState底層實現原理

React 的原始碼中,useState 的實現使用了 useReducer。在 React 原始碼中有這麼一個關鍵的函式 basicStateReducer

javascript function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; } 於是,當我們通過 setCount(prevCount => prevCount + 1) 改變狀態時,傳入的 action 就是一個 Reducer 函式,然後呼叫該函式並傳入當前的 state,得到更新後的狀態。而我們之前通過傳入具體的值修改狀態時(例如 setCount(5)),由於不是函式,所以直接取傳入的值作為更新後的狀態

傳入的 action 是一個具體的值 (setCount(xx))

當傳入 Setter 的是一個 Reducer 函式的時候:(setCount(c =>c+1))


後記

分享是一種態度

全文完,既然看到這裡了,如果覺得不錯,隨手點個贊和“在看”吧。