[toc]
## React 面試專題
### React.js是 MVVM 框架嗎?
React就是Facebook的一個開源JS框架,專注的層面為View層,不包括數據訪問層或者那種Hash路由(不過React 有插件支持),與Angularjs,Emberjs等大而全的框架不同,React專注的中心是Component,即組件。React認為一切頁面元 素都可以抽象成組件,比如一個表單,或者表單中的某一項。
React可以作為MVVM中第二個V,也就是View,但是并不是MVVM框架。MVVM一個最顯著的特征:雙向綁定。React沒有這個,它是單向數據綁定的。React是一個單向數據流的庫,狀態驅動視圖。react整體是函數式的思想,把組件設計成純組件,狀態和邏輯通過參數傳入,所以在react中,是單向數據流,推崇結合immutable來實現數據不可變。
### hooks用過嗎?聊聊react中class組件和函數組件的區別
類組件是使用ES6 的 class?來定義的組件。 函數組件是接收一個單一的?`props`?對象并返回一個React元素。
關于React的兩套API(類(class)API 和基于函數的鉤子(hooks) API)。官方推薦使用鉤子(函數),而不是類。因為鉤子更簡潔,代碼量少,用起來比較"輕",而類比較"重"。而且,鉤子是函數,更符合 React 函數式的本質。
函數一般來說,只應該做一件事,就是返回一個值。 如果你有多個操作,每個操作應該寫成一個單獨的函數。而且,數據的狀態應該與操作方法分離。根據函數這種理念,React 的函數組件只應該做一件事情:返回組件的 HTML 代碼,而沒有其他的功能。函數的返回結果只依賴于它的參數。不改變函數體外部數據、函數執行過程里面沒有副作用。
類(class)是數據和邏輯的封裝。 也就是說,組件的狀態和操作方法是封裝在一起的。如果選擇了類的寫法,就應該把相關的數據和操作,都寫在同一個 class 里面。
**類組件的缺點** :
大型組件很難拆分和重構,也很難測試。\
業務邏輯分散在組件的各個方法之中,導致重復邏輯或關聯邏輯。\
組件類引入了復雜的編程模式,比如 render props 和高階組件。\
難以理解的 class,理解 JavaScript 中?`this`?的工作方式。
**區別**:
函數組件的性能比類組件的性能要高,因為類組件使用的時候要實例化,而函數組件直接執行函數取返回結果即可。
1.狀態的有無\
hooks出現之前,函數組件`沒有實例`,`沒有生命周期`,`沒有state`,`沒有this`,所以我們稱函數組件為無狀態組件。 hooks出現之前,react中的函數組件通常只考慮負責UI的渲染,沒有自身的狀態沒有業務邏輯代碼,是一個純函數。它的輸出只由參數props決定,不受其他任何因素影響。
2.調用方式的不同\
函數組件重新渲染,將重新調用組件方法返回新的react元素。類組件重新渲染將new一個新的組件實例,然后調用render類方法返回react元素,這也說明為什么類組件中this是可變的。
3.因為調用方式不同,在函數組件使用中會出現問題\
在操作中改變狀態值,類組件可以獲取最新的狀態值,而函數組件則會按照順序返回狀態值
**React Hooks(鉤子的作用)**
*Hook*?是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
React Hooks的幾個常用鉤子:
1. `useState()` //狀態鉤子
1. `useContext()` //共享狀態鉤子
1. `useReducer()` //action 鉤子
1. `useEffect()` //副作用鉤子
還有幾個不常見的大概的說下,后續會專門寫篇文章描述下
- 1.useCallback 記憶函數 一般把**函數式組件理解為class組件render函數的語法糖**,所以每次重新渲染的時候,函數式組件內部所有的代碼都會重新執行一遍。而有了 useCallback 就不一樣了,你可以通過 useCallback 獲得一個記憶后的函數。
```js
function App() {
const memoizedHandleClick = useCallback(() => {
console.log('Click happened')
}, []); // 空數組代表無論什么情況下該函數都不會發生改變
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}
```
第二個參數傳入一個數組,數組中的每一項一旦值或者引用發生改變,useCallback 就會重新返回一個新的記憶函數提供給后面進行渲染。
- 2.useMemo 記憶組件 useCallback 的功能完全可以由 useMemo 所取代,如果你想通過使用 useMemo 返回一個記憶函數也是完全可以的。 唯一的區別是:**useCallback 不會執行第一個參數函數,而是將它返回給你,而 useMemo 會執行第一個函數并且將函數執行結果返回給你**。\
所以 useCallback 常用記憶事件函數,生成記憶后的事件函數并傳遞給子組件使用。而 useMemo 更適合經過函數計算得到一個確定的值,比如記憶組件。
- 3.useRef 保存引用值
useRef 跟 createRef 類似,都可以用來生成對 DOM 對象的引用。useRef 返回的值傳遞給組件或者 DOM 的 ref 屬性,就可以通過 ref.current 值**訪問組件或真實的 DOM 節點,重點是組件也是可以訪問到的**,從而可以對 DOM 進行一些操作,比如監聽事件等等。
- 4.useImperativeHandle 穿透 Ref
通過 useImperativeHandle 用于讓父組件獲取子組件內的索引
- 5.useLayoutEffect 同步執行副作用
大部分情況下,使用 useEffect 就可以幫我們處理組件的副作用,但是如果想要同步調用一些副作用,比如對 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用會在 DOM 更新之后同步執行。
**useEffect和useLayoutEffect有什么區別**:簡單來說就是調用時機不同,useLayoutEffect和原來componentDidMount&componentDidUpdate一致,在react完成DOM更新后馬上同步調用的代碼,會阻塞頁面渲染。而useEffect是會在整個頁面渲染完才會調用的代碼。`官方建議優先使用useEffect`
### React 組件通信方式
react組件間通信常見的幾種情況:
- 1. 父組件向子組件通信
- 2. 子組件向父組件通信
- 3. 跨級組件通信
- 4. 非嵌套關系的組件通信
#### 1)父組件向子組件通信
父組件通過 props 向子組件傳遞需要的信息。父傳子是在父組件中直接綁定一個正常的屬性,這個屬性就是指具體的值,在子組件中,用props就可以獲取到這個值
```js
// 子組件: Child
const Child = props =>{
return <p>{props.name}</p>
}
// 父組件 Parent
const Parent = ()=>{
return <Child name="京程一燈"></Child>
}
```
#### 2)子組件向父組件通信
props+回調的方式,使用公共組件進行狀態提升。子傳父是先在父組件上綁定屬性設置為一個函數,當子組件需要給父組件傳值的時候,則通過props調用該函數將參數傳入到該函數當中,此時就可以在父組件中的函數中接收到該參數了,這個參數則為子組件傳過來的值
```js
// 子組件: Child
const Child = props =>{
const cb = msg =>{
return ()=>{
props.callback(msg)
}
}
return (
<button onClick={cb("京程一燈歡迎你!")}>京程一燈歡迎你</button>
)
}
// 父組件 Parent
class Parent extends Component {
callback(msg){
console.log(msg)
}
render(){
return <Child callback={this.callback.bind(this)}></Child>
}
}
```
#### 3)跨級組件通信
即父組件向子組件的子組件通信,向更深層子組件通信。
- 使用props,利用中間組件層層傳遞,但是如果父組件結構較深,那么中間每一層組件都要去傳遞props,增加了復雜度,并且這些props并不是中間組件自己需要的。
- 使用context,context相當于一個大容器,我們可以把要通信的內容放在這個容器中,這樣不管嵌套多深,都可以隨意取用,對于跨越多層的全局數據可以使用context實現。
```js
// context方式實現跨級組件通信
// Context 設計目的是為了共享那些對于一個組件樹而言是“全局”的數據
const BatteryContext = createContext();
// 子組件的子組件
class GrandChild extends Component {
render(){
return (
<BatteryContext.Consumer>
{
color => <h1 style={{"color":color}}>我是紅色的:{color}</h1>
}
</BatteryContext.Consumer>
)
}
}
// 子組件
const Child = () =>{
return (
<GrandChild/>
)
}
// 父組件
class Parent extends Component {
state = {
color:"red"
}
render(){
const {color} = this.state
return (
<BatteryContext.Provider value={color}>
<Child></Child>
</BatteryContext.Provider>
)
}
}
```
#### 4)非嵌套關系的組件通信
即沒有任何包含關系的組件,包括兄弟組件以及不在同一個父級中的非兄弟組件。
- 1. 可以使用自定義事件通信(發布訂閱模式),使用pubsub-js
- 2. 可以通過redux等進行全局狀態管理
- 3. 如果是兄弟組件通信,可以找到這兩個兄弟節點共同的父節點, 結合父子間通信方式進行通信。
- 4. 也可以new一個 Vue 的 EventBus,進行事件監聽,一邊執行監聽,一邊執行新增 VUE的eventBus 就是發布訂閱模式,是可以在React中使用的;
### setState 既存在異步情況也存在同步情況
1.異步情況 在`React事件當中是異步操作`
2.同步情況 如果是在`setTimeout事件或者自定義的dom事件`中,都是同步的
```js
//setTimeout事件
import React,{ Component } from "react";
class Count extends Component{
constructor(props){
super(props);
this.state = {
count:0
}
}
render(){
return (
<>
<p>count:{this.state.count}</p>
<button onClick={this.btnAction}>增加</button>
</>
)
}
btnAction = ()=>{
//不能直接修改state,需要通過setState進行修改
//同步
setTimeout(()=>{
this.setState({
count: this.state.count + 1
});
console.log(this.state.count);
})
}
}
export default Count;
```
```js
//自定義dom事件
import React,{ Component } from "react";
class Count extends Component{
constructor(props){
super(props);
this.state = {
count:0
}
}
render(){
return (
<>
<p>count:{this.state.count}</p>
<button id="btn">綁定點擊事件</button>
</>
)
}
componentDidMount(){
//自定義dom事件,也是同步修改
document.querySelector('#btn').addEventListener('click',()=>{
this.setState({
count: this.state.count + 1
});
console.log(this.state.count);
});
}
}
export default Count;
```
### 生命周期

```js
安裝
當組件的實例被創建并插入到 DOM 中時,這些方法按以下順序調用:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
更新中
更新可能由道具或狀態的更改引起。當重新渲染組件時,這些方法按以下順序調用:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
卸載
當組件從 DOM 中移除時調用此方法:
componentWillUnmount()
```
### 說一下 react-fiber
#### 1)背景
react-fiber 產生的根本原因,是`大量的同步計算任務阻塞了瀏覽器的 UI 渲染`。默認情況下,JS 運算、頁面布局和頁面繪制都是運行在瀏覽器的主線程當中,他們之間是互斥的關系。如果 JS 運算持續占用主線程,頁面就沒法得到及時的更新。當我們調用`setState`更新頁面的時候,React 會遍歷應用的所有節點,計算出差異,然后再更新 UI。如果頁面元素很多,整個過程占用的時機就可能超過 16 毫秒,就容易出現掉幀的現象。
#### 2)實現原理
- react內部運轉分三層:
- Virtual DOM 層,描述頁面長什么樣。
- Reconciler 層,負責調用組件生命周期方法,進行 Diff 運算等。
- Renderer 層,根據不同的平臺,渲染出相應的頁面,比較常見的是 ReactDOM 和 ReactNative。
`Fiber 其實指的是一種數據結構,它可以用一個純 JS 對象來表示`:
```js
const fiber = {
stateNode, // 節點實例
child, // 子節點
sibling, // 兄弟節點
return, // 父節點
}
```
- 為了實現不卡頓,就需要有一個調度器 (Scheduler) 來進行任務分配。優先級高的任務(如鍵盤輸入)可以打斷優先級低的任務(如Diff)的執行,從而更快的生效。任務的優先級有六種:
- synchronous,與之前的Stack Reconciler操作一樣,同步執行
- task,在next tick之前執行
- animation,下一幀之前執行
- high,在不久的將來立即執行
- low,稍微延遲執行也沒關系
- offscreen,下一次render時或scroll時才執行
- Fiber Reconciler(react )執行過程分為2個階段:
- 階段一,生成 Fiber 樹,得出需要更新的節點信息。這一步是一個漸進的過程,可以被打斷。階段一可被打斷的特性,讓優先級更高的任務先執行,從框架層面大大降低了頁面掉幀的概率。
- 階段二,將需要更新的節點一次過批量更新,這個過程不能被打斷。
- Fiber樹:React 在 render 第一次渲染時,會通過 React.createElement 創建一顆 Element 樹,可以稱之為 Virtual DOM Tree,由于要記錄上下文信息,加入了 Fiber,每一個 Element 會對應一個 Fiber Node,將 Fiber Node 鏈接起來的結構成為 Fiber Tree。Fiber Tree 一個重要的特點是鏈表結構,將遞歸遍歷編程循環遍歷,然后配合 requestIdleCallback API, 實現任務拆分、中斷與恢復。
從Stack Reconciler到Fiber Reconciler,源碼層面其實就是干了一件遞歸改循環的事情
傳送門 ?[# 深入了解 Fiber](https://juejin.cn/post/7002250258826657799)
### Portals
Portals 提供了一種一流的方式來將子組件渲染到存在于父組件的 DOM 層次結構之外的 DOM 節點中。結構不受外界的控制的情況下就可以使用portals進行創建
### 何時要使用異步組件?如和使用異步組件
- 加載大組件的時候
- 路由異步加載的時候
react 中要配合 Suspense 使用
```js
// 異步懶加載
const Box = lazy(()=>import('./components/Box'));
// 使用組件的時候要用suspense進行包裹
<Suspense fallback={<div>loading...</div>}>
{show && <Box/>}
</Suspense>
```
### React 事件綁定原理
React并不是將click事件綁在該div的真實DOM上,而是`在document處監聽所有支持的事件`,當事件發生并冒泡至document處時,React將事件內容封裝并交由真正的處理函數運行。這樣的方式不僅減少了內存消耗,還能在組件掛載銷毀時統一訂閱和移除事件。\
另外冒泡到 document 上的事件也不是原生瀏覽器事件,而是 React 自己實現的合成事件(SyntheticEvent)。因此我們如果不想要事件冒泡的話,調用 event.stopPropagation 是無效的,而應該調用 `event.preventDefault`。

### React.lazy() 實現的原理
React的懶加載示例:
```js
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
```
**React.lazy 原理**
以下 React 源碼基于 16.8.0 版本
React.lazy 的源碼實現如下:
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
let lazyType = {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null,
};
return lazyType;
}
可以看到其返回了一個 LazyComponent 對象。
而對于 LazyComponent 對象的解析:
```js
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
```
```js
function mountLazyComponent(
_current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
) {
...
let Component = readLazyComponentType(elementType);
...
}
```
```js
// Pending = 0, Resolved = 1, Rejected = 2
export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
const status = lazyComponent._status;
const result = lazyComponent._result;
switch (status) {
case Resolved: {
const Component: T = result;
return Component;
}
case Rejected: {
const error: mixed = result;
throw error;
}
case Pending: {
const thenable: Thenable<T, mixed> = result;
throw thenable;
}
default: { // lazyComponent 首次被渲染
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
thenable.then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
// Handle synchronous thenables.
switch (lazyComponent._status) {
case Resolved:
return lazyComponent._result;
case Rejected:
throw lazyComponent._result;
}
lazyComponent._result = thenable;
throw thenable;
}
}
}
```
注:如果 readLazyComponentType 函數多次處理同一個 lazyComponent,則可能進入Pending、Rejected等 case 中。
從上述代碼中可以看出,對于最初 React.lazy() 所返回的 LazyComponent 對象,其 _status 默認是 -1,所以首次渲染時,會進入 readLazyComponentType 函數中的 default 的邏輯,這里才會真正異步執行 import(url)操作,由于并未等待,隨后會檢查模塊是否 Resolved,如果已經Resolved了(已經加載完畢)則直接返回moduleObject.default(動態加載的模塊的默認導出),否則將通過 throw 將 thenable 拋出到上層。
為什么要 throw 它?這就要涉及到 Suspense 的工作原理,我們接著往下分析。
**Suspense 原理**
由于 React 捕獲異常并處理的代碼邏輯比較多,這里就不貼源碼,感興趣可以去看 throwException 中的邏輯,其中就包含了如何處理捕獲的異常。簡單描述一下處理過程,React 捕獲到異常之后,會判斷異常是不是一個 thenable,如果是則會找到 SuspenseComponent ,如果 thenable 處于 pending 狀態,則會將其 children 都渲染成 fallback 的值,一旦 thenable 被 resolve 則 SuspenseComponent 的子組件會重新渲染一次。
為了便于理解,我們也可以用 componentDidCatch 實現一個自己的 Suspense 組件,如下:
```js
class Suspense extends React.Component {
state = {
promise: null
}
componentDidCatch(err) {
// 判斷 err 是否是 thenable
if (err !== null && typeof err === 'object' && typeof err.then === 'function') {
this.setState({ promise: err }, () => {
err.then(() => {
this.setState({
promise: null
})
})
})
}
}
render() {
const { fallback, children } = this.props
const { promise } = this.state
return <>{ promise ? fallback : children }</>
}
}
```
至此,我們分析完了 React 的懶加載原理。簡單來說,React利用 React.lazy與import()實現了渲染時的動態加載 ,并利用Suspense來處理異步加載資源時頁面應該如何顯示的問題。
參考傳送門? [React Lazy 的實現原理](https://thoamsy.github.io/blogs/react-lazy/)
- JavaScript
- 1. DOM事件流
- 2. 模擬 new, Object create(), bind
- 5. 封裝函數進行字符串駝峰命名的轉換
- 6. 什么是promise
- 7. 判斷一個數是否為數組
- 10. __proto__和prototype以及原型,原型鏈,構造函數
- 11. 繼承
- 12. 閉包
- 13. 回調函數
- 14. var 和 let 區別
- 15. this、bind、call、apply
- 16.undefined和null的區別
- 17.內存泄漏
- 18.垃圾回收機制
- html css
- 1. 元素垂直水平居中
- 2. 清除浮動
- 3. bootstrap柵格系統
- 4. px rpx em rem vw 的區別
- 5. 兩種盒子模型
- 6. 合集
- web類
- 1. html5的新特性以及理解(web標簽語義化)
- 2. 什么是路由,關于前端路由和后端路由
- 3. 對優質代碼的理解
- 4. cookie 和 sessionStorage和localStorage
- 5. 瀏覽器內核
- 6. http 狀態碼
- 7. href 和 src 的區別
- 8. link 和 @import 的區別
- 9. http 狀態碼
- 10. websocket
- 11. 瀏覽器解析url
- 12.http緩存
- vue
- 1.vue2和vue3有哪些區別
- 1. 對 mvvvm 的理解
- 2. mvvm的優缺點
- 3. 數據雙向綁定的原理
- 4. 生命周期
- 5. 組件如何通信
- 6. computed和watch的區別
- 7. proxy 和 Object.defineProperty
- 8. 虛擬dom和 diff算法
- 9. 路由的嵌套與傳參
- 10. 路由導航鉤子
- 11. axios 的理解
- 12. vue自定義指令 diretive
- 13. diff 的實現
- 14. 實現一個簡單的雙向綁定
- 15. 為什么 data 是一個函數
- 題譜
- js
- 手寫篇
- css
- vue
- react
- 算法
- 自我介紹
- 八股文
- 源項目地址
- 1.計算機網絡
- 2.瀏覽器
- 3.html和css
- 4.javascript
- 6.typescript
- 7.vue
- 8.react
- 大廠面試
- 面試題大全
- 常見性能優化
- 面試實戰
- 面試分析
- 押題
- 1.微前端在項目中的實際應用
- 2.性能優化
- vue相關
- 1.說一說HashRouter和HistoryRouter的區別和原理
- 無敵之路,牛客網面試題自測記錄
- 前端基礎
- 1.html
- 2.js基礎
- 珠峰性能優化
- WebWorker
- url到渲染
- 瀏覽器加載機制
- 自我介紹1
- 手寫題
- 1.compose
- 2.setTimeout模擬setInterval
- 3.手寫數組拍平
- 4.手寫promise.all
- 5.手寫深拷貝
- webpack
- 實戰