React 作為前端使用最多的框架,必然是面試的重點。我們接下來主要從 React 的使用方式、原始碼層面和周邊生態(如 redux, react-router 等)等幾個方便來進行總結。
這裡主要考察的是,在開發使用過程中,對 React 框架的瞭解,如 hook 的不同呼叫方式得到的結果、函陣列件中的 useState 和類元件的 state 的區別等等。
React 元件的 props 變動,會讓元件重新執行,但並不會引起 state 的值的變動。state 值的變動,只能由 setState() 來觸發。因此若想在 props 變動時,重置 state 的資料,需要監聽 props 的變動,如:
const App = props => {
const [count, setCount] = useState(0);
// 監聽 props 的變化,重置 count 的值
useEffect(() => {
setCount(0);
}, [props]);
return <div onClick={() => setCount(count + 1)}>{count}</div>;
};
React 的更新都是漸進式的更新,在 React18 中啟用的新特性,其實在 v17 中(甚至更早)就埋下了。
createRoot()
建立一個 root 節點,然後該 root 節點來呼叫render()
方法;React 中的並行
,並不是指同一時刻同時在做多件事情。因為 js 本身就是單執行緒的(同一時間只能執行一件事情),而且還要跟 UI 渲染競爭主執行緒。若一個很耗時的任務佔據了執行緒,那麼後續的執行內容都會被阻塞。為了避免這種情況,React 就利用 fiber 結構和時間切片的機制,將一個大任務分解成多個小任務,然後按照任務的優先順序和執行緒的佔用情況,對任務進行排程。
我們稍微瞭解下什麼是受控元件和非受控元件:
那麼如何將非受控元件改為受控元件呢?那就是把上面的這些純 html 元件資料或狀態的變更,交給 React 來操作:
const App = () => {
const [value, setValue] = useState('');
const [checked, setChecked] = useState(false);
return (
<>
<input value={value} onInput={event => setValue(event.target.value)} />
<input type="checkbox" checked={checked} onChange={event => setChecked(event.target.checked)} />
</>
);
};
上面程式碼中,輸入框和 checkbox 的變化,均是經過了 React 來操作的,在資料變更時,React 是能夠知道的。
高階元件通過包裹(wrapped)被傳入的 React 元件,經過一系列處理,最終返回一個相對增強(enhanced)的 React 元件,供其他元件呼叫。
作用:
參考:react 進階」一文吃透 React 高階元件(HOC)
官方網站有介紹該原因:使用 Hook 的動機。
這裡我們簡要的提煉下:
componentDidMount
中執行和獲取資料,而之後需在 componentWillUnmount
中清除;但在函陣列件中,不同的邏輯可以放在不同的 Hook 中執行,互不干擾;this
的使用,如 this.onClick.bind(this)
,this.state
,this.setState()
等,同時,class 不能很好的壓縮,並且會使熱過載出現不穩定的情況;Hook 使你在非 class 的情況下可以使用更多的 React 特性;useCallback 和 useMemo 可以用來快取函數和變數,提高效能,減少資源浪費。但並不是所有的函數和變數都需要用這兩者來實現,他也有對應的使用場景。
我們知道 useCallback 可以快取函數體,在依賴項沒有變化時,前後兩次渲染時,使用的函數體是一樣的。它的使用場景是:
主要是為了避免重新生成的函數,會導致其他 hook 或元件的不必要重新整理。
useMemo 用來快取函數執行的結果。如每次渲染時都要執行一段很複雜的運算,或者一個變數需要依賴另一個變數的運算結果,就都可以使用 useMemo()。
參考文章:React18 原始碼解析之 useCallback 和 useMemo。
useState()的傳參有兩種方式:純資料和回撥函數。這兩者在初始化時,除了傳入方式不同,沒啥區別。但在呼叫時,不同的呼叫方式和所在環境,輸出的結果也是不一樣的。
如:
const App = () => {
const [count, setCount] = useState(0);
const handleParamClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
const handleCbClick = () => {
setCount(count => count + 1);
setCount(count => count + 1);
setCount(count => count + 1);
};
};
上面的兩種傳入方式,最後得到的 count 結果是不一樣的。為什麼呢?因為在以資料的格式傳參時,這 3 個使用的是同一個 count 變數,數值是一樣的。相當於setCount(0 + 1)
,呼叫了 3 次;但以回撥函數的傳參方式,React 則一般地會直接該回撥函數,然後得到最新結果並儲存到 React 內部,下次使用時就是最新的了。注意:這個最新值是儲存在 React 內部的,外部的 count 並不會馬上更新,只有在下次渲染後才會更新。
還有,在定時器中,兩者得到的結果也是不一樣的:
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 500);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const timer = setInterval(() => {
setCount(count => count + 1);
}, 500);
return () => clearInterval(timer);
}, []);
};
在 React.StrictMode 模式下,如果用了 useState,usesMemo,useReducer 之類的 Hook,React 會故意渲染兩次,為的就是將一些不容易發現的錯誤容易暴露出來,同時 React.StrictMode 在正式環境中不會重複渲染。
也就是在測試環境的嚴格模式下,才會渲染兩次。
從 16.6.0 開始,React 提供了 lazy 和 Suspense 來實現懶載入。
import React, { lazy, Suspense } from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
);
}
屬性fallback
表示在載入元件前,渲染的內容。
若在定時器內直接使用 React 的程式碼,可能會收到意想不到的結果。如我們想實現一個每 1 秒加 1 的定時器:
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div className="App">{count}</div>;
};
可以看到,coun 從 0 變成 1 以後,就再也不變了。為什麼會這樣?
儘管由於定時器的存在,元件始終會一直重新渲染,但定時器的回撥函數是掛載期間定義的,所以它的閉包永遠是對掛載時 Counter 作用域的參照,故 count 永遠不會超過 1。
針對這個單一的 hook 呼叫,還比較好解決,例如可以監聽 count 的變化,或者通過 useState 的 callback 傳參方式。
const App = () => {
const [count, setCount] = useState(0);
// 監聽 count 的變化,不過這裡將定時器改成了 setTimeout
// 即使不修改,setInterval()的timer也會在每次渲染時被清除掉,
// 然後重新啟動一個新的定時器
useEffect(() => {
const timer = setTimeout(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]);
// 以回撥的方式
// 回撥的方式,會計算回撥的結果,然後作為下次更新的初始值
// 詳情可見: https://www.xiabingbao.com/post/react/react-usestate-rn5bc0.html#5.+updateReducer
useEffect(() => {
const timer = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div className="App">{count}</div>;
};
當然還有別的方式也可以實現 count 的更新。那要是呼叫更多的 hook,或者更復雜的程式碼,該怎麼辦呢?這裡我們可以封裝一個新的 hook 來使用:
// https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/
const useInterval = (callback: () => void, delay: number | null): void => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
useEffect(callback)的回撥函數裡,若有返回的函數,這是 effect 可選的清除機制。每個 effect 都可以返回一個清除函數。
React 何時清除 effect? React 會在元件解除安裝的時候執行清除操作。同時,若元件產生了更新,會先執行上一個的清除函數,然後再執行下一個 effect。如
// 執行第一個 effect
// 產生更新時
// 清除上一個 effect
// 執行下一個 effect
// 產生更新時
// 清除上一個 effect
// 執行下一個 effect
// 元件解除安裝時
// 清除最後一個 effect
這部分考察的就更有深度一些了,多多少少得了解一些原始碼,才能明白其中的緣由,比如 React 的 diff 對比,迴圈中 key 的作用等。
Virtual DOM 是以物件的方式來描述真實 dom 物件的,那麼在做一些 update 的時候,可以在記憶體中進行資料比對,減少對真實 dom 的操作減少瀏覽器重排重繪的次數,減少瀏覽器的壓力,提高程式的效能,並且因為 diff 演演算法的差異比較,記錄了差異部分,那麼在開發中就會幫助程式設計師減少對差異部分心智負擔,提高了開發效率。
虛擬 dom 好多這麼多,渲染速度上是不是比直接操作真實 dom 快呢?並不是。虛擬 dom 增加了一層記憶體運算,然後才操作真實 dom,將資料渲染到頁面上。渲染上肯定會慢上一些。雖然虛擬 dom 的缺點在初始化時增加了記憶體運算,增加了首頁的渲染時間,但是運算時間是以毫秒級別或微秒級別算出的,對使用者體驗影響並不是很大。
React 中所有觸發的事件,都是自己在其內部封裝了一套事件機制。目的是為了實現全瀏覽器的一致性,抹平不同瀏覽器之間的差異性。
在 React17 之前,React 是把事件委託在 document 上的,React17 及以後版本不再把事件委託在 document 上,而是委託在掛載的容器上。React 合成事件採用的是事件冒泡機制,當在某具體元素上觸發事件時,等冒泡到頂部被掛載事件的那個元素時,才會真正地執行事件。
而原生事件,當某具體元素觸發事件時,會立刻執行該事件。因此若要比較事件觸發的先後時機時,原生事件會先執行,React 合成事件會後執行。
key 幫助 React 識別哪些元素改變了,比如被新增或刪除。因此你應當給陣列中的每一個元素賦予一個確定的標識。
當元件重新整理時,React 內部會根據 key 和元素的 type,來對比元素是否發生了變化。若選做 key 的資料有問題,可能會在更新的過程中產生異常。
在 React18 中,無論是多個 useState()的 hook,還是操作(dispatch)多次的資料。只要他們在同一優先順序,React 就會將他們合併到一起操作,最後再更新資料。
這是基於 React18 的批次處理機制。React 將多個狀態更新分組到一個重新渲染中以獲得更好的效能。(將多次 setstate 事件合併);在 v18 之前只在事件處理常式中實現了批次處理,在 v18 中所有更新都將自動批次處理,包括 promise 鏈、setTimeout 等非同步程式碼以及原生事件處理常式;
參考:多次呼叫 useState() 中的 dispatch 方法,會產生多次渲染嗎?
首先宣告,我們不應當直接修改 state 的值,一方面是無法重新整理元件(無法將新資料渲染到頁面中),再有可能會對下次的更新產生影響。
唯一有影響的,就是後續要使用該變數的地方,會使用到新資料。但若其他 useState() 導致了元件的重新整理,剛才變數的值,若是基本型別(比如數位、字串等),會重置為修改之前的值;若是複雜型別,基於 js 的 物件參照 特性,也會同步修改 React 內部儲存的資料,但不會引起檢視的變化。
通過上面的 diff 對比過程,我們也可以看到,當元件產生比較大的變更時,React 需要做更多的動作,來構建出新的 fiber 樹,因此我們在開發過程中,若從效能優化的角度考慮,尤其要注意的是:
參考:React18 原始碼解析之 reconcileChildren 生成 fiber 的過程
JavaScript 中的 map 不會對為 null 或者 undefined 的資料進行處理,而 React.Children.map 中的 map 可以處理 React.Children 為 null 或者 undefined 的情況。
這部分主要考察 React 周邊生態配套的瞭解,如狀態管理庫 redux、mobx,路由元件 react-router-dom 等。
React-router: 提供了路由的核心 api。如 Router、Route、Switch 等,但沒有提供有關 dom 操作進行路由跳轉的 api;
React-router-dom: 提供了 BrowserRouter、Route、Link 等 api,可以通過 dom 操作觸發事件控制路由。
Link 元件,會渲染一個 a 標籤;BrowserRouter 和 HashRouter 元件,前者使用 pushState 和 popState 事件構建路由,後者使用 hash 和 hashchange 事件構建路由。
react-router-dom 在 react-router 的基礎上擴充套件了可操作 dom 的 api。 Swtich 和 Route 都是從 react-router 中匯入了相應的元件並重新匯出,沒做什麼特殊處理。
react-router-dom 中 package.json 依賴中存在對 react-router 的依賴,故此,不需要額外安裝 react-router。
Redux 使用 「Store」 將程式的整個狀態儲存在同一個地方。因此所有元件的狀態都儲存在 Store 中,並且它們從 Store 本身接收更新。單一狀態樹可以更容易地跟蹤隨時間的變化,並偵錯或檢查程式。
Redux 的優點如下:
React 涉及到的相關知識點非常多,我也會經常更新的。
歡迎關注我的公眾號:「前端小茶館」。