React 靈魂 23 問,你能答對幾個?
原文鏈接:https://zhuanlan.zhihu.com/p/304213203
1、setState 是異步還是同步?
合成事件中是異步 鉤子函數(shù)中的是異步 原生事件中是同步 setTimeout 中是同步
相關(guān)鏈接:你真的理解 setState 嗎?[1]
2、聊聊 [email protected] + 的生命周期

相關(guān)連接:React 生命周期[2]我對 React v16.4 生命周期的理解[3]
3、useEffect(fn, []) 和 componentDidMount 有什么差異?
useEffect 會捕獲 props 和 state。所以即便在回調(diào)函數(shù)里,你拿到的還是初始的 props 和 state。如果想得到“最新”的值,可以使用 ref。
4、hooks 為什么不能放在條件判斷里?
以 setState 為例,在 react 內(nèi)部,每個組件(Fiber)的 hooks 都是以鏈表的形式存在 memoizeState 屬性中:

update 階段,每次調(diào)用 setState,鏈表就會執(zhí)行 next 向后移動一步。如果將 setState 寫在條件判斷中,假設(shè)條件判斷不成立,沒有執(zhí)行里面的 setState 方法,會導(dǎo)致接下來所有的 setState 的取值出現(xiàn)偏移,從而導(dǎo)致異常發(fā)生。
參考鏈接:烤透 React Hook[4]
5、fiber 是什么?
React Fiber 是一種基于瀏覽器的單線程調(diào)度算法。
React Fiber 用類似 requestIdleCallback 的機制來做異步 diff。但是之前數(shù)據(jù)結(jié)構(gòu)不支持這樣的實現(xiàn)異步 diff,于是 React 實現(xiàn)了一個類似鏈表的數(shù)據(jù)結(jié)構(gòu),將原來的 遞歸 diff 變成了現(xiàn)在的 遍歷 diff,這樣就能做到異步可更新了。

相關(guān)鏈接:React Fiber 是什么?[5]
6、聊一聊 diff 算法
傳統(tǒng) diff 算法的時間復(fù)雜度是 O(n^3),這在前端 render 中是不可接受的。為了降低時間復(fù)雜度,react 的 diff 算法做了一些妥協(xié),放棄了最優(yōu)解,最終將時間復(fù)雜度降低到了 O(n)。
那么 react diff 算法做了哪些妥協(xié)呢?,參考如下:
1、tree diff:只對比同一層的 dom 節(jié)點,忽略 dom 節(jié)點的跨層級移動
如下圖,react 只會對相同顏色方框內(nèi)的 DOM 節(jié)點進行比較,即同一個父節(jié)點下的所有子節(jié)點。當(dāng)發(fā)現(xiàn)節(jié)點不存在時,則該節(jié)點及其子節(jié)點會被完全刪除掉,不會用于進一步的比較。
這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。

這就意味著,如果 dom 節(jié)點發(fā)生了跨層級移動,react 會刪除舊的節(jié)點,生成新的節(jié)點,而不會復(fù)用。
2、component diff:如果不是同一類型的組件,會刪除舊的組件,創(chuàng)建新的組件

3、element diff:對于同一層級的一組子節(jié)點,需要通過唯一 id 進行來區(qū)分
如果沒有 id 來進行區(qū)分,一旦有插入動作,會導(dǎo)致插入位置之后的列表全部重新渲染。
這也是為什么渲染列表時為什么要使用唯一的 key。
7、調(diào)用 setState 之后發(fā)生了什么?
在 setState的時候,React 會為當(dāng)前節(jié)點創(chuàng)建一個updateQueue的更新列隊。然后會觸發(fā) reconciliation過程,在這個過程中,會使用名為 Fiber 的調(diào)度算法,開始生成新的 Fiber 樹, Fiber 算法的最大特點是可以做到異步可中斷的執(zhí)行。然后 React Scheduler會根據(jù)優(yōu)先級高低,先執(zhí)行優(yōu)先級高的節(jié)點,具體是執(zhí)行doWork方法。在 doWork方法中,React 會執(zhí)行一遍updateQueue中的方法,以獲得新的節(jié)點。然后對比新舊節(jié)點,為老節(jié)點打上 更新、插入、替換 等 Tag。當(dāng)前節(jié)點 doWork完成后,會執(zhí)行performUnitOfWork方法獲得新節(jié)點,然后再重復(fù)上面的過程。當(dāng)所有節(jié)點都 doWork完成后,會觸發(fā)commitRoot方法,React 進入 commit 階段。在 commit 階段中,React 會根據(jù)前面為各個節(jié)點打的 Tag,一次性更新整個 dom 元素。
8、為什么虛擬 dom 會提高性能?
虛擬 dom 相當(dāng)于在 JS 和真實 dom 中間加了一個緩存,利用 diff 算法避免了沒有必要的 dom 操作,從而提高性能。
9、錯誤邊界是什么?它有什么用?
在 React 中,如果任何一個組件發(fā)生錯誤,它將破壞整個組件樹,導(dǎo)致整頁白屏。這時候我們可以用錯誤邊界優(yōu)雅地降級處理這些錯誤。
例如下面封裝的組件:
class?ErrorBoundary?extends?React.Component<IProps,?IState>?{
??constructor(props:?IProps)?{
????super(props);
????this.state?=?{?hasError:?false?};
??}
??static?getDerivedStateFromError()?{
????//?更新?state?使下一次渲染能夠顯示降級后的?UI
????return?{?hasError:?true?};
??}
??componentDidCatch(error,?errorInfo)?{
????//?可以將錯誤日志上報給服務(wù)器
????console.log('組件奔潰?Error',?error);
????console.log('組件奔潰?Info',?errorInfo);
??}
??render()?{
????if?(this.state.hasError)?{
??????//?你可以自定義降級后的?UI?并渲染
??????return?this.props.content;
????}
????return?this.props.children;
??}
}
10、什么是 Portals?
Portal 提供了一種將子節(jié)點渲染到存在于父組件以外的 DOM 節(jié)點的優(yōu)秀的方案。
ReactDOM.createPortal(child,?container)
11、React 組件間有那些通信方式?
父組件向子組件通信
1、 通過 props 傳遞
子組件向父組件通信
1、 主動調(diào)用通過 props 傳過來的方法,并將想要傳遞的信息,作為參數(shù),傳遞到父組件的作用域中
跨層級通信
1、 使用 react 自帶的 Context 進行通信,createContext 創(chuàng)建上下文, useContext 使用上下文。
參考下面代碼:
??import?React,?{?createContext,?useContext?}?from?'react';
??const?themes?=?{
????light:?{
??????foreground:?"#000000",
??????background:?"#eeeeee"
????},
????dark:?{
??????foreground:?"#ffffff",
??????background:?"#222222"
????}
??};
??const?ThemeContext?=?createContext(themes.light);
??function?App()?{
????return?(
??????<ThemeContext.Provider?value={themes.dark}>
????????<Toolbar?/>
??????ThemeContext.Provider>
????);
??}
??function?Toolbar()?{
????return?(
??????<div>
????????<ThemedButton?/>
??????div>
????);
??}
??function?ThemedButton()?{
????const?theme?=?useContext(ThemeContext);
????return?(
??????<button?style={{?background:?theme.background,?color:?theme.foreground?}}>
????????I?am?styled?by?theme?context!
??????button>
????);
??}
??export?default?App;
2、使用 Redux 或者 Mobx 等狀態(tài)管理庫
3、使用訂閱發(fā)布模式
相關(guān)鏈接:React Docs[6]
12、React 父組件如何調(diào)用子組件中的方法?
1、如果是在方法組件中調(diào)用子組件(>= [email protected]),可以使用 useRef 和 useImperativeHandle:
const?{?forwardRef,?useRef,?useImperativeHandle?}?=?React;
const?Child?=?forwardRef((props,?ref)?=>?{
??useImperativeHandle(ref,?()?=>?({
????getAlert()?{
??????alert("getAlert?from?Child");
????}
??}));
??return?<h1>Hih1>;
});
const?Parent?=?()?=>?{
??const?childRef?=?useRef();
??return?(
????<div>
??????<Child?ref={childRef}?/>
??????<button?onClick={()?=>?childRef.current.getAlert()}>Clickbutton>
????div>
??);
};
2、如果是在類組件中調(diào)用子組件(>= [email protected]),可以使用 createRef:
const?{?Component?}?=?React;
class?Parent?extends?Component?{
??constructor(props)?{
????super(props);
????this.child?=?React.createRef();
??}
??onClick?=?()?=>?{
????this.child.current.getAlert();
??};
??render()?{
????return?(
??????<div>
????????<Child?ref={this.child}?/>
????????<button?onClick={this.onClick}>Clickbutton>
??????div>
????);
??}
}
class?Child?extends?Component?{
??getAlert()?{
????alert('getAlert?from?Child');
??}
??render()?{
????return?<h1>Helloh1>;
??}
}
參考閱讀:Call child method from parent[7]
13、React 有哪些優(yōu)化性能的手段?
類組件中的優(yōu)化手段
1、使用純組件 PureComponent 作為基類。
2、使用 React.memo 高階函數(shù)包裝組件。
3、使用 shouldComponentUpdate 生命周期函數(shù)來自定義渲染邏輯。
方法組件中的優(yōu)化手段
1、使用 useMemo。
2、使用 useCallBack。
其他方式
1、在列表需要頻繁變動時,使用唯一 id 作為 key,而不是數(shù)組下標(biāo)。
2、必要時通過改變 CSS 樣式隱藏顯示組件,而不是通過條件判斷顯示隱藏組件。
3、使用 Suspense 和 lazy 進行懶加載,例如:
import?React,?{?lazy,?Suspense?}?from?"react";
export?default?class?CallingLazyComponents?extends?React.Component?{
??render()?{
????var?ComponentToLazyLoad?=?null;
????if?(this.props.name?==?"Mayank")?{
??????ComponentToLazyLoad?=?lazy(()?=>?import("./mayankComponent"));
????}?else?if?(this.props.name?==?"Anshul")?{
??????ComponentToLazyLoad?=?lazy(()?=>?import("./anshulComponent"));
????}
????return?(
??????<div>
????????<h1>This?is?the?Base?User:?{this.state.name}h1>
????????<Suspense?fallback={<div>Loading...div>}>
??????????<ComponentToLazyLoad?/>
????????Suspense>
??????div>
????)
??}
}
Suspense 用法可以參考官方文檔[8]
相關(guān)閱讀:21 個 React 性能優(yōu)化技巧[9]
14、為什么 React 元素有一個 $$typeof 屬性?

目的是為了防止 XSS 攻擊。因為 Synbol 無法被序列化,所以 React 可以通過有沒有 $$typeof 屬性來斷出當(dāng)前的 element 對象是從數(shù)據(jù)庫來的還是自己生成的。
如果沒有 $$typeof 這個屬性,react 會拒絕處理該元素。
在 React 的古老版本中,下面的寫法會出現(xiàn) XSS 攻擊:
//?服務(wù)端允許用戶存儲?JSON
let?expectedTextButGotJSON?=?{
??type:?'div',
??props:?{
????dangerouslySetInnerHTML:?{
??????__html:?'/*?把你想的擱著?*/'
????},
??},
??//?...
};
let?message?=?{?text:?expectedTextButGotJSON?};
//?React?0.13?中有風(fēng)險
<p>
??{message.text}
p>
相關(guān)閱讀:Dan Abramov Blog[10]
15、React 如何區(qū)分 Class 組件 和 Function 組件?
一般的方式是借助 typeof 和 Function.prototype.toString 來判斷當(dāng)前是不是 class,如下:
function?isClass(func)?{
??return?typeof?func?===?'function'
????&&?/^class\s/.test(Function.prototype.toString.call(func));
}
但是這個方式有它的局限性,因為如果用了 babel 等轉(zhuǎn)換工具,將 class 寫法全部轉(zhuǎn)為 function 寫法,上面的判斷就會失效。
React 區(qū)分 Class 組件 和 Function 組件的方式很巧妙,由于所有的類組件都要繼承 React.Component,所以只要判斷原型鏈上是否有 React.Component 就可以了:
AComponent.prototype?instanceof?React.Component
相關(guān)閱讀:Dan Abramov Blog[11]
16、HTML 和 React 事件處理有什么區(qū)別?
在 HTML 中事件名必須小寫:
<button?onclick='activateLasers()'>
而在 React 中需要遵循駝峰寫法:
<button?onClick={activateLasers}>
在 HTML 中可以返回 false 以阻止默認(rèn)的行為:
?<a?href='#'?onclick='console.log("The?link?was?clicked.");?return?false;'?/>
而在 React 中必須地明確地調(diào)用 preventDefault():
function?handleClick(event)?{
??event.preventDefault()
??console.log('The?link?was?clicked.')
}
17、什么是 suspense 組件?
Suspense 讓組件“等待”某個異步操作,直到該異步操作結(jié)束即可渲染。在下面例子中,兩個組件都會等待異步 API 的返回值:
const?resource?=?fetchProfileData();
function?ProfilePage()?{
??return?(
????<Suspense?fallback={<h1>Loading?profile...h1>}>
??????<ProfileDetails?/>
??????<Suspense?fallback={<h1>Loading?posts...h1>}>
????????<ProfileTimeline?/>
??????Suspense>
????Suspense>
??);
}
function?ProfileDetails()?{
??//?嘗試讀取用戶信息,盡管該數(shù)據(jù)可能尚未加載
??const?user?=?resource.user.read();
??return?<h1>{user.name}h1>;
}
function?ProfileTimeline()?{
??//?嘗試讀取博文信息,盡管該部分?jǐn)?shù)據(jù)可能尚未加載
??const?posts?=?resource.posts.read();
??return?(
????<ul>
??????{posts.map(post?=>?(
????????<li?key={post.id}>{post.text}li>
??????))}
????ul>
??);
}
Suspense 也可以用于懶加載,參考下面的代碼:
const?OtherComponent?=?React.lazy(()?=>?import('./OtherComponent'));
function?MyComponent()?{
??return?(
????<div>
??????<Suspense?fallback={<div>Loading...div>}>
????????<OtherComponent?/>
??????Suspense>
????div>
??);
}
18、為什么 JSX 中的組件名要以大寫字母開頭?
因為 React 要知道當(dāng)前渲染的是組件還是 HTML 元素。
19、redux 是什么?
Redux 是一個為 JavaScript 應(yīng)用設(shè)計的,可預(yù)測的狀態(tài)容器。
它解決了如下問題:
跨層級組件之間的數(shù)據(jù)傳遞變得很容易 所有對狀態(tài)的改變都需要 dispatch,使得整個數(shù)據(jù)的改變可追蹤,方便排查問題。
但是它也有缺點:
概念偏多,理解起來不容易 樣板代碼太多
20、react-redux 的實現(xiàn)原理?
通過 redux 和 react context 配合使用,并借助高階函數(shù),實現(xiàn)了 react-redux。
參考鏈接:React.js 小書[12]
21、reudx 和 mobx 的區(qū)別?
得益于 Mobx 的 observable,使用 mobx 可以做到精準(zhǔn)更新;對應(yīng)的 Redux 是用 dispath 進行廣播,通過 Provider 和 connect 來比對前后差別控制更新粒度;
相關(guān)閱讀:Redux or MobX: An attempt to dissolve the Confusion[13]
22、redux 異步中間件有什么什么作用?
假如有這樣一個需求:請求數(shù)據(jù)前要向 Store dispatch 一個 loading 狀態(tài),并帶上一些信息;請求結(jié)束后再向 Store dispatch 一個 loaded 狀態(tài)
一些同學(xué)可能會這樣做:
function?App()?{
??const?onClick?=?()?=>?{
????dispatch({?type:?'LOADING',?message:?'data?is?loading'?})
????fetch('dataurl').then(()?=>?{
??????dispatch({?type:?'LOADED'?})
????});
??}
??return?(<div>
????<button?onClick={onClick}>clickbutton>
??div>);
}
但是如果有非常多的地方用到這塊邏輯,那應(yīng)該怎么辦?
聰明的同學(xué)會想到可以將 onClick 里的邏輯抽象出來復(fù)用,如下:
function?fetchData(message:?string)?{
??return?(dispatch)?=>?{
????dispatch({?type:?'LOADING',?message?})
????setTimeout(()?=>?{
??????dispatch({?type:?'LOADED'?})
????},?1000)
??}
}
function?App()?{
??const?onClick?=?()?=>?{
????fetchData('data?is?loading')(dispatch)
??}
??return?(<div>
????<button?onClick={onClick}>clickbutton>
??div>);
}
很好,但是 fetchData('data is loading')(dispatch) 這種寫法有點奇怪,會增加開發(fā)者的心智負(fù)擔(dān)。
于是可以借助 rudux 相關(guān)的異步中間件,以 rudux-chunk 為例,將寫法改為如下:
function?fetchData(message:?string)?{
??return?(dispatch)?=>?{
????dispatch({?type:?'LOADING',?message?})
????setTimeout(()?=>?{
??????dispatch({?type:?'LOADED'?})
????},?1000)
??}
}
function?App()?{
??const?onClick?=?()?=>?{
-???fetchData('data?is?loading')(dispatch)
+???dispatch(fetchData('data?is?loading'))
??}
??return?(<div>
????<button?onClick={onClick}>clickbutton>
??div>);
}
這樣就更符合認(rèn)知一些了,redux 異步中間件沒有什么奧秘,主要做的就是這樣的事情。
相關(guān)閱讀:Why do we need middleware for async flow in Redux?[14]
23、redux 有哪些異步中間件?
1、redux-thunk
源代碼簡短優(yōu)雅,上手簡單
2、redux-saga[15]
借助 JS 的 generator 來處理異步,避免了回調(diào)的問題
3、redux-observable[16]
借助了 RxJS 流的思想以及其各種強大的操作符,來處理異步問題
覺得不錯可以點擊這個 repo[17] 關(guān)注更多內(nèi)容。
參考資料
你真的理解 setState 嗎?: https://link.zhihu.com/?target=https%3A//juejin.im/post/6844903636749778958
[2]React 生命周期: https://link.zhihu.com/?target=https%3A//projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
[3]我對 React v16.4 生命周期的理解: https://link.zhihu.com/?target=https%3A//juejin.im/post/6844903655372488712
[4]烤透 React Hook: https://link.zhihu.com/?target=https%3A//juejin.im/post/6867745889184972814
[5]React Fiber 是什么?: https://link.zhihu.com/?target=https%3A//github.com/WangYuLue/react-in-deep/blob/main/02.React%2520Fiber%2520%25E6%2598%25AF%25E4%25BB%2580%25E4%25B9%2588%25EF%25BC%259F.md
[6]React Docs: https://link.zhihu.com/?target=https%3A//zh-hans.reactjs.org/docs/hooks-reference.html%23usecontext
[7]Call child method from parent: https://link.zhihu.com/?target=https%3A//stackoverflow.com/questions/37949981/call-child-method-from-parent
[8]官方文檔: https://link.zhihu.com/?target=https%3A//zh-hans.reactjs.org/docs/concurrent-mode-suspense.html
[9]21 個 React 性能優(yōu)化技巧: https://link.zhihu.com/?target=https%3A//www.infoq.cn/article/KVE8xtRs-uPphptq5LUz
[10]Dan Abramov Blog: https://link.zhihu.com/?target=https%3A//overreacted.io/zh-hans/why-do-react-elements-have-typeof-property/
[11]Dan Abramov Blog: https://link.zhihu.com/?target=https%3A//overreacted.io/zh-hans/how-does-react-tell-a-class-from-a-function/
[12]React.js 小書: https://link.zhihu.com/?target=http%3A//huziketang.mangojuice.top/books/react/lesson36
[13]Redux or MobX: An attempt to dissolve the Confusion: https://link.zhihu.com/?target=https%3A//segmentfault.com/a/1190000011148981
[14]Why do we need middleware for async flow in Redux?: https://link.zhihu.com/?target=https%3A//stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux
[15]redux-saga: https://link.zhihu.com/?target=https%3A//redux-saga-in-chinese.js.org/
[16]redux-observable: https://link.zhihu.com/?target=https%3A//redux-observable.js.org/docs/basics/Epics.html
[17]repo: https://link.zhihu.com/?target=https%3A//github.com/WangYuLue/ecode-frontend-cards
推薦閱讀
