React高頻面試題梳理,看看面試怎麼答?(上)
- 2019 年 10 月 4 日
- 筆記
這是ConardLi的第67篇原創,謝謝你的支持!
文中流程圖都為ConardLi原創,如需使用,請標明來源。
前段時間準備面試,總結了很多,下面是我在準備React面試時,結合自己的實際面試經歷,以及我以前源碼分析的文章,總結出來的一些 React
高頻面試題目。
以前我寫的源碼分析的文章,並沒有很多人看,因為大部分情況下你不需要深入源碼也能懂得其中原理,並解決實際問題,這也是我總結這些面試題的原因,讓你在更短的時間內獲得更大的收益。
由於是以面試題的角度來討論,所以某些點可能不能非常深入,我在問題下面都貼了相關鏈接,如果想深入理解,請點擊這些文章。
由於題目較多,分為上、下兩篇,本篇文章我們先來討論如下19個題目:
React
生命周期有哪些,16版本生命周期發生了哪些變化?setState
是同步的還是異步的?- 為什麼有時連續多次
setState
只有一次生效? React
如何實現自己的事件機制?- 為何
React
事件要自己綁定this
? - 原生事件和
React
事件的區別? React
的合成事件是什麼?React
和原生事件的執行順序是什麼?可以混用嗎?虛擬Dom
是什麼?虛擬Dom
比普通Dom
更快嗎?虛擬Dom
中的$$typeof
屬性的作用是什麼?React
組件的渲染流程是什麼?- 為什麼代碼中一定要引入
React
? - 為什麼
React
組件首字母必須大寫? React
在渲染真實Dom
時做了哪些性能優化?- 什麼是高階組件?如何實現?
HOC
在業務場景中有哪些實際應用場景?- 高階組件(
HOC
)和Mixin
的異同點是什麼? Hook
有哪些優勢?
React生命周期有哪些,16版本生命周期發生了哪些變化?
15生命周期

- 初始化階段
constructor
構造函數getDefaultProps
props
默認值getInitialState
state
默認值
- 掛載階段
componentWillMount
組件初始化渲染前調用render
組件渲染componentDidMount
組件掛載到DOM
後調用
- 更新階段
componentWillReceiveProps
組件將要接收新props
前調用shouldComponentUpdate
組件是否需要更新componentWillUpdate
組件更新前調用render
組件渲染componentDidUpdate
組件更新後調用
- 卸載階段
componentWillUnmount
組件卸載前調用
16生命周期

- 初始化階段
constructor
構造函數getDefaultProps
props
默認值getInitialState
state
默認值
- 掛載階段
staticgetDerivedStateFromProps(props,state)
render
componentDidMount
getDerivedStateFromProps
:組件每次被rerender
的時候,包括在組件構建之後(虛擬dom
之後,實際dom
掛載之前),每次獲取新的props
或state
之後;每次接收新的props之後都會返回一個對象作為新的state
,返回null則說明不需要更新state
;配合componentDidUpdate
,可以覆蓋componentWillReceiveProps
的所有用法
- 更新階段
staticgetDerivedStateFromProps(props,state)
shouldComponentUpdate
render
getSnapshotBeforeUpdate(prevProps,prevState)
componentDidUpdate
getSnapshotBeforeUpdate
:觸發時間:update
發生的時候,在render
之後,在組件dom
渲染之前;返回一個值,作為componentDidUpdate
的第三個參數;配合componentDidUpdate
, 可以覆蓋componentWillUpdate
的所有用法
- 卸載階段
componentWillUnmount
- 錯誤處理
componentDidCatch
React16
新的生命周期棄用了 componentWillMount、componentWillReceivePorps,componentWillUpdate
新增了 getDerivedStateFromProps、getSnapshotBeforeUpdate
來代替棄用的三個鉤子函數。
React16
並沒有刪除這三個鉤子函數,但是不能和新增的鉤子函數混用,React17
將會刪除這三個鉤子函數,新增了對錯誤的處理(componentDidCatch
)
setState是同步的還是異步的?
- 生命周期和合成事件中
在 React
的生命周期和合成事件中, React
仍然處於他的更新機制中,這時無論調用多少次 setState
,都會不會立即執行更新,而是將要更新的·存入 _pendingStateQueue
,將要更新的組件存入 dirtyComponent
。
當上一次更新機制執行完畢,以生命周期為例,所有組件,即最頂層組件 didmount
後會將批處理標誌設置為 false
。這時將取出 dirtyComponent
中的組件以及 _pendingStateQueue
中的 state
進行更新。這樣就可以確保組件不會被重新渲染多次。
componentDidMount() { this.setState({ index: this.state.index + 1 }) console.log('state', this.state.index); }
所以,如上面的代碼,當我們在執行 setState
後立即去獲取 state
,這時是獲取不到更新後的 state
的,因為處於 React
的批處理機制中, state
被暫存起來,待批處理機制完成之後,統一進行更新。
所以。setState
本身並不是異步的,而是 React
的批處理機制給人一種異步的假象。
- 異步代碼和原生事件中
componentDidMount() { setTimeout(() => { console.log('調用setState'); this.setState({ index: this.state.index + 1 }) console.log('state', this.state.index); }, 0); }
如上面的代碼,當我們在異步代碼中調用 setState
時,根據 JavaScript
的異步機制,會將異步代碼先暫存,等所有同步代碼執行完畢後在執行,這時 React
的批處理機制已經走完,處理標誌設被設置為 false
,這時再調用 setState
即可立即執行更新,拿到更新後的結果。
在原生事件中調用 setState
並不會出發 React
的批處理機制,所以立即能拿到最新結果。
- 最佳實踐
setState
的第二個參數接收一個函數,該函數會在 React
的批處理機制完成之後調用,所以你想在調用 setState
後立即獲取更新後的值,請在該回調函數中獲取。
this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); })
推薦閱讀:由實際問題探究setState的執行機制
為什麼有時連續多次setState只有一次生效?
例如下面的代碼,兩次打印出的結果是相同的:
componentDidMount() { this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); }) this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); }) }
原因就是 React
會批處理機制中存儲的多個 setState
進行合併,來看下 React
源碼中的 _assign
函數,類似於 Object
的 assign
:
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
如果傳入的是對象,很明顯會被合併成一次,所以上面的代碼兩次打印的結果是相同的:
Object.assign( nextState, {index: state.index+ 1}, {index: state.index+ 1} )
注意, assign
函數中對函數做了特殊處理,處理第一個參數傳入的是函數,函數的參數 preState
是前一次合併後的結果,所以計算結果是準確的:
componentDidMount() { this.setState((state, props) => ({ index: state.index + 1 }), () => { console.log(this.state.index); }) this.setState((state, props) => ({ index: state.index + 1 }), () => { console.log(this.state.index); }) }
所以上面的代碼兩次打印的結果是不同的。
- 最佳實踐
React
會對多次連續的 setState
進行合併,如果你想立即使用上次 setState
後的結果進行下一次 setState
,可以讓 setState
接收一個函數而不是一個對象。這個函數用上一個 state
作為第一個參數,將此次更新被應用時的 props
做為第二個參數。
React如何實現自己的事件機制?
React
事件並沒有綁定在真實的 Dom
節點上,而是通過事件代理,在最外層的 document
上對事件進行統一分發。

組件掛載、更新時:
- 通過
lastProps
、nextProps
判斷是否新增、刪除事件分別調用事件註冊、卸載方法。 - 調用
EventPluginHub
的enqueuePutListener
進行事件存儲 - 獲取
document
對象。 - 根據事件名稱(如
onClick
、onCaptureClick
)判斷是進行冒泡還是捕獲。 - 判斷是否存在
addEventListener
方法,否則使用attachEvent
(兼容IE)。 - 給
document
註冊原生事件回調為dispatchEvent
(統一的事件分發機制)。
事件初始化:
EventPluginHub
負責管理React
合成事件的callback
,它將callback
存儲在listenerBank
中,另外還存儲了負責合成事件的Plugin
。- 獲取綁定事件的元素的唯一標識
key
。 - 將
callback
根據事件類型,元素的唯一標識key
存儲在listenerBank
中。 listenerBank
的結構是:listenerBank[registrationName][key]
。
觸發事件時:
- 觸發
document
註冊原生事件的回調dispatchEvent
- 獲取到觸發這個事件最深一級的元素
- 遍歷這個元素的所有父元素,依次對每一級元素進行處理。
- 構造合成事件。
- 將每一級的合成事件存儲在
eventQueue
事件隊列中。 - 遍歷
eventQueue
。 - 通過
isPropagationStopped
判斷當前事件是否執行了阻止冒泡方法。 - 如果阻止了冒泡,停止遍歷,否則通過
executeDispatch
執行合成事件。 - 釋放處理完成的事件。
React
在自己的合成事件中重寫了stopPropagation
方法,將isPropagationStopped
設置為true
,然後在遍歷每一級事件的過程中根據此遍歷判斷是否繼續執行。這就是React
自己實現的冒泡機制。
推薦閱讀:【React深入】React事件機制
為何React事件要自己綁定this?
在上面提到的事件處理流程中, React
在 document
上進行統一的事件分發, dispatchEvent
通過循環調用所有層級的事件來模擬事件冒泡和捕獲。
在 React
源碼中,當具體到某一事件處理函數將要調用時,將調用 invokeGuardedCallback
方法。
function invokeGuardedCallback(name, func, a) { try { func(a); } catch (x) { if (caughtError === null) { caughtError = x; } } }
可見,事件處理函數是直接調用的,並沒有指定調用的組件,所以不進行手動綁定的情況下直接獲取到的 this
是不準確的,所以我們需要手動將當前組件綁定到 this
上。
原生事件和React事件的區別?
React
事件使用駝峰命名,而不是全部小寫。- 通過
JSX
, 你傳遞一個函數作為事件處理程序,而不是一個字符串。 - 在
React
中你不能通過返回false
來阻止默認行為。必須明確調用preventDefault
。
React的合成事件是什麼?
React
根據 W3C
規範定義了每個事件處理函數的參數,即合成事件。
事件處理程序將傳遞 SyntheticEvent
的實例,這是一個跨瀏覽器原生事件包裝器。它具有與瀏覽器原生事件相同的接口,包括 stopPropagation()
和 preventDefault()
,在所有瀏覽器中他們工作方式都相同。
React
合成的 SyntheticEvent
採用了事件池,這樣做可以大大節省內存,而不會頻繁的創建和銷毀事件對象。
另外,不管在什麼瀏覽器環境下,瀏覽器會將該事件類型統一創建為合成事件,從而達到了瀏覽器兼容的目的。
React和原生事件的執行順序是什麼?可以混用嗎?
React
的所有事件都通過 document
進行統一分發。當真實 Dom
觸發事件後冒泡到 document
後才會對 React
事件進行處理。
所以原生的事件會先執行,然後執行 React
合成事件,最後執行真正在 document
上掛載的事件
React
事件和原生事件最好不要混用。原生事件中如果執行了 stopPropagation
方法,則會導致其他 React
事件失效。因為所有元素的事件將無法冒泡到 document
上,導致所有的 React
事件都將無法被觸發。。
虛擬Dom是什麼?

在原生的 JavaScript
程序中,我們直接對 DOM
進行創建和更改,而 DOM
元素通過我們監聽的事件和我們的應用程序進行通訊。
而 React
會先將你的代碼轉換成一個 JavaScript
對象,然後這個 JavaScript
對象再轉換成真實 DOM
。這個 JavaScript
對象就是所謂的虛擬 DOM
。
當我們需要創建或更新元素時, React
首先會讓這個 VitrualDom
對象進行創建和更改,然後再將 VitrualDom
對象渲染成真實DOM。
當我們需要對 DOM
進行事件監聽時,首先對 VitrualDom
進行事件監聽, VitrualDom
會代理原生的 DOM
事件從而做出響應。
推薦閱讀:【React深入】深入分析虛擬DOM的渲染過程和特性
虛擬Dom比普通Dom更快嗎?
很多文章說 VitrualDom
可以提升性能,這一說法實際上是很片面的。
直接操作 DOM
是非常耗費性能的,這一點毋庸置疑。但是 React
使用 VitrualDom
也是無法避免操作 DOM
的。
如果是首次渲染, VitrualDom
不具有任何優勢,甚至它要進行更多的計算,消耗更多的內存。
VitrualDom
的優勢在於 React
的 Diff
算法和批處理策略, React
在頁面更新之前,提前計算好了如何進行更新和渲染 DOM
。實際上,這個計算過程我們在直接操作 DOM
時,也是可以自己判斷和實現的,但是一定會耗費非常多的精力和時間,而且往往我們自己做的是不如 React
好的。所以,在這個過程中 React
幫助我們"提升了性能"。
所以,我更傾向於說, VitrualDom
幫助我們提高了開發效率,在重複渲染時它幫助我們計算如何更高效的更新,而不是它比 DOM
操作更快。
虛擬Dom中的$$typeof屬性的作用是什麼?
ReactElement
中有一個 $$typeof
屬性,它被賦值為 REACT_ELEMENT_TYPE
:
var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || 0xeac7;
可見, $$typeof
是一個 Symbol
類型的變量,這個變量可以防止 XSS
。
如果你的服務器有一個漏洞,允許用戶存儲任意 JSON
對象, 而客戶端代碼需要一個字符串,這可能會成為一個問題:
// JSON let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* put your exploit here */' }, }, }; let message = { text: expectedTextButGotJSON }; <p> {message.text} </p>
JSON
中不能存儲 Symbol
類型的變量。
ReactElement.isValidElement
函數用來判斷一個 React
組件是否是有效的,下面是它的具體實現。
ReactElement.isValidElement = function (object) { return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE; };
可見 React
渲染時會把沒有 $$typeof
標識,以及規則校驗不通過的組件過濾掉。
當你的環境不支持 Symbol
時, $$typeof
被賦值為 0xeac7
,至於為什麼, React
開發者給出了答案:
0xeac7
看起來有點像React
。
React組件的渲染流程是什麼?
- 使用
React.createElement
或JSX
編寫React
組件,實際上所有的JSX
代碼最後都會轉換成React.createElement(...)
,Babel
幫助我們完成了這個轉換的過程。 -
createElement
函數對key
和ref
等特殊的props
進行處理,並獲取defaultProps
對默認props
進行賦值,並且對傳入的孩子節點進行處理,最終構造成一個ReactElement
對象(所謂的虛擬DOM
)。 -
ReactDOM.render
將生成好的虛擬DOM
渲染到指定容器上,其中採用了批處理、事務等機制並且對特定瀏覽器進行了性能優化,最終轉換為真實DOM
。
為什麼代碼中一定要引入React?
JSX
只是為 React.createElement(component,props,...children)
方法提供的語法糖。
所有的 JSX
代碼最後都會轉換成 React.createElement(...)
, Babel
幫助我們完成了這個轉換的過程。
所以使用了 JSX
的代碼都必須引入 React
。
為什麼React組件首字母必須大寫?
babel
在編譯時會判斷 JSX
中組件的首字母,當首字母為小寫時,其被認定為原生 DOM
標籤, createElement
的第一個變量被編譯為字符串;當首字母為大寫時,其被認定為自定義組件, createElement
的第一個變量被編譯為對象;
React在渲染真實Dom時做了哪些性能優化?
在 IE(8-11)
和 Edge
瀏覽器中,一個一個插入無子孫的節點,效率要遠高於插入一整個序列化完整的節點樹。
React
通過 lazyTree
,在 IE(8-11)
和 Edge
中進行單個節點依次渲染節點,而在其他瀏覽器中則首先將整個大的 DOM
結構構建好,然後再整體插入容器。
並且,在單獨渲染節點時, React
還考慮了 fragment
等特殊節點,這些節點則不會一個一個插入渲染。
什麼是高階組件?如何實現?
高階組件可以看作 React
對裝飾模式的一種實現,高階組件就是一個函數,且該函數接受一個組件作為參數,並返回一個新的組件。
高階組件(
HOC
)是React
中的高級技術,用來重用組件邏輯。但高階組件本身並不是ReactAPI
。它只是一種模式,這種模式是由React
自身的組合性質必然產生的。
function visible(WrappedComponent) { return class extends Component { render() { const { visible, ...props } = this.props; if (visible === false) return null; return <WrappedComponent {...props} />; } } }
上面的代碼就是一個 HOC
的簡單應用,函數接收一個組件作為參數,並返回一個新組件,新組建可以接收一個 visible props
,根據 visible
的值來判斷是否渲染Visible。

我們可以通過以下兩種方式實現高階組件:
屬性代理
函數返回一個我們自己定義的組件,然後在 render
中返回要包裹的組件,這樣我們就可以代理所有傳入的 props
,並且決定如何渲染,實際上 ,這種方式生成的高階組件就是原組件的父組件,上面的函數 visible
就是一個 HOC
屬性代理的實現方式。
function proxyHOC(WrappedComponent) { return class extends Component { render() { return <WrappedComponent {...this.props} />; } } }
對比原生組件增強的項:
- 可操作所有傳入的
props
- 可操作組件的生命周期
- 可操作組件的
static
方法 - 獲取
refs
反向繼承
返回一個組件,繼承原組件,在 render
中調用原組件的 render
。由於繼承了原組件,能通過this訪問到原組件的 生命周期、props、state、render
等,相比屬性代理它能操作更多的屬性。
function inheritHOC(WrappedComponent) { return class extends WrappedComponent { render() { return super.render(); } } }
對比原生組件增強的項:
- 可操作所有傳入的
props
- 可操作組件的生命周期
- 可操作組件的
static
方法 - 獲取
refs
- 可操作
state
- 可以渲染劫持
推薦閱讀:【React深入】從Mixin到HOC再到Hook
HOC在業務場景中有哪些實際應用場景?
HOC
可以實現的功能:
- 組合渲染
- 條件渲染
- 操作
props
- 獲取
refs
- 狀態管理
- 操作
state
- 渲染劫持
HOC
在業務中的實際應用場景:
- 日誌打點
- 權限控制
- 雙向綁定
- 表單校驗
具體實現請參考我這篇文章:https://juejin.im/post/5cad39b3f265da03502b1c0a
高階組件(HOC)和Mixin的異同點是什麼?
Mixin
和 HOC
都可以用來解決 React
的代碼復用問題。

圖片來源於網絡
Mixin
可能會相互依賴,相互耦合,不利於代碼維護- 不同的
Mixin
中的方法可能會相互衝突 Mixin
非常多時,組件是可以感知到的,甚至還要為其做相關處理,這樣會給代碼造成滾雪球式的複雜性
而 HOC
的出現可以解決這些問題:
- 高階組件就是一個沒有副作用的純函數,各個高階組件不會互相依賴耦合
- 高階組件也有可能造成衝突,但我們可以在遵守約定的情況下避免這些行為
- 高階組件並不關心數據使用的方式和原因,而被包裹的組件也不關心數據來自何處。高階組件的增加不會為原組件增加負擔
Hook有哪些優勢?
- 減少狀態邏輯復用的風險
Hook
和 Mixin
在用法上有一定的相似之處,但是 Mixin
引入的邏輯和狀態是可以相互覆蓋的,而多個 Hook
之間互不影響,這讓我們不需要在把一部分精力放在防止避免邏輯復用的衝突上。在不遵守約定的情況下使用 HOC
也有可能帶來一定衝突,比如 props
覆蓋等等,使用 Hook
則可以避免這些問題。
- 避免地獄式嵌套
大量使用 HOC
的情況下讓我們的代碼變得嵌套層級非常深,使用 HOC
,我們可以實現扁平式的狀態邏輯復用,而避免了大量的組件嵌套。
- 讓組件更容易理解
在使用 class
組件構建我們的程序時,他們各自擁有自己的狀態,業務邏輯的複雜使這些組件變得越來越龐大,各個生命周期中會調用越來越多的邏輯,越來越難以維護。使用 Hook
,可以讓你更大限度的將公用邏輯抽離,將一個組件分割成更小的函數,而不是強制基於生命周期方法進行分割。
- 使用函數代替class
相比函數,編寫一個 class
可能需要掌握更多的知識,需要注意的點也越多,比如 this
指向、綁定事件等等。另外,計算機理解一個 class
比理解一個函數更快。Hooks
讓你可以在 classes
之外使用更多 React
的新特性。
下篇預告:
ReactDiff
算法的策略是什麼?React
中key
的作用是什麼?ReactFiber
是什麼?為什麼要引入?- 為什麼推薦在
componentDidMount
中發起網絡請求? React
代碼優化?React
組件設計要掌握哪些原則?Redux
的核心原理是什麼?- 什麼是
Redux
中間件? Reduxconnect
函數的實現策略?Mox
的核心原理是什麼?Redux
和Mobx
的異同點,如何選擇?