以下面試題來源於github專案前端面試指南,那裡有超過200道高頻前端面試題及答案,目前擁有1400star.
為什麼選擇使用框架而不是原生?
框架的好處:
- 元件化: 其中以 React 的元件化最為徹底,甚至可以到函式級別的原子元件,高度的元件化可以是我們的工程易於維護、易於組合擴充。
- 天然分層: JQuery 時代的程式碼大部分情況下是麵條程式碼,耦合嚴重,現代框架不管是 MVC、MVP還是MVVM 模式都能幫助我們進行分層,程式碼解耦更易於讀寫。
- 生態: 現在主流前端框架都自帶生態,不管是資料流管理架構還是 UI 庫都有成熟的解決方案。
- 開發效率: 現代前端框架都預設自動更新DOM,而非我們手動操作,解放了開發者的手動DOM成本,提高開發效率,從根本上解決了UI 與狀態同步問題.
虛擬DOM的優劣如何?
優點:
- 保證效能下限: 虛擬DOM可以經過diff找出最小差異,然後批量進行patch,這種操作雖然比不上手動優化,但是比起粗暴的DOM操作效能要好很多,因此虛擬DOM可以保證效能下限
- 無需手動操作DOM: 虛擬DOM的diff和patch都是在一次更新中自動進行的,我們無需手動操作DOM,極大提高開發效率
- 跨平臺: 虛擬DOM本質上是JavaScript物件,而DOM與平臺強相關,相比之下虛擬DOM可以進行更方便地跨平臺操作,例如伺服器渲染、移動端開發等等
缺點:
- 無法進行極致優化: 在一些效能要求極高的應用中虛擬DOM無法進行鍼對性的極致優化,比如VScode採用直接手動操作DOM的方式進行極端的效能優化
虛擬DOM實現原理?
- 虛擬DOM本質上是JavaScript物件,是對真實DOM的抽象
- 狀態變更時,記錄新樹和舊樹的差異
- 最後把差異更新到真正的dom中
React最新的生命週期是怎樣的?
React 16之後有三個生命週期被廢棄(但並未刪除)
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
官方計劃在17版本完全刪除這三個函式,只保留UNSAVE_字首的三個函式,目的是為了向下相容,但是對於開發者而言應該儘量避免使用他們,而是使用新增的生命週期函式替代它們
目前React 16.8 +的生命週期分為三個階段,分別是掛載階段、更新階段、解除安裝階段
掛載階段:
- constructor: 建構函式,最先被執行,我們通常在建構函式裡初始化state物件或者給自定義方法繫結this
- getDerivedStateFromProps:
static getDerivedStateFromProps(nextProps, prevState)
,這是個靜態方法,當我們接收到新的屬性想去修改我們state,可以使用getDerivedStateFromProps - render: render函式是純函式,只返回需要渲染的東西,不應該包含其它的業務邏輯,可以返回原生的DOM、React元件、Fragment、Portals、字串和數字、Boolean和null等內容
- componentDidMount: 元件裝載之後呼叫,此時我們可以獲取到DOM節點並操作,比如對canvas,svg的操作,伺服器請求,訂閱都可以寫在這個裡面,但是記得在componentWillUnmount中取消訂閱
更新階段:
- getDerivedStateFromProps: 此方法在更新個掛載階段都可能會呼叫
- shouldComponentUpdate:
shouldComponentUpdate(nextProps, nextState)
,有兩個引數nextProps和nextState,表示新的屬性和變化之後的state,返回一個布林值,true表示會觸發重新渲染,false表示不會觸發重新渲染,預設返回true,我們通常利用此生命週期來優化React程式效能 - render: 更新階段也會觸發此生命週期
- getSnapshotBeforeUpdate:
getSnapshotBeforeUpdate(prevProps, prevState)
,這個方法在render之後,componentDidUpdate之前呼叫,有兩個引數prevProps和prevState,表示之前的屬性和之前的state,這個函式有一個返回值,會作為第三個引數傳給componentDidUpdate,如果你不想要返回值,可以返回null,此生命週期必須與componentDidUpdate搭配使用 - componentDidUpdate:
componentDidUpdate(prevProps, prevState, snapshot)
,該方法在getSnapshotBeforeUpdate方法之後被呼叫,有三個引數prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三個引數是getSnapshotBeforeUpdate返回的,如果觸發某些回撥函式時需要用到 DOM 元素的狀態,則將對比或計算的過程遷移至 getSnapshotBeforeUpdate,然後在 componentDidUpdate 中統一觸發回撥或更新狀態。
解除安裝階段:
- componentWillUnmount: 當我們的元件被解除安裝或者銷燬了就會呼叫,我們可以在這個函式裡去清除一些定時器,取消網路請求,清理無效的DOM元素等垃圾清理工作
一個檢視react生命週期的網站
React的請求應該放在哪個生命週期中?
React的非同步請求到底應該放在哪個生命週期裡,有人認為在componentWillMount
中可以提前進行非同步請求,避免白屏,其實這個觀點是有問題的.
由於JavaScript中非同步事件的性質,當您啟動API呼叫時,瀏覽器會在此期間返回執行其他工作。當React渲染一個元件時,它不會等待componentWillMount它完成任何事情 - React繼續前進並繼續render,沒有辦法“暫停”渲染以等待資料到達。
而且在componentWillMount
請求會有一系列潛在的問題,首先,在伺服器渲染時,如果在 componentWillMount 裡獲取資料,fetch data會執行兩次,一次在服務端一次在客戶端,這造成了多餘的請求,其次,在React 16進行React Fiber重寫後,componentWillMount
可能在一次渲染中多次呼叫.
目前官方推薦的非同步請求是在componentDidmount
中進行.
如果有特殊需求需要提前請求,也可以在特殊情況下在constructor
中請求:
react 17之後
componentWillMount
會被廢棄,僅僅保留UNSAFE_componentWillMount
setState到底是非同步還是同步?
先給出答案: 有時表現出非同步,有時表現出同步
setState
只在合成事件和鉤子函式中是“非同步”的,在原生事件和setTimeout
中都是同步的。setState
的“非同步”並不是說內部由非同步程式碼實現,其實本身執行的過程和程式碼都是同步的,只是合成事件和鉤子函式的呼叫順序在更新之前,導致在合成事件和鉤子函式中沒法立馬拿到更新後的值,形成了所謂的“非同步”,當然可以通過第二個引數setState(partialState, callback)
中的callback
拿到更新後的結果。setState
的批量更新優化也是建立在“非同步”(合成事件、鉤子函式)之上的,在原生事件和setTimeout 中不會批量更新,在“非同步”中如果對同一個值進行多次setState
,setState
的批量更新策略會對其進行覆蓋,取最後一次的執行,如果是同時setState
多個不同的值,在更新時會對其進行合併批量更新。
React元件通訊如何實現?
React元件間通訊方式:
- 父元件向子元件通訊: 父元件可以向子元件通過傳 props 的方式,向子元件進行通訊
- 子元件向父元件通訊: props+回撥的方式,父元件向子元件傳遞props進行通訊,此props為作用域為父元件自身的函式,子元件呼叫該函式,將子元件想要傳遞的資訊,作為引數,傳遞到父元件的作用域中
- 兄弟元件通訊: 找到這兩個兄弟節點共同的父節點,結合上面兩種方式由父節點轉發資訊進行通訊
- 跨層級通訊:
Context
設計目的是為了共享那些對於一個元件樹而言是“全域性”的資料,例如當前認證的使用者、主題或首選語言,對於跨越多層的全域性資料通過Context
通訊再適合不過 - 釋出訂閱模式: 釋出者釋出事件,訂閱者監聽事件並做出反應,我們可以通過引入event模組進行通訊
- 全域性狀態管理工具: 藉助Redux或者Mobx等全域性狀態管理工具進行通訊,這種工具會維護一個全域性狀態中心Store,並根據不同的事件產生新的狀態
React有哪些優化效能是手段?
效能優化的手段很多時候是通用的詳情見前端效能優化載入篇
React如何進行元件/邏輯複用?
拋開已經被官方棄用的Mixin,元件抽象的技術目前有三種比較主流:
- 高階元件:
- 屬性代理
- 反向繼承
- 渲染屬性
- react-hooks
元件複用詳解見元件複用
mixin、hoc、render props、react-hooks的優劣如何?
Mixin的缺陷:
-
元件與 Mixin 之間存在隱式依賴(Mixin 經常依賴元件的特定方法,但在定義元件時並不知道這種依賴關係)
-
多個 Mixin 之間可能產生衝突(比如定義了相同的state欄位)
-
Mixin 傾向於增加更多狀態,這降低了應用的可預測性(The more state in your application, the harder it is to reason about it.),導致複雜度劇增
-
隱式依賴導致依賴關係不透明,維護成本和理解成本迅速攀升:
-
難以快速理解元件行為,需要全盤瞭解所有依賴 Mixin 的擴充套件行為,及其之間的相互影響
-
組價自身的方法和state欄位不敢輕易刪改,因為難以確定有沒有 Mixin 依賴它
-
Mixin 也難以維護,因為 Mixin 邏輯最後會被打平合併到一起,很難搞清楚一個 Mixin 的輸入輸出
-
HOC相比Mixin的優勢:
- HOC通過外層元件通過 Props 影響內層元件的狀態,而不是直接改變其 State不存在衝突和互相干擾,這就降低了耦合度
- 不同於 Mixin 的打平+合併,HOC 具有天然的層級結構(元件樹結構),這又降低了複雜度
HOC的缺陷:
- 擴充套件性限制: HOC 無法從外部訪問子元件的 State因此無法通過shouldComponentUpdate濾掉不必要的更新,React 在支援 ES6 Class 之後提供了React.PureComponent來解決這個問題
- Ref 傳遞問題: Ref 被隔斷,後來的React.forwardRef 來解決這個問題
- Wrapper Hell: HOC可能出現多層包裹元件的情況,多層抽象同樣增加了複雜度和理解成本
- 命名衝突: 如果高階元件多次巢狀,沒有使用名稱空間的話會產生衝突,然後覆蓋老屬性
- 不可見性: HOC相當於在原有元件外層再包裝一個元件,你壓根不知道外層的包裝是啥,對於你是黑盒
Render Props優點:
- 上述HOC的缺點Render Props都可以解決
Render Props缺陷:
- 使用繁瑣: HOC使用只需要藉助裝飾器語法通常一行程式碼就可以進行復用,Render Props無法做到如此簡單
- 巢狀過深: Render Props雖然擺脫了元件多層巢狀的問題,但是轉化為了函式回撥的巢狀
React Hooks優點:
- 簡潔: React Hooks解決了HOC和Render Props的巢狀問題,更加簡潔
- 解耦: React Hooks可以更方便地把 UI 和狀態分離,做到更徹底的解耦
- 組合: Hooks 中可以引用另外的 Hooks形成新的Hooks,組合變化萬千
- 函式友好: React Hooks為函式元件而生,從而解決了類元件的幾大問題:
- this 指向容易錯誤
- 分割在不同宣告週期中的邏輯使得程式碼難以理解和維護
- 程式碼複用成本高(高階元件容易使程式碼量劇增)
React Hooks缺陷:
-
額外的學習成本(Functional Component 與 Class Component 之間的困惑)
-
寫法上有限制(不能出現在條件、迴圈中),並且寫法限制增加了重構成本
-
破壞了PureComponent、React.memo淺比較的效能優化效果(為了取最新的props和state,每次render()都要重新建立事件處函式)
-
在閉包場景可能會引用到舊的state、props值
-
內部實現上不直觀(依賴一份可變的全域性狀態,不再那麼“純”)
-
React.memo並不能完全替代shouldComponentUpdate(因為拿不到 state change,只針對 props change)
關於react-hooks的評價來源於官方react-hooks RFC
你是如何理解fiber的?
React Fiber 是一種基於瀏覽器的單執行緒排程演算法.
React 16之前 ,reconcilation
演算法實際上是遞迴,想要中斷遞迴是很困難的,React 16 開始使用了迴圈來代替之前的遞迴.
Fiber
:一種將 recocilation
(遞迴 diff),拆分成無數個小任務的演算法;它隨時能夠停止,恢復。停止恢復的時機取決於當前的一幀(16ms)內,還有沒有足夠的時間允許計算。
你對 Time Slice的理解?
時間分片
- React 在渲染(render)的時候,不會阻塞現在的執行緒
- 如果你的裝置足夠快,你會感覺渲染是同步的
- 如果你裝置非常慢,你會感覺還算是靈敏的
- 雖然是非同步渲染,但是你將會看到完整的渲染,而不是一個元件一行行的渲染出來
- 同樣書寫元件的方式
也就是說,這是React背後在做的事情,對於我們開發者來說,是透明的,具體是什麼樣的效果呢?
有圖表三個圖表,有一個輸入框,以及上面的三種模式這個元件非常的巨大,而且在輸入框每次**輸入東西的時候,就會進去一直在渲染。**為了更好的看到渲染的效能,Dan為我們做了一個表。
我們先看看,同步模式:
同步模式下,我們都知道,我們沒輸入一個字元,React就開始渲染,當React渲染一顆巨大的樹的時候,是非常卡的,所以才會有shouldUpdate的出現,在這裡Dan也展示了,這種卡!
我們再來看看第二種(Debounced模式):
Debounced模式簡單的來說,就是延遲渲染,比如,當你輸入完成以後,再開始渲染所有的變化。
這麼做的壞處就是,至少不會阻塞使用者的輸入了,但是依然有非常嚴重的卡頓。
切換到非同步模式:
非同步渲染模式就是不阻塞當前執行緒,繼續跑。在視訊裡可以看到所有的輸入,表上都會是原諒色的。
時間分片正是基於可隨時打斷、重啟的Fiber架構,可打斷當前任務,優先處理緊急且重要的任務,保證頁面的流暢執行.
redux的工作流程?
首先,我們看下幾個核心概念:
- Store:儲存資料的地方,你可以把它看成一個容器,整個應用只能有一個Store。
- State:Store物件包含所有資料,如果想得到某個時點的資料,就要對Store生成快照,這種時點的資料集合,就叫做State。
- Action:State的變化,會導致View的變化。但是,使用者接觸不到State,只能接觸到View。所以,State的變化必須是View導致的。Action就是View發出的通知,表示State應該要發生變化了。
- Action Creator:View要傳送多少種訊息,就會有多少種Action。如果都手寫,會很麻煩,所以我們定義一個函式來生成Action,這個函式就叫Action Creator。
- Reducer:Store收到Action以後,必須給出一個新的State,這樣View才會發生變化。這種State的計算過程就叫做Reducer。Reducer是一個函式,它接受Action和當前State作為引數,返回一個新的State。
- dispatch:是View發出Action的唯一方法。
然後我們過下整個工作流程:
- 首先,使用者(通過View)發出Action,發出方式就用到了dispatch方法。
- 然後,Store自動呼叫Reducer,並且傳入兩個引數:當前State和收到的Action,Reducer會返回新的State
- State一旦有變化,Store就會呼叫監聽函式,來更新View。
到這兒為止,一次使用者互動流程結束。可以看到,在整個流程中資料都是單向流動的,這種方式保證了流程的清晰。
react-redux是如何工作的?
- Provider: Provider的作用是從最外部封裝了整個應用,並向connect模組傳遞store
- connect: 負責連線React和Redux
- 獲取state: connect通過context獲取Provider中的store,通過store.getState()獲取整個store tree 上所有state
- 包裝原元件: 將state和action通過props的方式傳入到原元件內部wrapWithConnect返回一個ReactComponent物件Connect,Connect重新render外部傳入的原元件WrappedComponent,並把connect中傳入的mapStateToProps, mapDispatchToProps與元件上原有的props合併後,通過屬性的方式傳給WrappedComponent
- 監聽store tree變化: connect快取了store tree中state的狀態,通過當前state狀態和變更前state狀態進行比較,從而確定是否呼叫
this.setState()
方法觸發Connect及其子元件的重新渲染
redux與mobx的區別?
兩者對比:
- redux將資料儲存在單一的store中,mobx將資料儲存在分散的多個store中
- redux使用plain object儲存資料,需要手動處理變化後的操作;mobx適用observable儲存資料,資料變化後自動處理響應的操作
- redux使用不可變狀態,這意味著狀態是隻讀的,不能直接去修改它,而是應該返回一個新的狀態,同時使用純函式;mobx中的狀態是可變的,可以直接對其進行修改
- mobx相對來說比較簡單,在其中有很多的抽象,mobx更多的使用物件導向的程式設計思維;redux會比較複雜,因為其中的函數語言程式設計思想掌握起來不是那麼容易,同時需要藉助一系列的中介軟體來處理非同步和副作用
- mobx中有更多的抽象和封裝,除錯會比較困難,同時結果也難以預測;而redux提供能夠進行時間回溯的開發工具,同時其純函式以及更少的抽象,讓除錯變得更加的容易
場景辨析:
基於以上區別,我們可以簡單得分析一下兩者的不同使用場景.
mobx更適合資料不復雜的應用: mobx難以除錯,很多狀態無法回溯,面對複雜度高的應用時,往往力不從心.
redux適合有回溯需求的應用: 比如一個畫板應用、一個表格應用,很多時候需要撤銷、重做等操作,由於redux不可變的特性,天然支援這些操作.
mobx適合短平快的專案: mobx上手簡單,樣板程式碼少,可以很大程度上提高開發效率.
當然mobx和redux也並不一定是非此即彼的關係,你也可以在專案中用redux作為全域性狀態管理,用mobx作為元件區域性狀態管理器來用.
redux中如何進行非同步操作?
當然,我們可以在componentDidmount
中直接進行請求無須藉助redux.
但是在一定規模的專案中,上述方法很難進行非同步流的管理,通常情況下我們會藉助redux的非同步中介軟體進行非同步處理.
redux非同步流中介軟體其實有很多,但是當下主流的非同步中介軟體只有兩種redux-thunk、redux-saga,當然redux-observable可能也有資格佔據一席之地,其餘的非同步中介軟體不管是社群活躍度還是npm下載量都比較差了.
redux非同步中介軟體之間的優劣?
redux-thunk優點:
- 體積小: redux-thunk的實現方式很簡單,只有不到20行程式碼
- 使用簡單: redux-thunk沒有引入像redux-saga或者redux-observable額外的正規化,上手簡單
redux-thunk缺陷:
- 樣板程式碼過多: 與redux本身一樣,通常一個請求需要大量的程式碼,而且很多都是重複性質的
- 耦合嚴重: 非同步操作與redux的action偶合在一起,不方便管理
- 功能孱弱: 有一些實際開發中常用的功能需要自己進行封裝
redux-saga優點:
- 非同步解耦: 非同步操作被被轉移到單獨 saga.js 中,不再是摻雜在 action.js 或 component.js 中
- action擺脫thunk function: dispatch 的引數依然是一個純粹的 action (FSA),而不是充滿 “黑魔法” thunk function
- 異常處理: 受益於 generator function 的 saga 實現,程式碼異常/請求失敗 都可以直接通過 try/catch 語法直接捕獲處理
- 功能強大: redux-saga提供了大量的Saga 輔助函式和Effect 建立器供開發者使用,開發者無須封裝或者簡單封裝即可使用
- 靈活: redux-saga可以將多個Saga可以序列/並行組合起來,形成一個非常實用的非同步flow
- 易測試,提供了各種case的測試方案,包括mock task,分支覆蓋等等
redux-saga缺陷:
- 額外的學習成本: redux-saga不僅在使用難以理解的 generator function,而且有數十個API,學習成本遠超redux-thunk,最重要的是你的額外學習成本是隻服務於這個庫的,與redux-observable不同,redux-observable雖然也有額外學習成本但是背後是rxjs和一整套思想
- 體積龐大: 體積略大,程式碼近2000行,min版25KB左右
- 功能過剩: 實際上併發控制等功能很難用到,但是我們依然需要引入這些程式碼
- ts支援不友好: yield無法返回TS型別
redux-observable優點:
- 功能最強: 由於背靠rxjs這個強大的響應式程式設計的庫,藉助rxjs的操作符,你可以幾乎做任何你能想到的非同步處理
- 背靠rxjs: 由於有rxjs的加持,如果你已經學習了rxjs,redux-observable的學習成本並不高,而且隨著rxjs的升級redux-observable也會變得更強大
redux-observable缺陷:
- 學習成本奇高: 如果你不會rxjs,則需要額外學習兩個複雜的庫
- 社群一般: redux-observable的下載量只有redux-saga的1/5,社群也不夠活躍,在複雜非同步流中介軟體這個層面redux-saga仍處於領導地位
關於redux-saga與redux-observable的詳細比較可見此連結
公眾號
想要實時關注筆者最新的文章和最新的文件更新請關注公眾號程式設計師面試官,後續的文章會優先在公眾號更新.
簡歷模板: 關注公眾號回覆「模板」獲取
《前端面試手冊》: 配套於本指南的突擊手冊,關注公眾號回覆「fed」獲取