2021高頻前端面試題匯總之React篇
React視頻教程系列
React 實戰:CNode視頻教程
完整教程目錄:點擊查看
1. React 事件機制
<div onClick={this.handleClick.bind(this)}>點我</div>
復制代碼
React並不是將click事件綁定到了div的真實DOM上,而是在document處監聽了所有的事件,當事件發生並且冒泡到document處的時候,React將事件內容封裝並交由真正的處理函數運行。這樣的方式不僅僅減少了內存的消耗,還能在組件掛在銷毀時統一訂閱和移除事件。
除此之外,冒泡到document上的事件也不是原生的瀏覽器事件,而是由react自己實現的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的話應該調用event.preventDefault()方法,而不是調用event.stopProppagation()方法。 JSX 上寫的事件並沒有綁定在對應的真實 DOM 上,而是通過事件代理的方式,將所有的事件都統一綁定在了
document
上。這樣的方式不僅減少了內存消耗,還能在組件掛載銷毀時統一訂閱和移除事件。
另外冒泡到 document
上的事件也不是原生瀏覽器事件,而是 React 自己實現的合成事件(SyntheticEvent)。因此我們如果不想要事件冒泡的話,調用 event.stopPropagation
是無效的,而應該調用 event.preventDefault
。
實現合成事件的目的如下:
- 合成事件首先抹平了瀏覽器之間的兼容問題,另外這是一個跨瀏覽器原生事件包裝器,賦予了跨瀏覽器開發的能力;
- 對於原生瀏覽器事件來說,瀏覽器會給監聽器創建一個事件對象。如果你有很多的事件監聽,那么就需要分配很多的事件對象,造成高額的內存分配問題。但是對於合成事件來說,有一個事件池專門來管理它們的創建和銷毀,當事件需要被使用時,就會從池子中復用對象,事件回調結束后,就會銷毀事件對象上的屬性,從而便於下次復用事件對象。
2. React 高階組件、Render props、hooks 有什么區別,為什么要不斷迭代
這三者是目前react解決代碼復用的主要方式:
- 高階組件(HOC)是 React 中用於復用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分,它是一種基於 React 的組合特性而形成的設計模式。具體而言,高階組件是參數為組件,返回值為新組件的函數。
- render props是指一種在 React 組件之間使用一個值為函數的 prop 共享代碼的簡單技術,更具體的說,render prop 是一個用於告知組件需要渲染什么內容的函數 prop。
- 通常,render props 和高階組件只渲染一個子節點。讓 Hook 來服務這個使用場景更加簡單。這兩種模式仍有用武之地,(例如,一個虛擬滾動條組件或許會有一個 renderltem 屬性,或是一個可見的容器組件或許會有它自己的 DOM 結構)。但在大部分場景下,Hook 足夠了,並且能夠幫助減少嵌套。
(1)HOC 官方解釋∶
高階組件(HOC)是 React 中用於復用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分,它是一種基於 React 的組合特性而形成的設計模式。
簡言之,HOC是一種組件的設計模式,HOC接受一個組件和額外的參數(如果需要),返回一個新的組件。HOC 是純函數,沒有副作用。
// hoc的定義
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
data: selectData(DataSource, props)
};
}
// 一些通用的邏輯處理
render() {
// ... 並使用新數據渲染被包裝的組件!
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
// 使用
const BlogPostWithSubscription = withSubscription(BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id));
復制代碼
HOC的優缺點∶
- 優點∶ 邏輯服用、不影響被包裹組件的內部邏輯。
- 缺點∶ hoc傳遞給被包裹組件的props容易和被包裹后的組件重名,進而被覆蓋
(2)Render props 官方解釋∶
"render prop"是指一種在 React 組件之間使用一個值為函數的 prop 共享代碼的簡單技術
具有render prop 的組件接受一個返回React元素的函數,將render的渲染邏輯注入到組件內部。在這里,"render"的命名可以是任何其他有效的標識符。
// DataProvider組件內部的渲染邏輯如下
class DataProvider extends React.Components {
state = {
name: 'Tom'
}
render() {
return (
<div>
<p>共享數據組件自己內部的渲染邏輯</p>
{ this.props.render(this.state) }
</div>
);
}
}
// 調用方式
<DataProvider render={data => (
<h1>Hello {data.name}</h1>
)}/>
復制代碼
由此可以看到,render props的優缺點也很明顯∶
- 優點:數據共享、代碼復用,將組件內的state作為props傳遞給調用者,將渲染邏輯交給調用者。
- 缺點:無法在 return 語句外訪問數據、嵌套寫法不夠優雅
(3)Hooks 官方解釋∶
Hook是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。通過自定義hook,可以復用代碼邏輯。
// 自定義一個獲取訂閱數據的hook
function useSubscription() {
const data = DataSource.getComments();
return [data];
}
//
function CommentList(props) {
const {data} = props;
const [subData] = useSubscription();
...
}
// 使用
<CommentList data='hello' />
復制代碼
以上可以看出,hook解決了hoc的prop覆蓋的問題,同時使用的方式解決了render props的嵌套地獄的問題。hook的優點如下∶
- 使用直觀;
- 解決hoc的prop 重名問題;
- 解決render props 因共享數據 而出現嵌套地獄的問題;
- 能在return之外使用數據的問題。
需要注意的是:hook只能在組件頂層使用,不可在分支語句中使用。
總結∶ Hoc、render props和hook都是為了解決代碼復用的問題,但是hoc和render props都有特定的使用場景和明顯的缺點。hook是react16.8更新的新的API,讓組件邏輯復用更簡潔明了,同時也解決了hoc和render props的一些缺點。
3. React.Component 和 React.PureComponent 的區別
PureComponent表示一個純組件,可以用來優化React程序,減少render函數執行的次數,從而提高組件的性能。
在React中,當prop或者state發生變化時,可以通過在shouldComponentUpdate生命周期函數中執行return false來阻止頁面的更新,從而減少不必要的render執行。React.PureComponent會自動執行 shouldComponentUpdate。
不過,pureComponent中的 shouldComponentUpdate() 進行的是淺比較,也就是說如果是引用數據類型的數據,只會比較不是同一個地址,而不會比較這個地址里面的數據是否一致。淺比較會忽略屬性和或狀態突變情況,其實也就是數據引用指針沒有變化,而數據發生改變的時候render是不會執行的。如果需要重新渲染那么就需要重新開辟空間引用數據。PureComponent一般會用在一些純展示組件上。
使用pureComponent的好處:當組件更新時,如果組件的props或者state都沒有改變,render函數就不會觸發。省去虛擬DOM的生成和對比過程,達到提升性能的目的。這是因為react自動做了一層淺比較。
4. Redux 中異步的請求怎么處理
可以在 componentDidmount 中直接進⾏請求⽆須借助redux。但是在⼀定規模的項⽬中,上述⽅法很難進⾏異步流的管理,通常情況下我們會借助redux的異步中間件進⾏異步處理。redux異步流中間件其實有很多,當下主流的異步中間件有兩種redux-thunk、redux-saga。
(1)使用react-thunk中間件
redux-thunk優點:
- 體積⼩: redux-thunk的實現⽅式很簡單,只有不到20⾏代碼
- 使⽤簡單: redux-thunk沒有引⼊像redux-saga或者redux-observable額外的范式,上⼿簡單
redux-thunk缺陷:
- 樣板代碼過多: 與redux本身⼀樣,通常⼀個請求需要⼤量的代碼,⽽且很多都是重復性質的
- 耦合嚴重: 異步操作與redux的action偶合在⼀起,不⽅便管理
- 功能孱弱: 有⼀些實際開發中常⽤的功能需要⾃⼰進⾏封裝
使用步驟:
- 配置中間件,在store的創建中配置
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk'
// 設置調試工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// 設置中間件
const enhancer = composeEnhancers(
applyMiddleware(thunk)
);
const store = createStore(reducer, enhancer);
export default store;
復制代碼
- 添加一個返回函數的actionCreator,將異步請求邏輯放在里面
/**
發送get請求,並生成相應action,更新store的函數
@param url {string} 請求地址
@param func {function} 真正需要生成的action對應的actionCreator
@return {function}
*/
// dispatch為自動接收的store.dispatch函數
export const getHttpAction = (url, func) => (dispatch) => {
axios.get(url).then(function(res){
const action = func(res.data)
dispatch(action)
})
}
復制代碼
- 生成action,並發送action
componentDidMount(){
var action = getHttpAction('/getData', getInitTodoItemAction)
// 發送函數類型的action時,該action的函數體會自動執行
store.dispatch(action)
}
復制代碼
(2)使用redux-saga中間件
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-saga可以捕獲action,然后執行一個函數,那么可以把異步代碼放在這個函數中,使用步驟如下:
- 配置中間件
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga'
import TodoListSaga from './sagas'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const sagaMiddleware = createSagaMiddleware()
const enhancer = composeEnhancers(
applyMiddleware(sagaMiddleware)
);
const store = createStore(reducer, enhancer);
sagaMiddleware.run(TodoListSaga)
export default store;
復制代碼
- 將異步請求放在sagas.js中
import {takeEvery, put} from 'redux-saga/effects'
import {initTodoList} from './actionCreator'
import {GET_INIT_ITEM} from './actionTypes'
import axios from 'axios'
function* func(){
try{
// 可以獲取異步返回數據
const res = yield axios.get('/getData')
const action = initTodoList(res.data)
// 將action發送到reducer
yield put(action)
}catch(e){
console.log('網絡請求失敗')
}
}
function* mySaga(){
// 自動捕獲GET_INIT_ITEM類型的action,並執行func
yield takeEvery(GET_INIT_ITEM, func)
}
export default mySaga
復制代碼
- 發送action
componentDidMount(){
const action = getInitTodoItemAction()
store.dispatch(action)
}
5. Redux 中間件是什么?接受幾個參數?柯里化函數兩端的參數具體是什么?
Redux 的中間件提供的是位於 action 被發起之后,到達 reducer 之前的擴展點,換而言之,原本 view -→> action -> reducer -> store 的數據流加上中間件后變成了 view -> action -> middleware -> reducer -> store ,在這一環節可以做一些"副作用"的操作,如異步請求、打印日志等。
applyMiddleware源碼:
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 利用傳入的createStore和reducer和創建一個store
const store = createStore(...args)
let dispatch = () => {
throw new Error()
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 讓每個 middleware 帶着 middlewareAPI 這個參數分別執行一遍
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 接着 compose 將 chain 中的所有匿名函數,組裝成一個新的函數,即新的 dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
復制代碼
從applyMiddleware中可以看出∶
- redux中間件接受一個對象作為參數,對象的參數上有兩個字段 dispatch 和 getState,分別代表着 Redux Store 上的兩個同名函數。
- 柯里化函數兩端一個是 middewares,一個是store.dispatch
6. 對 React Hook 的理解,它的實現原理是什么
React-Hooks 是 React 團隊在 React 組件開發實踐中,逐漸認知到的一個改進點,這背后其實涉及對類組件和函數組件兩種組件形式的思考和側重。
(1)類組件: 所謂類組件,就是基於 ES6 Class 這種寫法,通過繼承 React.Component 得來的 React 組件。以下是一個類組件:
class DemoClass extends React.Component {
state = {
text: ""
};
componentDidMount() {
//...
}
changeText = (newText) => {
this.setState({
text: newText
});
};
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>修改</button>
</div>
);
}
}
復制代碼
可以看出,React 類組件內部預置了相當多的“現成的東西”等着我們去調度/定制,state 和生命周期就是這些“現成東西”中的典型。要想得到這些東西,難度也不大,只需要繼承一個 React.Component 即可。
當然,這也是類組件的一個不便,它太繁雜了,對於解決許多問題來說,編寫一個類組件實在是一個過於復雜的姿勢。復雜的姿勢必然帶來高昂的理解成本,這也是我們所不想看到的。除此之外,由於開發者編寫的邏輯在封裝后是和組件粘在一起的,這就使得類組件內部的邏輯難以實現拆分和復用。
(2)函數組件:函數組件就是以函數的形態存在的 React 組件。早期並沒有 React-Hooks,函數組件內部無法定義和維護 state,因此它還有一個別名叫“無狀態組件”。以下是一個函數組件:
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`函數組件接收的內容:[${text}]`}</p>
</div>
);
}
復制代碼
相比於類組件,函數組件肉眼可見的特質自然包括輕量、靈活、易於組織和維護、較低的學習成本等。
通過對比,從形態上可以對兩種組件做區分,它們之間的區別如下:
- 類組件需要繼承 class,函數組件不需要;
- 類組件可以訪問生命周期方法,函數組件不能;
- 類組件中可以獲取到實例化后的 this,並基於這個 this 做各種各樣的事情,而函數組件不可以;
- 類組件中可以定義並維護 state(狀態),而函數組件不可以;
除此之外,還有一些其他的不同。通過上面的區別,我們不能說誰好誰壞,它們各有自己的優勢。在 React-Hooks 出現之前,類組件的能力邊界明顯強於函數組件。
實際上,類組件和函數組件之間,是面向對象和函數式編程這兩套不同的設計思想之間的差異。而函數組件更加契合 React 框架的設計理念: React 組件本身的定位就是函數,一個輸入數據、輸出 UI 的函數。作為開發者,我們編寫的是聲明式的代碼,而 React 框架的主要工作,就是及時地把聲明式的代碼轉換為命令式的 DOM 操作,把數據層面的描述映射到用戶可見的 UI 變化中去。這就意味着從原則上來講,React 的數據應該總是緊緊地和渲染綁定在一起的,而類組件做不到這一點。函數組件就真正地將數據和渲染綁定到了一起。函數組件是一個更加匹配其設計理念、也更有利於邏輯拆分與重用的組件表達形式。
為了能讓開發者更好的的去編寫函數式組件。於是,React-Hooks 便應運而生。
React-Hooks 是一套能夠使函數組件更強大、更靈活的“鈎子”。
函數組件比起類組件少了很多東西,比如生命周期、對 state 的管理等。這就給函數組件的使用帶來了非常多的局限性,導致我們並不能使用函數這種形式,寫出一個真正的全功能的組件。而React-Hooks 的出現,就是為了幫助函數組件補齊這些(相對於類組件來說)缺失的能力。
如果說函數組件是一台輕巧的快艇,那么 React-Hooks 就是一個內容豐富的零部件箱。“重裝戰艦”所預置的那些設備,這個箱子里基本全都有,同時它還不強制你全都要,而是允許你自由地選擇和使用你需要的那些能力,然后將這些能力以 Hook(鈎子)的形式“鈎”進你的組件里,從而定制出一個最適合你的“專屬戰艦”。