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))


後記

分享是一種態度

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