1、setState 是異步還是同步?
- 合成事件中是異步
- 鈎子函數中的是異步
- 原生事件中是同步
- setTimeout中是同步
相關鏈接:你真的理解setState嗎?:
2、聊聊 react@16.4 + 的生命周期
相關連接:React 生命周期 我對 React v16.4 生命周期的理解
3、useEffect(fn, []) 和 componentDidMount 有什么差異?
useEffect
會捕獲 props
和 state
。所以即便在回調函數里,你拿到的還是初始的 props
和 state
。如果想得到“最新”的值,可以使用 ref
。
4、hooks 為什么不能放在條件判斷里?
以 setState
為例,在 react 內部,每個組件(Fiber)的 hooks 都是以鏈表的形式存在 memoizeState
屬性中:
update 階段,每次調用 setState
,鏈表就會執行 next 向后移動一步。如果將 setState
寫在條件判斷中,假設條件判斷不成立,沒有執行里面的 setState
方法,會導致接下來所有的 setState
的取值出現偏移,從而導致異常發生。
參考鏈接:烤透 React Hook
5、fiber 是什么?
React Fiber 是一種基於瀏覽器的單線程調度算法。
React Fiber 用類似 requestIdleCallback
的機制來做異步 diff。但是之前數據結構不支持這樣的實現異步 diff,於是 React 實現了一個類似鏈表的數據結構,將原來的 遞歸diff 變成了現在的 遍歷diff,這樣就能做到異步可更新了。
相關鏈接:React Fiber 是什么?
6、聊一聊 diff 算法
傳統 diff 算法的時間復雜度是 O(n^3),這在前端 render 中是不可接受的。為了降低時間復雜度,react 的 diff 算法做了一些妥協,放棄了最優解,最終將時間復雜度降低到了 O(n)。
那么 react diff 算法做了哪些妥協呢?,參考如下:
1、tree diff:只對比同一層的 dom 節點,忽略 dom 節點的跨層級移動
如下圖,react 只會對相同顏色方框內的 DOM 節點進行比較,即同一個父節點下的所有子節點。當發現節點不存在時,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。
這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。
這就意味着,如果 dom 節點發生了跨層級移動,react 會刪除舊的節點,生成新的節點,而不會復用。
2、component diff:如果不是同一類型的組件,會刪除舊的組件,創建新的組件
3、element diff:對於同一層級的一組子節點,需要通過唯一 id 進行來區分
如果沒有 id 來進行區分,一旦有插入動作,會導致插入位置之后的列表全部重新渲染。
這也是為什么渲染列表時為什么要使用唯一的 key。
7、調用 setState 之后發生了什么?
- 在
setState
的時候,React 會為當前節點創建一個updateQueue
的更新列隊。 - 然后會觸發
reconciliation
過程,在這個過程中,會使用名為 Fiber 的調度算法,開始生成新的 Fiber 樹, Fiber 算法的最大特點是可以做到異步可中斷的執行。 - 然后
React Scheduler
會根據優先級高低,先執行優先級高的節點,具體是執行doWork
方法。 - 在
doWork
方法中,React 會執行一遍updateQueue
中的方法,以獲得新的節點。然后對比新舊節點,為老節點打上 更新、插入、替換 等 Tag。 - 當前節點
doWork
完成后,會執行performUnitOfWork
方法獲得新節點,然后再重復上面的過程。 - 當所有節點都
doWork
完成后,會觸發commitRoot
方法,React 進入 commit 階段。 - 在 commit 階段中,React 會根據前面為各個節點打的 Tag,一次性更新整個 dom 元素。
8、為什么虛擬dom 會提高性能?
虛擬dom 相當於在 JS 和真實 dom 中間加了一個緩存,利用 diff 算法避免了沒有必要的 dom 操作,從而提高性能。
9、錯誤邊界是什么?它有什么用?
在 React 中,如果任何一個組件發生錯誤,它將破壞整個組件樹,導致整頁白屏。這時候我們可以用錯誤邊界優雅地降級處理這些錯誤。
例如下面封裝的組件:
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) {
// 可以將錯誤日志上報給服務器
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 提供了一種將子節點渲染到存在於父組件以外的 DOM 節點的優秀的方案。
ReactDOM.createPortal(child, container)
11、React 組件間有那些通信方式?
父組件向子組件通信
1、 通過 props 傳遞
子組件向父組件通信
1、 主動調用通過 props 傳過來的方法,並將想要傳遞的信息,作為參數,傳遞到父組件的作用域中
跨層級通信
1、 使用 react 自帶的 Context
進行通信,createContext
創建上下文, 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 等狀態管理庫
3、使用訂閱發布模式
相關鏈接:React Docs
12、React 父組件如何調用子組件中的方法?
1、如果是在方法組件中調用子組件(>= react@16.8
),可以使用 useRef
和 useImperativeHandle
:
const { forwardRef, useRef, useImperativeHandle } = React;
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
getAlert() {
alert("getAlert from Child");
}
}));
return <h1>Hi</h1>;
});
const Parent = () => {
const childRef = useRef();
return (
<div>
<Child ref={childRef} />
<button onClick={() => childRef.current.getAlert()}>Click</button>
</div>
);
};
2、如果是在類組件中調用子組件(>= react@16.4
),可以使用 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}>Click</button>
</div>
);
}
}
class Child extends Component {
getAlert() {
alert('getAlert from Child');
}
render() {
return <h1>Hello</h1>;
}
}
參考閱讀:Call child method from parent
13、React有哪些優化性能的手段?
類組件中的優化手段
1、使用純組件 PureComponent
作為基類。
2、使用 React.memo
高階函數包裝組件。
3、使用 shouldComponentUpdate
生命周期函數來自定義渲染邏輯。
方法組件中的優化手段
1、使用 useMemo
。
2、使用 useCallBack
。
其他方式
1、在列表需要頻繁變動時,使用唯一 id 作為 key,而不是數組下標。
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
用法可以參考官方文檔
相關閱讀:21個React性能優化技巧
14、為什么 React 元素有一個 $$typeof 屬性?
目的是為了防止 XSS 攻擊。因為 Synbol 無法被序列化,所以 React 可以通過有沒有 $$typeof 屬性來斷出當前的 element 對象是從數據庫來的還是自己生成的。
如果沒有 $$typeof 這個屬性,react 會拒絕處理該元素。
在 React 的古老版本中,下面的寫法會出現 XSS 攻擊:
// 服務端允許用戶存儲 JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* 把你想的擱着 */'
},
},
// ...
};
let message = { text: expectedTextButGotJSON };
// React 0.13 中有風險
<p>
{message.text}
</p>
相關閱讀:Dan Abramov Blog
15、React 如何區分 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 區分 Class組件 和 Function組件的方式很巧妙,由於所有的類組件都要繼承 React.Component,所以只要判斷原型鏈上是否有 React.Component 就可以了:
AComponent.prototype instanceof React.Component
相關閱讀:Dan Abramov Blog
16、HTML 和 React 事件處理有什么區別?
在 HTML 中事件名必須小寫:
<button onclick='activateLasers()'>
而在 React 中需要遵循駝峰寫法:
<button onClick={activateLasers}>
在 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 (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// 嘗試讀取用戶信息,盡管該數據可能尚未加載
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// 嘗試讀取博文信息,盡管該部分數據可能尚未加載
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 要知道當前渲染的是組件還是 HTML 元素。
19、redux 是什么?
Redux 是一個為 JavaScript 應用設計的,可預測的狀態容器。
它解決了如下問題:
- 跨層級組件之間的數據傳遞變得很容易
- 所有對狀態的改變都需要 dispatch,使得整個數據的改變可追蹤,方便排查問題。
但是它也有缺點:
- 概念偏多,理解起來不容易
- 樣板代碼太多
20、react-redux 的實現原理?
通過 redux 和 react context 配合使用,並借助高階函數,實現了 react-redux
。
參考鏈接:React.js 小書
21、reudx 和 mobx 的區別?
得益於 Mobx 的 observable,使用 mobx 可以做到精准更新;對應的 Redux 是用 dispath 進行廣播,通過Provider 和 connect 來比對前后差別控制更新粒度;
相關閱讀:Redux or MobX: An attempt to dissolve the Confusion
22、redux 異步中間件有什么什么作用?
假如有這樣一個需求:請求數據前要向 Store dispatch 一個 loading 狀態,並帶上一些信息;請求結束后再向Store dispatch 一個 loaded 狀態
一些同學可能會這樣做:
function App() {
const onClick = () => {
dispatch({ type: 'LOADING', message: 'data is loading' })
fetch('dataurl').then(() => {
dispatch({ type: 'LOADED' })
});
}
return (<div>
<button onClick={onClick}>click</button>
</div>);
}
但是如果有非常多的地方用到這塊邏輯,那應該怎么辦?
聰明的同學會想到可以將 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 (<div>
<button onClick={onClick}>click</button>
</div>);
}
很好,但是 fetchData('data is loading')(dispatch)
這種寫法有點奇怪,會增加開發者的心智負擔。
於是可以借助 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 (<div>
<button onClick={onClick}>click</button>
</div>);
}
這樣就更符合認知一些了,redux 異步中間件沒有什么奧秘,主要做的就是這樣的事情。
相關閱讀:Why do we need middleware for async flow in Redux?
23、redux 有哪些異步中間件?
1、redux-thunk
源代碼簡短優雅,上手簡單
2、redux-saga
借助 JS 的 generator 來處理異步,避免了回調的問題
3、redux-observable
借助了 RxJS 流的思想以及其各種強大的操作符,來處理異步問題