React 靈魂 23 問,你能答對幾個?

授權轉載自:王玉略?
https://zhuanlan.zhihu.com/p/304213203
文內所有鏈接請點擊底部【閱讀原文】查看
1、setState 是異步還是同步?
合成事件中是異步 鉤子函數(shù)中的是異步 原生事件中是同步 setTimeout中是同步
相關鏈接:你真的理解setState嗎?:
2、聊聊 [email protected] + 的生命周期

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

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

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

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

3、element diff:對于同一層級的一組子節(jié)點,需要通過唯一 id 進行來區(qū)分
如果沒有 id 來進行區(qū)分,一旦有插入動作,會導致插入位置之后的列表全部重新渲染。
這也是為什么渲染列表時為什么要使用唯一的 key。
7、調用 setState 之后發(fā)生了什么?
在? setState?的時候,React 會為當前節(jié)點創(chuàng)建一個?updateQueue?的更新列隊。然后會觸發(fā)? reconciliation?過程,在這個過程中,會使用名為 Fiber 的調度算法,開始生成新的 Fiber 樹, Fiber 算法的最大特點是可以做到異步可中斷的執(zhí)行。然后? React Scheduler?會根據(jù)優(yōu)先級高低,先執(zhí)行優(yōu)先級高的節(jié)點,具體是執(zhí)行?doWork方法。在? doWork?方法中,React 會執(zhí)行一遍?updateQueue?中的方法,以獲得新的節(jié)點。然后對比新舊節(jié)點,為老節(jié)點打上 更新、插入、替換 等 Tag。當前節(jié)點? doWork?完成后,會執(zhí)行?performUnitOfWork?方法獲得新節(jié)點,然后再重復上面的過程。當所有節(jié)點都? doWork?完成后,會觸發(fā)?commitRoot?方法,React 進入 commit 階段。在 commit 階段中,React 會根據(jù)前面為各個節(jié)點打的 Tag,一次性更新整個 dom 元素。
8、為什么虛擬dom 會提高性能?
虛擬dom 相當于在 JS 和真實 dom 中間加了一個緩存,利用 diff 算法避免了沒有必要的 dom 操作,從而提高性能。
9、錯誤邊界是什么?它有什么用?
在 React 中,如果任何一個組件發(fā)生錯誤,它將破壞整個組件樹,導致整頁白屏。這時候我們可以用錯誤邊界優(yōu)雅地降級處理這些錯誤。
例如下面封裝的組件:
class ErrorBoundary extends React.Component {
constructor(props: IProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
// 更新 state 使下一次渲染能夠顯示降級后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 可以將錯誤日志上報給服務器
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、 主動調用通過 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 (
);
}
function Toolbar() {
return (
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
);
}
export default App;
2、使用 Redux 或者 Mobx 等狀態(tài)管理庫
3、使用訂閱發(fā)布模式
相關鏈接:React Docs
12、React 父組件如何調用子組件中的方法?
1、如果是在方法組件中調用子組件(>= [email protected]),可以使用?useRef?和?useImperativeHandle:
const { forwardRef, useRef, useImperativeHandle } = React;
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
getAlert() {
alert("getAlert from Child");
}
}));
return Hi
;
});
const Parent = () => {
const childRef = useRef();
return (
);
};
2、如果是在類組件中調用子組件(>= [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 (
);
}
}
class Child extends Component {
getAlert() {
alert('getAlert from Child');
}
render() {
return Hello
;
}
}
參考閱讀:Call child method from parent
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ù)組下標。
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 (
This is the Base User: {this.state.name}
Loading... }>
)
}
}
Suspense?用法可以參考官方文檔
相關閱讀:21個React性能優(yōu)化技巧
14、為什么 React 元素有一個 $$typeof 屬性?

目的是為了防止 XSS 攻擊。因為 Synbol 無法被序列化,所以 React 可以通過有沒有 $$typeof 屬性來斷出當前的 element 對象是從數(shù)據(jù)庫來的還是自己生成的。
如果沒有 $$typeof 這個屬性,react 會拒絕處理該元素。
在 React 的古老版本中,下面的寫法會出現(xiàn) XSS 攻擊:
// 服務端允許用戶存儲 JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* 把你想的擱著 */'
},
},
// ...
};
let message = { text: expectedTextButGotJSON };
// React 0.13 中有風險
{message.text}
相關閱讀:Dan Abramov Blog
15、React 如何區(qū)分 Class組件 和 Function組件?
一般的方式是借助 typeof 和 Function.prototype.toString 來判斷當前是不是 class,如下:
function?isClass(func)?{
??return?typeof?func?===?'function'
????&&?/^class\s/.test(Function.prototype.toString.call(func));
}
但是這個方式有它的局限性,因為如果用了 babel 等轉換工具,將 class 寫法全部轉為 function 寫法,上面的判斷就會失效。
React 區(qū)分 Class組件 和 Function組件的方式很巧妙,由于所有的類組件都要繼承 React.Component,所以只要判斷原型鏈上是否有 React.Component 就可以了:
AComponent.prototype?instanceof?React.Component
相關閱讀:Dan Abramov Blog
16、HTML 和 React 事件處理有什么區(qū)別?
在 HTML 中事件名必須小寫:
<button?onclick='activateLasers()'>
而在 React 中需要遵循駝峰寫法:
在 HTML 中可以返回 false 以阻止默認的行為:
<a?href='#'?onclick='console.log("The?link?was?clicked.");?return?false;'?/>
而在 React 中必須地明確地調用?preventDefault():
function?handleClick(event)?{
??event.preventDefault()
??console.log('The?link?was?clicked.')
}
17、什么是 suspense 組件?
Suspense 讓組件“等待”某個異步操作,直到該異步操作結束即可渲染。在下面例子中,兩個組件都會等待異步 API 的返回值:
const resource = fetchProfileData();
function ProfilePage() {
return (
Loading profile...}>
Loading posts...}>
);
}
function ProfileDetails() {
// 嘗試讀取用戶信息,盡管該數(shù)據(jù)可能尚未加載
const user = resource.user.read();
return {user.name}
;
}
function ProfileTimeline() {
// 嘗試讀取博文信息,盡管該部分數(shù)據(jù)可能尚未加載
const posts = resource.posts.read();
return (
{posts.map(post => (
- {post.text}
))}
);
}
Suspense 也可以用于懶加載,參考下面的代碼:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
Loading... }>
);
}
18、為什么 JSX 中的組件名要以大寫字母開頭?
因為 React 要知道當前渲染的是組件還是 HTML 元素。
19、redux 是什么?
Redux 是一個為 JavaScript 應用設計的,可預測的狀態(tài)容器。
它解決了如下問題:
跨層級組件之間的數(shù)據(jù)傳遞變得很容易 所有對狀態(tài)的改變都需要 dispatch,使得整個數(shù)據(jù)的改變可追蹤,方便排查問題。
但是它也有缺點:
概念偏多,理解起來不容易 樣板代碼太多
20、react-redux 的實現(xiàn)原理?
通過 redux 和 react context 配合使用,并借助高階函數(shù),實現(xiàn)了?react-redux。
參考鏈接:React.js 小書
21、reudx 和 mobx 的區(qū)別?
得益于 Mobx 的 observable,使用 mobx 可以做到精準更新;對應的 Redux 是用 dispath 進行廣播,通過Provider 和 connect 來比對前后差別控制更新粒度;
相關閱讀:Redux or MobX: An attempt to dissolve the Confusion
22、redux 異步中間件有什么什么作用?
假如有這樣一個需求:請求數(shù)據(jù)前要向 Store dispatch 一個 loading 狀態(tài),并帶上一些信息;請求結束后再向Store dispatch 一個 loaded 狀態(tài)
一些同學可能會這樣做:
function App() {
const onClick = () => {
dispatch({ type: 'LOADING', message: 'data is loading' })
fetch('dataurl').then(() => {
dispatch({ type: 'LOADED' })
});
}
return (
);
}
但是如果有非常多的地方用到這塊邏輯,那應該怎么辦?
聰明的同學會想到可以將 onClick 里的邏輯抽象出來復用,如下:
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 (
);
}
很好,但是?fetchData('data is loading')(dispatch)?這種寫法有點奇怪,會增加開發(fā)者的心智負擔。
于是可以借助 rudux 相關的異步中間件,以?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?(
????
??);
}
這樣就更符合認知一些了,redux 異步中間件沒有什么奧秘,主要做的就是這樣的事情。
相關閱讀:Why do we need middleware for async flow in Redux?
23、redux 有哪些異步中間件?
1、redux-thunk
源代碼簡短優(yōu)雅,上手簡單
2、redux-saga
借助 JS 的 generator 來處理異步,避免了回調的問題
3、redux-observable
借助了 RxJS 流的思想以及其各種強大的操作符,來處理異步問題
覺得不錯可以點擊這個 repo 關注更多內容。
關注數(shù):10億+?文章數(shù):10億+
粉絲量:10億+?點擊量:10億+
?
懸賞博主專區(qū)請掃描這里

喜愛數(shù):?1億+?發(fā)帖數(shù):?1億+
回帖數(shù):?1億+?結貼率:?99.9%
—————END—————
喜歡本文的朋友,歡迎關注公眾號?程序員哆啦A夢,收看更多精彩內容
點個[在看],是對小達最大的支持!
如果覺得這篇文章還不錯,來個【分享、點贊、在看】三連吧,讓更多的人也看到~


