2023面試真題之框架篇
highlight: a11y-dark theme: Chinese-red
閱讀使人充實,會談使人敏捷,寫作使人精確
大家好,我是柒八九。
今天,我們繼續2023前端面試真題系列。我們來談談關於前端框架的相關知識點。
如果,想了解該系列的文章,可以參考我們已經發布的文章。如下是往期文章。
文章list
你能所學到的知識點
- React Diff 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- setState同步非同步問題 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- React 18新特性 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- React 生命週期 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- Hook的相關知識點 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- ref能否拿到函式元件的例項 推薦閱讀指數⭐️⭐️⭐️
- useCallbck vs useMemo的區別 推薦閱讀指數⭐️⭐️⭐️
- React.memo 推薦閱讀指數⭐️⭐️⭐️⭐️
- 類元件和函式元件的區別 推薦閱讀指數⭐️⭐️⭐️⭐️
- componentWillUnmount在瀏覽器重新整理後,會執行嗎 推薦閱讀指數⭐️⭐️⭐️
- React 元件優化 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- React-Router實現原理 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- XXR 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- WebComponents 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- Lit 推薦閱讀指數⭐️⭐️⭐️⭐️
- npm 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- yarn 推薦閱讀指數⭐️⭐️⭐️⭐️
- pnpm 推薦閱讀指數⭐️⭐️⭐️⭐️
- yarn PnP 推薦閱讀指數⭐️⭐️⭐️⭐️
- npm install 發生了啥 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- 使用 history 模式的前端路由時靜態資源伺服器配置詳解 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- webpack 優化 推薦閱讀指數⭐️⭐️⭐️⭐️
- Redux內部實現 推薦閱讀指數⭐️⭐️⭐️⭐️ 24.Vue和 React的區別 推薦閱讀指數⭐️⭐️⭐️⭐️
- Webpack有哪些常用的loader和plugin 推薦閱讀指數⭐️⭐️⭐️⭐️
- Babel 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- Fiber 實現時間切片的原理 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
- devServer進行跨域處理 推薦閱讀指數⭐️⭐️⭐️
- React-Hook 實現原理 推薦閱讀指數⭐️⭐️⭐️⭐️⭐️
好了,天不早了,乾點正事哇。
React Diff
在React
中,diff演算法
需要與虛擬DOM
配合才能發揮出真正的威力。React
會使用diff
演算法計算出虛擬DOM
中真正發生變化的部分,並且只會針對該部分進行dom
操作,從而避免了對頁面進行大面積的更新渲染,減小效能的開銷。
React diff演算法
在傳統的diff演算法
中複雜度會達到O(n^3)
。React
中定義了三種策略,在對比時,根據策略只需遍歷一次樹就可以完成對比,將複雜度降到了O(n)
:
-
tree diff:在兩個樹對比時,只會比較同一層級的節點,會忽略掉跨層級的操作
-
component diff:在對比兩個元件時,首先會判斷它們兩個的型別是否相同
- 如果不是,則將該元件判斷為
dirty component
,從而替換整個元件下的所有子節點
- 如果不是,則將該元件判斷為
-
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
,setTimeout
,setState
就是同步更新的
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 Update
(isBatchingUpdates是否為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新特性
React
從 v16
到 v18
主打的特性包括三個變化:
- v16:
Async Mode
(非同步模式) - v17:
Concurrent Mode
(併發模式) - v18:
Concurrent Render
(併發更新)
React
中 Fiber
樹的更新流程分為兩個階段 render
階段和 commit
階段。
1. 元件的 render
函式執行時稱為 render
(本次更新需要做哪些變更),純 js 計算;
2. 而將 render
的結果渲染到頁面的過程稱為 commit
(變更到真實的宿主環境中,在瀏覽器中就是操作DOM
)。
在 Sync
模式下,render
階段是一次性執行完成;而在 Concurrent
模式下 render
階段可以被拆解,每個時間片內執行一部分,直到執行完畢。由於 commit
階段有 DOM
的更新,不可能讓 DOM
更新到一半中斷,必須一次性執行完畢。
React 併發新特性
併發渲染機制
concurrent rendering
的目的:根據使用者的裝置效能和網速對渲染過程進行適當的調整, 保證React
應用在長時間的渲染過程中依舊保持可互動性,避免頁面出現卡頓或無響應的情況,從而提升使用者體驗。
- 新 root API
- 通過
createRoot
Api 手動建立root
節點。
- 通過
- 自動批處理優化 Automatic batching
React
將多個狀態更新分組到一個重新渲染中以獲得更好的效能。(將多次setstate
事件合併)- 在
v18
之前只在事件處理函式中實現了批處理,在v18
中所有更新都將自動批處理,包括promise鏈
、setTimeout
等非同步程式碼以及原生事件處理函式
。 - 想退出自動批處理立即更新的話,可以使用
ReactDOM.flushSync()
進行包裹
startTransition
- 可以用來降低渲染優先順序。分別用來包裹計算量大的
function
和value
,降低優先順序,減少重複渲染次數。 startTransition
可以指定 UI 的渲染優先順序,哪些需要實時更新,哪些需要延遲更新- hook 版本的
useTransition
,接受傳入一個毫秒的引數用來修改最遲更新時間,返回一個過渡期的pending
狀態和startTransition
函式。
- 可以用來降低渲染優先順序。分別用來包裹計算量大的
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個)
componentWillMount
:在元件掛載到DOM
前呼叫- 這裡面的呼叫的
this.setState
不會引起元件的重新渲染,也可以把寫在這邊的內容提到constructor()
,所以在專案中很少。 - 只會呼叫一次
- 這裡面的呼叫的
render
: 渲染- 只要
props
和state
發生改變(無論值是否有變化,兩者的重傳遞和重賦值,都可以引起元件重新render
),都會重新渲染render
。 return
:是必須的,是一個React元素,不負責元件實際渲染工作,由React
自身根據此元素去渲染出DOM
。render
是純函式,不能執行this.setState
。
- 只要
componentDidMount
:元件掛載到DOM
後呼叫- 呼叫一次
Update(更新)(5個)
componentWillReceiveProps(nextProps)
:調用於props
引起的元件更新過程中nextProps
:父元件傳給當前元件新的props
- 可以用
nextProps
和this.props
來查明重傳props
是否發生改變(原因:不能保證父元件重傳的props
有變化) - 只要
props
發生變化就會,引起呼叫
shouldComponentUpdate(nextProps, nextState)
:用於效能優化nextProps
:當前元件的this.props
nextState
:當前元件的this.state
- 通過比較
nextProps
和nextState
,來判斷當前元件是否有必要繼續執行更新過程。 - 返回
false
:表示停止更新,用於減少元件的不必要渲染,優化效能 - 返回
true
:繼續執行更新 - 像
componentWillReceiveProps()
中執行了this.setState
,更新了state
,但在render
前(如shouldComponentUpdate
,componentWillUpdate
),this.state
依然指向更新前的state,不然nextState
及當前元件的this.state
的對比就一直是true
了
componentWillUpdate(nextProps, nextState)
:元件更新前呼叫- 在
render
方法前執行 - 由於元件更新就會呼叫,所以一般很少使用
- 在
-
render
:重新渲染 -
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-hooks
是React 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
攔截 getter
和 setter
的能力,讓我們可以很方便的通過修改物件本身,建立新的物件。
React
通過 Object.is
函式比較 props
,也就是說對於引用一致的物件,react是不會重新整理檢視的,這也是為什麼我們不能直接修改呼叫 useState
得到的 state 來更新檢視,而是要通過 setState
重新整理檢視,通常,為了方便,我們會使用 es6
的 spread
運算子構造新的物件(淺拷貝)。
對於巢狀層級多的物件,使用
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
都屬於副作用操作
- 不斷執行
- 當
useEffect
不設立第二個引數時,無論什麼情況,都會執行
- 當
- 根據依賴值改變
- 設定
useEffect
的第二個值
- 設定
useContext
useContext
:上下文,類似於Context
:其本意就是設定全域性共享資料,使所有元件可跨層級實現資料共享
useContent
的引數一般是由createContext
的建立,通過 xxContext.Provider
包裹的元件,才能通過 useContext
獲取對應的值
存在的問題及解決方案
useContext
是 React
官方推薦的共享狀態的方式,然而在需要共享狀態的元件非常多的情況下,這有著嚴重的效能問題,例如有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
:可以理解為和useState
的setState
一樣的效果
- reducer
:可以理解為redux
的reducer
- initialArg
:初始值
- init
:惰性初始化
useMemo
useMemo
:與memo
的理念上差不多,都是判斷是否滿足當前的限定條件來決定是否執行callback
函式,而useMemo
的第二個引數是一個陣列,通過這個陣列來判定是否執行回撥函式
當一個父元件中呼叫了一個子元件的時候,父元件的
state
發生變化,會導致父元件更新,而子元件雖然沒有發生改變,但也會進行更新。
只要父元件的狀態更新,無論有沒有對子元件進行操作,子元件都會進行更新,useMemo
就是為了防止這點而出現的。
useCallback
useCallback
與useMemo
極其類似,唯一不同的是
- 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 (
在元件解除安裝的時候移除
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
- 通過
useRef
獲取對應的React元素
的屬性值 - 快取資料
useImperativeHandle
useImperativeHandle
:可以讓你在使用 ref
時自定義暴露給父元件的例項值
javascript
useImperativeHandle(ref, createHandle, [deps])
- ref
:useRef
所建立的ref
- createHandle
:處理的函式,返回值作為暴露給父元件的 ref
物件。
- deps
:依賴項,依賴項更改形成新的 ref
物件。
useImperativeHandle
和forwardRef
配合使用
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
是同步
要注意的是useLayoutEffect
在 DOM 更新之後,瀏覽器繪製之前,這樣做的好處是可以更加方便的修改 DOM,獲取 DOM 資訊,這樣瀏覽器只會繪製一次,所以useLayoutEffect在useEffect之前執行
如果是 useEffect
的話 ,useEffect
執行在瀏覽器繪製檢視之後,如果在此時改變DOM,有可能會導致瀏覽器再次迴流和重繪。
除此之外useLayoutEffect
的 callback
中程式碼執行會阻塞瀏覽器繪製
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做個對比 - 相同點:
useDeferredValue
和useTransition
一樣,都是過渡更新任務 - 不同點:useTransition
給的是一個狀態,而useDeferredValue
給的是一個值
useInsertionEffect
useInsertionEffect
:與 useLayoutEffect
一樣,但它在所有 DOM 突變之前同步觸發
在執行順序上 useInsertionEffect
> useLayoutEffect
> useEffect
seInsertionEffect
應僅限於css-in-js
庫作者使用。
優先考慮使用useEffect
或useLayoutEffect
來替代。
useId
useId
: 是一個用於生成橫跨服務端和客戶端的穩定的唯一 ID 的同時避免hydration
不匹配的 hook。
ref能否拿到函式元件的例項
使用forwordRef
將input
單獨封裝成一個元件TextInput
。
jsx
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], );
useCallback
與useMemo
極其類似,唯一不同的是
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); ```
- 第一個引數:元件本身,也就是要優化的元件
- 第二個引數:
(pre, next) => boolean
,pre
:之前的資料next
:現在的資料- 返回一個布林值
- 若為 true 則不更新
- 為
false
更新
memo的注意事項
React.memo
與 PureComponent
的區別:
- 服務物件不同:
PureComponent
服務於類元件,React.memo
既可以服務於類元件,也可以服務與函式式元件,useMemo
服務於函式式元件
- 針對的物件不同:
PureComponent
針對的是props
和state
React.memo
只能針對props
來決定是否渲染
React.memo
的第二個引數的返回值與shouldComponentUpdate
的返回值是相反的 -React.memo
:返回true
元件不渲染 , 返回false
元件重新渲染。 -shouldComponentUpdate
: 返回true
元件渲染 , 返回false
元件不渲染
類元件和函式元件的區別
相同點
元件是 React
可複用的最小程式碼片段,它們會返回要在頁面中渲染 React
元素,也正是基於這一點,所以在 React
中無論是函式元件,還是類元件,其實它們最終呈現的效果都是一致的。
不同點
設計思想
- 類元件的根基是
OOP
(面向物件程式設計),所以它會有繼承,有內部狀態管理等 - 函式元件的根基是
FP
(函數語言程式設計)
未來的發展趨勢
React
團隊從 Facebook
的實際業務場景觸發,通過探索時間切片和併發模式,以及考慮效能的進一步優化和元件間更合理的程式碼拆分後,認為 類元件的模式並不能很好地適應未來的趨勢,它們給出了以下3個原因:
this
的模糊性- 業務邏輯耦合在生命週期中
React
的元件程式碼缺乏標準的拆分方式
componentWillUnmount在瀏覽器重新整理後,會執行嗎
不會。
如果想實現,在重新整理頁面時進行資料處理。使用beforeunload
事件。
還有一個navigator.sendBeacon()
React 元件優化
- 父元件重新整理,而不波及子元件
- 元件自己控制自己是否重新整理
- 減少波及範圍,無關重新整理資料不存入
state
中- 合併
state
,減少重複setState
的操作
父元件重新整理,而不波及子元件
- 子元件自己判斷是否需要更新 ,典型的就是
PureComponent
,shouldComponentUpdate
,React.memo
- 父元件對子元件做個緩衝判斷
使用PureComponent注意點
- 父元件是函式元件,子元件用
PureComponent
時,匿名函式,箭頭函式和普通函式都會重新宣告- 可以使用
useMemo
或者useCallback
,利用他們緩衝一份函式,保證不會出現重複宣告就可以了。
- 可以使用
- 類元件中不使用箭頭函式,匿名函式
class
元件中每一次重新整理都會重複呼叫render
函式,那麼render
函式中使用的匿名函式,箭頭函式就會造成重複重新整理的問題- 處理方式- 換成普通函式
- 在
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中
- 無意義重複呼叫
setState
,合併相關的state
- 和頁面重新整理無關的資料,不存入
state
中 - 通過存入
useRef
的資料中,避免父子元件的重複重新整理 - 合併
state
,減少重複setState
的操作ReactDOM.unstable_batchedUpdates
;- 多個
setState
會合並執行一次。
React-Router實現原理
react-router-dom和react-router和history庫三者什麼關係
history
可以理解為react-router
的核心,也是整個路由原理的核心,裡面集成了popState
,history.pushState
等底層路由實現的原理方法react-router
可以理解為是react-router-dom
的核心,裡面封裝了Router
,Route
,Switch
等核心元件,實現了從路由的改變到元件的更新的核心功能react-router-dom
,在react-router
的核心基礎上,添加了用於跳轉的Link
元件,和histoy
模式下的BrowserRouter
和hash
模式下的HashRouter
元件等。- 所謂
BrowserRouter
和HashRouter
,也只不過用了history
庫中createBrowserHistory
和createHashHistory
方法
- 所謂
單頁面實現核心原理
單頁面應用路由實現原理是,切換
url
,監聽url
變化,從而渲染不同的頁面元件。
主要的方式有history
模式和hash
模式。
history模式原理
- 改變路由
history.pushState(state,title,path)
- 監聽路由
window.addEventListener('popstate',function(e){ /* 監聽改變 */})
hash模式原理
- 改變路由
- 通過
window.location.hash
屬性獲取和設定hash
值
- 通過
- 監聽路由
window.addEventListener('hashchange',function(e){ /* 監聽改變 */})
XXR
根據不同的構建、渲染過程有不同的優劣勢和適用情況。
- 現代 UI 庫加持下常用的 CSR
、
- 具有更好 SEO
效果的 SSR
(SPR
)、
- 轉換思路主打構建時生成的 SSG
、
- 大架構視野之上的 ISR
、DPR
,
- 還有更少聽到的 NSR
、ESR
。
CSR(Client Side Rendering)
頁面託管伺服器只需要對頁面的訪問請求響應一個如下的空頁面
```html
``
頁面中留出一個<span style="font-weight:800;color:red;font-size:18px">用於填充渲染內容的檢視節點</span> (
div#root),並插入指向專案**編譯壓縮後**的
-
JS Bundle檔案的
script節點
- 指向
CSS檔案的
link.stylesheet` 節點等。
瀏覽器接收到這樣的文件響應之後,會根據文件內的連結載入指令碼與樣式資源,並完成以下幾方面主要工作:
- 執行指令碼
- 進行網路訪問以獲取線上資料
- 使用 DOM API 更新頁面結構
- 繫結互動事件
- 注入樣式
以此完成整個渲染過程。
CSR 模式有以下幾方面優點:
- UI 庫支援
- 前後端分離
- 伺服器負擔輕
SSR (Server Side Rendering)
SSR 的概念,即與 CSR
相對地,在服務端完成大部分渲染工作,--- 伺服器在響應站點訪問請求的時候,就已經渲染好可供呈現的頁面。
像 React
、Vue
這樣的 UI 生態巨頭,其實都有一個關鍵的 Virtual DOM
(or VDOM) 概念,先自己建模處理視圖表現與更新、再批量調 DOM API
完成檢視渲染更新。這就帶來了一種 SSR
方案:
VDOM
是自建模型,是一種抽象的巢狀資料結構,也就可以在 Node
環境(或者說一切服務端環境)下跑起來,把原來的檢視程式碼拿來在服務端跑,通過 VDOM
維護,再在最後拼接好字串作為頁面響應,生成文件作為響應頁面,此時的頁面內容已經基本生成完畢,把邏輯程式碼、樣式程式碼附上,則可以實現完整的、可呈現頁面的響應。
SSR優點
- 呈現速度和使用者體驗佳
SEO
友好
SSR缺點
- 引入成本高
- 將檢視渲染的工作交給了伺服器做,引入了新的概念和技術棧(如 Node)
- 響應時間長
- SSR 在完成訪問響應的時候需要做更多的計算和生成工作
- 關鍵指標
TTFB
(Time To First Byte
) 將變得更大
- 首屏互動不佳
- 雖然 SSR 可以讓頁面請求響應後更快在瀏覽器上渲染出來
- 但在首幀出現,需要客戶端載入啟用的邏輯程式碼(如事件繫結)還沒有初始化完畢的時候,其實是不可互動的狀態
SSR-React 原理
- VDOM
- 同構
- 雙端對比
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 見於各種移動端 + Webview
的 Hybrid
場景,是需要頁面與客戶端研發協作的一種優化手段。
ESR (Edge Side Rendering)
Edge
就是邊緣,類比前面的各種 XSR
,ESR
就是將渲染工作交給邊緣伺服器節點,常見的就是 CDN
的邊緣節點。這個方案主打的是邊緣節點相比核心伺服器與使用者的距離優勢,利用了 CDN
分級快取的概念,渲染和內容填充也可以是分級進行並快取下來的。
ESR
之下靜態內容與動態內容是分流的,
1. 邊緣 CDN 節點可以將靜態頁面內容先響應給使用者
2. 然後再自己發起動態內容請求,得到核心伺服器響應之後再返回給使用者
是在大型網路架構下非常極致的一種優化,但這也就依賴更龐大的技術基建體系了。
ISR (Incremental Site Rendering)
增量式網站渲染,就是對待頁面內容小刀切,有更細的差異化渲染粒度,能漸進、分層地進行渲染。
常見的選擇是:
- 對於重要頁面如首屏、訪問量較大的直接落地頁,進行預渲染並新增快取,保證最佳的訪問效能;
- 對於次要頁面,則確保有兜底內容可以即時 fallback
,再將其實時資料的渲染留到 CSR 層次完成,同時觸發非同步快取更新。
對於“非同步快取更新”,則需要提到一個常見的內容快取策略:Stale While Revalidate
,CDN 對於資料請求始終首先響應快取內容,如果這份內容已經過期,則在響應之後再觸發非同步更新——這也是對於次要元素或頁面的快取處理方式。
WebComponents
Web Components
是一套不同的技術,允許您建立可重用的定製元素並且在您的 web 應用中使用它們
三要素
Custom elements
(自定義元素): 一組JavaScript
API,允許您定義custom elements
及其行為,然後可以在您的使用者介面中按照需要使用它們。- 通過
class A extends HTMLElement {}
定義元件, - 通過
window.customElements.define('a-b', A)
掛載已定義元件。
- 通過
Shadow DOM
(影子 DOM ):一組JavaScript
API,用於將封裝的“影子” DOM 樹附加到元素(與主文件 DOM 分開呈現)並控制其關聯的功能。- 通過這種方式,您可以保持元素的功能私有,這樣它們就可以被指令碼化和樣式化,而不用擔心與文件的其他部分發生衝突。
- 使用
const shadow = this.attachShadow({mode : 'open'})
在WebComponents
中開啟。
HTML templates
(HTML 模板)slot
:template
可以簡化生成dom
元素的操作,不再需要createElement
每一個節點。
雖然 WebComponents
有三個要素,但卻不是缺一不可的,WebComponents
- 藉助
shadow dom
來實現樣式隔離,- 藉助
templates
來簡化標籤的操作。
內部生命週期函式(4個)
connectedCallback
: 當WebComponents
第一次被掛在到dom
上是觸發的鉤子,並且只會觸發一次。- 類似
React
中的useEffect(() => {}, [])
,componentDidMount
。
- 類似
disconnectedCallback
: 當自定義元素與文件DOM
斷開連線時被呼叫。adoptedCallback
: 當自定義元素被移動到新文件時被呼叫。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 中
property
與attribute
的區別: -attribute
是HTML
標籤上的特性,可以理解為標籤屬性,它的值只能夠是String
型別,並且會自動新增同名 DOM 屬性作為 property 的初始值; -property
是DOM
中的屬性,是JavaScript
裡的物件,有同名attribiute
標籤屬性的property
屬性值的改變也並不會同步引起attribute
標籤屬性值的改變;
Lit
元件接收標籤屬性 attribute
並將其狀態儲存為 JavaScript
的 class
欄位屬性或 properties
。響應式 properties
是可以在更改時觸發響應式更新週期、重新渲染元件以及可選地讀取或重新寫入 attribute
的屬性。每一個 properties
屬性都可以配置它的選項物件
傳入複雜資料型別
對於複雜資料的處理,為什麼會存在這個問題,根本原因還是因為 attribute
標籤屬性值只能是 String
型別,其他型別需要進行序列化。在 LitElement
中,只需要在父元件模板的屬性值前使用.
操作符,這樣子元件內部 properties
就可以正確序列化為目標型別。
優點
LitElement
在 Web Components
開發方面有著很多比原生的優勢,它具有以下特點:
- 簡單:在
Web Components
標準之上構建,Lit
添加了響應式、宣告性模板和一些周到的功能,減少了模板檔案。- 快速:更新速度很快,因為
Lit
會跟蹤UI
的動態部分,並且只在底層狀態發生變化時更新那些部分——無需重建整個虛擬樹並將其與 DOM 的當前狀態進行比較。- 輕便:
Lit
的壓縮後大小約為 5 KB,有助於保持較小的包大小並縮短載入時間。- 高擴充套件性:
lit-html
基於標記的template
,它結合了 ES6 中的模板字串語法,使得它無需預編譯、預處理,就能獲得瀏覽器原生支援,並且擴充套件能力強。- 相容良好:對瀏覽器相容性非常好,對主流瀏覽器都能有非常好的支援。
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.0
的 E
模組,此時:
A
和 D
依賴 [email protected]
C
和 E
依賴 [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
的安裝速度在大多數場景都比 npm
和 yarn
快 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:查詢路徑是按照給定的root
或alias
為根路徑來查詢的
- 關鍵點3:如果給出的file
都沒有匹配到,則重新請求最後一個引數給定的uri
,就是新的location
匹配
webpack 優化
時間方向(8個)
- 開發環境 -
EvalSourceMapDevToolPlugin
排除第三方模組devtool:false
EvalSourceMapDevToolPlugin
,通過傳入module: true
和column:false
,達到和預設eval-cheap-module-source-map
一樣的質量
- 縮小
loader
的搜尋範圍:test、include、exclude
Module.noParse
noParse: /jquery|lodash/
,
TypeScript
編譯優化Resolve.modules
指定查詢模組的目錄範圍Resolve.alias
Resolve.extensions
指定查詢模組的檔案類型範圍HappyPack
資源大小(9個)
- 按需引入類庫模組 (工具類庫)
- 使用
babel-plugin-import
對其處理
- 使用
- 使用
externals
優化cdn
靜態資源 - CSS抽離+剔除無用樣式 -
MiniCssExtractPlugin
+PurgeCSS
- CSS壓縮 -
CssMinimizerWebpackPlugin
TreeSharking
- CSS 方向 -
glob-all
purify-css
purifycss-webpack
- JS方向 -
babel-loader
版本問題
- CSS 方向 -
Code Spilt
-optimization
-splitChunks
-chunks:all
- 魔法註釋 -
webpackChunkName:’xxx‘
Scope Hoisting
-optimization
-concatenateModules:true
- 普通打包只是將一個模組最終放入一個單獨的函式中,如果模組很多,就意味著在輸出結果中會有很多的模組函式。concatenateModules 配置的作用,儘可能將所有模組合併到一起輸出到一個函式中,既提升了執行效率,又減少了程式碼的體積。
- 圖片壓縮 -
image-webpack-loader
- 只要在file-loader
之後加入image-webpack-loader
即可
共同方案
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的區別
共同點
- 資料驅動檢視
- 元件化
- 都使用
Virtual DOM
不同點
- 核心思想
Vue
靈活易用的漸進式框架,進行資料攔截/代理,它對偵測資料的變化更敏感、更精確React
推崇函數語言程式設計(純元件),資料不可變以及單向資料流
- 元件寫法差異
React
推薦的做法是JSX + inline style
, 也就是把HTML
和CSS
全都寫進 JavaScript 中,即all in js
;Vue
推薦的做法是template
的單檔案元件格式即html
,css
,JS
寫在同一個檔案
diff
演算法不同- 兩者流程思路上是類似的:不同的元件產生不同的 DOM 結構。當type不相同時,對應DOM操作就是直接銷燬老的DOM,建立新的DOM。 同一層次的一組子節點,可以通過唯一的 key 區分。
Vue-Diff
演算法採用了雙端比較的演算法,同時從新舊children
的兩端開始進行比較,藉助key
值找到可複用的節點,再進行相關操作。相比React
的Diff
演算法,同樣情況下可以減少移動節點次數,減少不必要的效能損耗,更加的優雅。
- 響應式原理不同
Vue
依賴收集,自動優化,資料可變, 當資料改變時,自動找到引用元件重新渲染React
基於狀態機,手動優化,資料不可變,需要setState
驅動新的state
替換老的state
。 當資料改變時,以元件為根目錄,預設全部重新渲染。
Webpack有哪些常用的loader和plugin
Webpack Loader vs Plugin
loader
是檔案載入器,能夠載入資原始檔,並對這些檔案進行一些處理,諸如編譯、壓縮等,最終一起打包到指定的檔案中plugin
賦予了webpack
各種靈活的功能,例如打包優化、資源管理、環境變數注入等,目的是解決 loader 無法實現的其他事
-
loader
執行在打包檔案之前
- plugins
在整個編譯週期都起作用
常用loader
- 樣式:
style-loader
、css-loader
、less-loader
、sass-loader
、MiniCssExtractPlugin
+PurgeCSS
+CssMinimizerWebpackPlugin
- js:
bable-loader
/ts-loader
- 圖片:
url-loader
(limit
)、file-loader
、image-webpack-loader
- 程式碼校驗:
eslint-loader
常用plugin
HtmlWebpackPlugin
:會在打包結束之後自動建立一個index.html
, 並將打包好的JS自動引入到這個檔案中MiniCssExtractPlugin
IgnorePlugin
:用於忽略第三方包指定目錄,讓指定目錄不被打包進去terser-webpack-plugin
:壓縮js程式碼SplitChunksPlugin
:Code-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+ 新語法和新特性;- 注:語法和特性不是一回事,語法上的迭代是讓我們書寫程式碼更加簡單和方便,如展開運算子、類,結構等,因此這些語法稱為語法糖;特性上的迭代是為了擴充套件語言的能力,如
Map
、Promise
等, - 事實上,
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 個問題:
- 從
corejs
引入的polyfill
是全域性範圍的,不是模組作用域返回的,可能存在汙染全域性變數的風險; - 對於某些新語法,如
class
,會在編譯後的檔案中注入很多helper
函式宣告,而不是從某個地方require
進來的函式引用,從而增大編譯後的包體積;
runtime
runtime
是 babel7
提出來的概念,旨在解決如上提出的效能問題的。
實踐一下 @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 之後以後生成的。這樣就可以加上 state
、props
和一些動態邏輯,動態產生 vdom
。
vdom
生成之後不再是直接渲染,而是先轉成 fiber,這個vdom
轉fiber
的過程叫做reconcile
。
fiber
是一個連結串列結構,可以打斷,這樣就可以通過 requestIdleCallback
來空閒排程 reconcile
,這樣不斷的迴圈,直到處理完所有的 vdom
轉 fiber
的 reconcile
,就開始 commit
,也就是更新到 dom
。
reconcile
的過程會提前建立好 dom
,還會標記出增刪改,那麼 commit
階段就很快了。
從之前遞迴渲染時做
diff
來確定增刪改以及建立dom
,提前到了可打斷的reconcile
階段,讓commit
變得非常快,這就是fiber
架構的目的和意義。
併發&排程(Concurrency & Scheduler)
Concurrency
併發: 有能力優先處理更高優事務,同時對正在執行的中途任務可暫存,待高優完成後,再去執行。Scheduler
協調排程: 暫存未執行任務,等待時機成熟後,再去安排執行剩下未完成任務。
考慮到可中斷渲染,並可重回構造。React
自行實現了一套體系叫做 React fiber
架構。
React Fiber
核心: 自行實現 虛擬棧幀。
schedule 就是通過空閒排程每個
fiber
節點的reconcile
(vdom
轉fiber
),全部reconcile
完了就執行commit
。
Fiber
的資料結構有三層資訊: (採用連結串列結構)
1. 例項屬性
- 該Fiber的基本資訊,例如元件型別等。
2. 構建屬性
- 構建屬性 (return
、child
、sibling
)
3. 工作屬性
- 資料的變更會導致UI層的變更
- 為了減少對DOM
的直接操作,通過Reconcile
進行diff
查詢,並將需要變更節點,打上標籤,變更路徑保留在effectList
裡
- 待變更內容要有Scheduler
優先順序處理
- 涉及到diff
等查詢操作,是需要有個高效手段來處理前後變化,即雙快取機制。
連結串列結構即可支援隨時隨時中斷的訴求
Scheduler 執行核心點
- 有個任務佇列
queue
,該佇列存放可中斷的任務。 workLoop
對佇列裡取第一個任務currentTask
,進入迴圈開始執行。- 當該任務沒有時間 或 需要中斷 (渲染任務 或 其他高優任務插入等),則讓出主執行緒。
requestAnimationFrame
計算一幀的空餘時間;- 使用
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本質
注意其中一些細節:
useState
和useEffect
在每次呼叫時都被新增到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:
- React 函式元件
- 自定義 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}
關於記憶化快取(Memoization)
Memoization
,一般稱為記憶化快取(或者“記憶”),它背後的思想很簡單:假如我們有一個計算量很大的純函式(給定相同的輸入,一定會得到相同的輸出),那麼我們在第一次遇到特定輸入的時候,把它的輸出結果“記”(快取)下來,那麼下次碰到同樣的輸出,只需要從快取裡面拿出來直接返回就可以了,省去了計算的過程!
記憶化快取(Memoization)的兩個使用場景:
- 通過快取計算結果,節省費時的計算
- 保證相同輸入下返回值的引用相等
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. 保證相同輸入下返回值的引用相等
而useCallback
和uesMemo
從Memoization
角度來說
- 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))
後記
分享是一種態度。
全文完,既然看到這裡了,如果覺得不錯,隨手點個贊和“在看”吧。