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

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

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

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

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

3、element diff:對(duì)于同一層級(jí)的一組子節(jié)點(diǎn),需要通過(guò)唯一 id 進(jìn)行來(lái)區(qū)分
如果沒(méi)有 id 來(lái)進(jìn)行區(qū)分,一旦有插入動(dòng)作,會(huì)導(dǎo)致插入位置之后的列表全部重新渲染。
這也是為什么渲染列表時(shí)為什么要使用唯一的 key。
7、調(diào)用 setState 之后發(fā)生了什么?
在 setState的時(shí)候,React 會(huì)為當(dāng)前節(jié)點(diǎn)創(chuàng)建一個(gè)updateQueue的更新列隊(duì)。然后會(huì)觸發(fā) reconciliation過(guò)程,在這個(gè)過(guò)程中,會(huì)使用名為 Fiber 的調(diào)度算法,開(kāi)始生成新的 Fiber 樹(shù), Fiber 算法的最大特點(diǎn)是可以做到異步可中斷的執(zhí)行。然后 React Scheduler會(huì)根據(jù)優(yōu)先級(jí)高低,先執(zhí)行優(yōu)先級(jí)高的節(jié)點(diǎn),具體是執(zhí)行doWork方法。在 doWork方法中,React 會(huì)執(zhí)行一遍updateQueue中的方法,以獲得新的節(jié)點(diǎn)。然后對(duì)比新舊節(jié)點(diǎn),為老節(jié)點(diǎn)打上 更新、插入、替換 等 Tag。當(dāng)前節(jié)點(diǎn) doWork完成后,會(huì)執(zhí)行performUnitOfWork方法獲得新節(jié)點(diǎn),然后再重復(fù)上面的過(guò)程。當(dāng)所有節(jié)點(diǎn)都 doWork完成后,會(huì)觸發(fā)commitRoot方法,React 進(jìn)入 commit 階段。在 commit 階段中,React 會(huì)根據(jù)前面為各個(gè)節(jié)點(diǎn)打的 Tag,一次性更新整個(gè) dom 元素。
8、為什么虛擬 dom 會(huì)提高性能?
虛擬 dom 相當(dāng)于在 JS 和真實(shí) dom 中間加了一個(gè)緩存,利用 diff 算法避免了沒(méi)有必要的 dom 操作,從而提高性能。
9、錯(cuò)誤邊界是什么?它有什么用?
在 React 中,如果任何一個(gè)組件發(fā)生錯(cuò)誤,它將破壞整個(gè)組件樹(shù),導(dǎo)致整頁(yè)白屏。這時(shí)候我們可以用錯(cuò)誤邊界優(yōu)雅地降級(jí)處理這些錯(cuò)誤。
例如下面封裝的組件:
class?ErrorBoundary?extends?React.Component<IProps,?IState>?{
??constructor(props:?IProps)?{
????super(props);
????this.state?=?{?hasError:?false?};
??}
??static?getDerivedStateFromError()?{
????//?更新?state?使下一次渲染能夠顯示降級(jí)后的?UI
????return?{?hasError:?true?};
??}
??componentDidCatch(error,?errorInfo)?{
????//?可以將錯(cuò)誤日志上報(bào)給服務(wù)器
????console.log('組件奔潰?Error',?error);
????console.log('組件奔潰?Info',?errorInfo);
??}
??render()?{
????if?(this.state.hasError)?{
??????//?你可以自定義降級(jí)后的?UI?并渲染
??????return?this.props.content;
????}
????return?this.props.children;
??}
}
10、什么是 Portals?
Portal 提供了一種將子節(jié)點(diǎn)渲染到存在于父組件以外的 DOM 節(jié)點(diǎn)的優(yōu)秀的方案。
ReactDOM.createPortal(child,?container)
11、React 組件間有那些通信方式?
父組件向子組件通信
1、 通過(guò) props 傳遞
子組件向父組件通信
1、 主動(dòng)調(diào)用通過(guò) props 傳過(guò)來(lái)的方法,并將想要傳遞的信息,作為參數(shù),傳遞到父組件的作用域中
跨層級(jí)通信
1、 使用 react 自帶的 Context 進(jìn)行通信,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)管理庫(kù)
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ù)來(lái)自定義渲染邏輯。
方法組件中的優(yōu)化手段
1、使用 useMemo。
2、使用 useCallBack。
其他方式
1、在列表需要頻繁變動(dòng)時(shí),使用唯一 id 作為 key,而不是數(shù)組下標(biāo)。
2、必要時(shí)通過(guò)改變 CSS 樣式隱藏顯示組件,而不是通過(guò)條件判斷顯示隱藏組件。
3、使用 Suspense 和 lazy 進(jìn)行懶加載,例如:
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 個(gè) React 性能優(yōu)化技巧[9]
14、為什么 React 元素有一個(gè) $$typeof 屬性?

目的是為了防止 XSS 攻擊。因?yàn)?Synbol 無(wú)法被序列化,所以 React 可以通過(guò)有沒(méi)有 $$typeof 屬性來(lái)斷出當(dāng)前的 element 對(duì)象是從數(shù)據(jù)庫(kù)來(lái)的還是自己生成的。
如果沒(méi)有 $$typeof 這個(gè)屬性,react 會(huì)拒絕處理該元素。
在 React 的古老版本中,下面的寫(xiě)法會(huì)出現(xiàn) XSS 攻擊:
//?服務(wù)端允許用戶存儲(chǔ)?JSON
let?expectedTextButGotJSON?=?{
??type:?'div',
??props:?{
????dangerouslySetInnerHTML:?{
??????__html:?'/*?把你想的擱著?*/'
????},
??},
??//?...
};
let?message?=?{?text:?expectedTextButGotJSON?};
//?React?0.13?中有風(fēng)險(xiǎn)
<p>
??{message.text}
p>
相關(guān)閱讀:Dan Abramov Blog[10]
15、React 如何區(qū)分 Class 組件 和 Function 組件?
一般的方式是借助 typeof 和 Function.prototype.toString 來(lái)判斷當(dāng)前是不是 class,如下:
function?isClass(func)?{
??return?typeof?func?===?'function'
????&&?/^class\s/.test(Function.prototype.toString.call(func));
}
但是這個(gè)方式有它的局限性,因?yàn)槿绻昧?babel 等轉(zhuǎn)換工具,將 class 寫(xiě)法全部轉(zhuǎn)為 function 寫(xiě)法,上面的判斷就會(huì)失效。
React 區(qū)分 Class 組件 和 Function 組件的方式很巧妙,由于所有的類組件都要繼承 React.Component,所以只要判斷原型鏈上是否有 React.Component 就可以了:
AComponent.prototype?instanceof?React.Component
相關(guān)閱讀:Dan Abramov Blog[11]
16、HTML 和 React 事件處理有什么區(qū)別?
在 HTML 中事件名必須小寫(xiě):
<button?onclick='activateLasers()'>
而在 React 中需要遵循駝峰寫(xiě)法:
<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 讓組件“等待”某個(gè)異步操作,直到該異步操作結(jié)束即可渲染。在下面例子中,兩個(gè)組件都會(huì)等待異步 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 中的組件名要以大寫(xiě)字母開(kāi)頭?
因?yàn)?React 要知道當(dāng)前渲染的是組件還是 HTML 元素。
19、redux 是什么?
Redux 是一個(gè)為 JavaScript 應(yīng)用設(shè)計(jì)的,可預(yù)測(cè)的狀態(tài)容器。
它解決了如下問(wèn)題:
跨層級(jí)組件之間的數(shù)據(jù)傳遞變得很容易 所有對(duì)狀態(tài)的改變都需要 dispatch,使得整個(gè)數(shù)據(jù)的改變可追蹤,方便排查問(wèn)題。
但是它也有缺點(diǎn):
概念偏多,理解起來(lái)不容易 樣板代碼太多
20、react-redux 的實(shí)現(xiàn)原理?
通過(guò) redux 和 react context 配合使用,并借助高階函數(shù),實(shí)現(xiàn)了 react-redux。
參考鏈接:React.js 小書(shū)[12]
21、reudx 和 mobx 的區(qū)別?
得益于 Mobx 的 observable,使用 mobx 可以做到精準(zhǔn)更新;對(duì)應(yīng)的 Redux 是用 dispath 進(jìn)行廣播,通過(guò) Provider 和 connect 來(lái)比對(duì)前后差別控制更新粒度;
相關(guān)閱讀:Redux or MobX: An attempt to dissolve the Confusion[13]
22、redux 異步中間件有什么什么作用?
假如有這樣一個(gè)需求:請(qǐng)求數(shù)據(jù)前要向 Store dispatch 一個(gè) loading 狀態(tài),并帶上一些信息;請(qǐng)求結(jié)束后再向 Store dispatch 一個(gè) loaded 狀態(tài)
一些同學(xué)可能會(huì)這樣做:
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é)會(huì)想到可以將 onClick 里的邏輯抽象出來(lái)復(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) 這種寫(xiě)法有點(diǎn)奇怪,會(huì)增加開(kāi)發(fā)者的心智負(fù)擔(dān)。
于是可以借助 rudux 相關(guān)的異步中間件,以 rudux-chunk 為例,將寫(xiě)法改為如下:
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 異步中間件沒(méi)有什么奧秘,主要做的就是這樣的事情。
相關(guān)閱讀:Why do we need middleware for async flow in Redux?[14]
23、redux 有哪些異步中間件?
1、redux-thunk
源代碼簡(jiǎn)短優(yōu)雅,上手簡(jiǎn)單
2、redux-saga[15]
借助 JS 的 generator 來(lái)處理異步,避免了回調(diào)的問(wèn)題
3、redux-observable[16]
借助了 RxJS 流的思想以及其各種強(qiáng)大的操作符,來(lái)處理異步問(wèn)題
覺(jué)得不錯(cuò)可以點(diǎn)擊這個(gè) 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]我對(duì) 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 個(gè) 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 小書(shū): 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
推薦閱讀
