[TOC]
# Hook
簡單來說 Hook 擁抱了函數式編程,Fiber 架構從底層優化了 React 的性能。
使用 Hook
```js
import React, { useState, useEffect } from 'react';
// 自定義hook
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null); // 將 setState 的統一操作抽離為一個個的函數
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// useEffect 簡化了事件監聽器的添加與移除的書寫
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
// 使用自定義hook
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
```
不使用 Hook
```js
class XXX extend Component {
state = {
isOnline: false
}
handleStatusChange (xxx) {
this.setState({
isOnline: xxx
})
}
componentDidMount =()=> {
ChatAPI.subscribeToFriendStatus(friendID, this.handleStatusChange);
}
componentWillUnmount = () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, this.handleStatusChange);
}
render () {
XXXXXXX
}
}
```
## state hook(讓函數組件擁有 state)
```js
import React, { useState } from 'react';
function Example() {
// 聲明一個叫 "count" 的 state 變量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```
等價的 Class 示例:
```js
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
```
`const [count, setCount] = useState(0);`
這條語句到底做了哪些事情?
我們聲明了一個叫`count`的 state 變量,然后把它設為`0`。React 會在重復渲染時記住它當前的值,并且提供最新的值給我們的函數。我們可以通過調用`setCount`來更新當前的`count`。
下面具體分析下`useState()`方法
1、調用 useState 方法的時候做了什么?它定義一個 “state 變量”。我們的變量叫`count`, 這是一種在函數調用時保存變量的方式 ——`useState`是一種新方法,它與 class 里面的`this.state`提供的功能完全相同。一般來說,在函數退出后變量就就會”消失”,而 state 中的變量會被 React 保留。
2、`useState`需要哪些參數?`useState()`方法里面唯一的參數就是初始 state。不同于 class 的是,我們可以按照需要使用數字或字符串對其進行賦值,而不一定是對象。在示例中,只需使用數字來記錄用戶點擊次數,所以我們傳了`0`作為變量的初始 state。(如果我們想要在 state 中存儲兩個不同的變量,只需調用`useState()`兩次即可。)
3、`useState`方法的返回值是什么?返回值為:當前 state 以及更新 state 的函數。這就是我們寫`const [count, setCount] = useState()`的原因。這與 class 里面`this.state.count`和`this.setState`類似,唯一區別就是你需要成對的獲取它們。
### 讀取與更新 state
讀取 state:
當我們想在 class 中顯示當前的 count,我們讀取`this.state.count`:
~~~
<p>You clicked {this.state.count} times</p>
~~~
在函數中,我們可以直接用`count`:
~~~
<p>You clicked {count} times</p>
~~~
更新 state:
在 class 中,我們需要調用`this.setState()`來更新`count`值:
~~~
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
~~~
在函數中,我們已經有了`setCount`和`count`變量,所以我們不需要`this`:
~~~
<button onClick={() => setCount(count + 1)}>
Click me
</button>
~~~
## Effect Hook(讓函數組件擁有生命周期)
你可以把`useEffect`Hook 看做`componentDidMount`,`componentDidUpdate`和`componentWillUnmount`這三個函數的組合。
<br/>
*Effect Hook* 可以讓你在函數組件中執行副作用操作,在 React 組件中有兩種常見副作用操作:需要清除的和不需要清除的,所以對應的 Effect Hook 也分為無需清除的 effect 和需要清除的 effect。
<br/>
默認情況下,effect 將在每輪渲染結束后執行,但也可以選擇讓其在只有某些值改變的時候才執行。
### 無需清除的 effect
有時候,我們只想**在 React 更新 DOM 之后運行一些額外的代碼**。比如發送網絡請求,手動變更 DOM,記錄日志,這些都是常見的無需清除的操作。因為我們在執行完這些操作之后,就可以忽略他們了。讓我們對比一下使用 class 和 Hook 都是怎么實現這些副作用的。
```js
// 使用 class
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
// 我們基本上都希望在 React 更新 DOM 之后才執行我們的操作
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
```
在這個 class 中,我們需要在兩個生命周期函數中編寫重復的代碼。
這是因為很多情況下,我們希望在組件加載和更新時執行同樣的操作。從概念上說,我們希望它在每次渲染之后執行 —— 但 React 的 class 組件沒有提供這樣的方法。即使我們提取出一個方法,我們還是要在兩個地方調用它。
```js
// 使用 hook
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 通過使用這個 Hook,你可以告訴 React 組件需要在渲染后執行某些操作。
// React 會保存你傳遞的函數(我們將它稱之為 “effect”),并且在執行 DOM 更新之后調用它。
useEffect(() => {
document.title = `You clicked ${count} times`; // 可以直接訪問 count state 變量或其他 prop,它們保存在函數作用域中
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```
### 需要清除的 effect
之前,我們研究了如何使用不需要清除的副作用,還有一些副作用是需要清除的。例如**訂閱外部數據源**。這種情況下,清除工作是非常重要的,可以防止引起內存泄露!現在讓我們來比較一下如何用 Class 和 Hook 來實現。(常見的如手動添加事件處理程序,需要在組件銷毀之前移除)
```js
// 使用 class
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
// 與 componentDidMount 邏輯相對應的,我們必須這樣拆分代碼~~~
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
```
### effect 的條件執行
默認情況下,effect 會在每輪組件渲染完成后執行。這樣的話,一旦 effect 的依賴發生變化,它就會被重新創建。
<br/>
然而,在某些場景下這么做可能會矯枉過正。比如,在上一章節的訂閱示例中,我們不需要在每次組件更新時都創建新的訂閱,而是僅需要在`source`prop 改變時重新創建。
<br/>
要實現這一點,可以給`useEffect`傳遞第二個參數,它是 effect 所依賴的值數組。更新后的示例如下:
```
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
```
此時,只有當`props.source`改變后才會重新創建訂閱。
如果想執行只運行一次的 effect(僅在組件掛載和卸載時執行),可以傳遞一個空數組(`[]`)作為第二個參數。這就告訴 React 你的 effect 不依賴于 props 或 state 中的任何值,所以它永遠都不需要重復執行。這并不屬于特殊情況 —— 它依然遵循輸入數組的工作方式。
[關于依賴列表是否為空的注意事項](https://react.docschina.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies)
## 自定義 Hook
[https://zh-hans.reactjs.org/docs/hooks-custom.html#gatsby-focus-wrapper](https://zh-hans.reactjs.org/docs/hooks-custom.html#gatsby-focus-wrapper)
- 將組件邏輯提取到可重用的函數中(使組件可以共享某一重復的邏輯)
例如,將一個獲取鼠標位置的狀態邏輯寫成自定義 Hook:
```
import React, { useState, useEffect } from 'react'
// 自定義 Hook 是一個函數,其名稱必須以 "use" 開頭(約定)
const useMousePosition = () => {
const [ positions, setPositions ] = useState({x: 0, y: 0})
useEffect(() => {
const updateMouse = (event) => {
setPositions({ x: event.clientX, y: event.clientY })
}
document.addEventListener('mousemove', updateMouse)
return () => {
document.removeEventListener('mousemove', updateMouse)
}
})
return positions
}
export default useMousePosition
```
在組件中使用自定義 Hook 也很簡單:
```
function App() {
const position = useMousePosition()
return (
<div className="App">
<header className="App-header">
<h1>{position.x}</h1>
</header>
</div>
)
}
```
在兩個組件中使用相同的 Hook 不會共享 state。自定義 Hook 是一種重用狀態邏輯的機制(例如設置為訂閱并存儲當前值),所以每次使用自定義 Hook 時,其中的所有 state 和副作用都是完全隔離的。
# useRef 的使用
~~~
const refContainer = useRef(initialValue);
~~~
`useRef`返回一個可變的 ref 對象,其`.current`屬性被初始化為傳入的參數(`initialValue`)。返回的 ref 對象在組件的整個生命周期內保持不變。可以將其視為多次渲染之間的紐帶。另外,修改 ref 對象的值并不會觸發組件重新渲染。
一個常見的用例便是命令式地訪問子組件:
~~~
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已掛載到 DOM 上的文本輸入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
~~~
注意這里 ref 訪問 DOM 的方式,將 ref 對象以`<div ref={myRef} />`形式傳入組件,則無論該節點如何改變,React 都會將 ref 對象的`.current`屬性設置為相應的 DOM 節點。
# useContext 解決多層屬性傳遞
~~~
const value = useContext(MyContext);
~~~
接收一個 context 對象(`React.createContext`的返回值)并返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的`<MyContext.Provider>`的`value`prop 決定。
當組件上層最近的`<MyContext.Provider>`更新時,該 Hook 會觸發重渲染,并使用最新傳遞給`MyContext`provider 的 context`value`值。
~~~
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light); // ① createContext 創建 Context 對象
function App() {
return (
<ThemeContext.Provider value={themes.dark}> // ② Provider 包裹組件
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext); return ( // ③ 子組件先引入 Context 對象(import ...)然后使用 useContext 獲取
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button> );
}
~~~
# Fiber
## Fiber 架構解決了什么問題
在頁面元素很多,且需要頻繁刷新的場景下,React 15 會出現掉幀的現象。請看以下例子:

其根本原因,是大量的同步計算任務阻塞了瀏覽器的 UI 渲染。默認情況下,JS 運算、頁面布局和頁面繪制都是運行在瀏覽器的主線程當中,他們之間是互斥的關系。如果 JS 運算持續占用主線程,頁面就沒法得到及時的更新。當我們調用`setState`更新頁面的時候,React 會遍歷應用的所有節點,計算出差異,然后再更新 UI。整個過程是一氣呵成,不能被打斷的。如果頁面元素很多,整個過程占用的時機就可能超過 16 毫秒,就容易出現掉幀的現象。
針對這一問題,React 團隊從框架層面對 web 頁面的運行機制做了優化,得到很好的效果。

## 實現淺析
React 框架內部的運作可以分為 3 層:
* Virtual DOM 層,描述頁面長什么樣。
* Reconciler 層,負責調用組件生命周期方法,進行 Diff 運算等。
* Renderer 層,根據不同的平臺,渲染出相應的頁面,比較常見的是 ReactDOM 和 ReactNative。
這次改動最大的當屬 Reconciler 層了,React 團隊也給它起了個新的名字,叫`Fiber Reconciler`。這就引入另一個關鍵詞:Fiber。
先看一下`stack-reconciler`下的 React 是怎么工作的。代碼中創建(或更新)一些元素, React 會根據這些元素創建(或更新)Virtual DOM,然后 React 根據更新前后 Virtual DOM 的區別,去修改真正的 DOM。注意,**在 stack reconciler 下,DOM 的更新是同步的,也就是說,在 Virtual DOM 的比對過程中,發現一個 Instance 有更新,會立即執行 DOM 操作**。

而`Fiber Reconciler`下,操作是可以分成很多小部分,并且可以被中斷的,所以同步操作 DOM 可能會導致 fiber-tree 與實際 DOM 的不同步。對于每個節點來說,其不光存儲了對應元素的基本信息,還要保存一些用于任務調度的信息。因此,fiber 僅僅是一個對象,表征 reconciliation 階段所能拆分的最小工作單元,和上圖中的 react instance一一對應。通過`stateNode`屬性管理 Instance 自身的特性。通過`child`和`sibling`表征當前工作單元的下一個工作單元,`return`表示處理完成后返回結果所要合并的目標,通常指向父節點。整個結構是一個鏈表樹。每個工作單元(fiber)執行完成后,都會查看是否還繼續擁有主線程時間片,如果有繼續下一個,如果沒有則先處理其他高優先級事務,等主線程空閑下來繼續執行。
Fiber 就是一種數據結構,它可以用一個純 JS 對象來表示:
```js
const fiber = {
stateNode: {}, // 管理 Instance 自身的特性
child: {}, // 表征當前工作單元的下一工作單元
sibling: {}, // 表征當前工作單元的下一工作單元
return: {}, // 表示處理完成后返回結果所要合并的目標,通常指向父節點
}
```
### 舉個例子
當前頁面包含一個列表,通過該列表渲染出一個 button 和一組 Item,Item 中包含一個 div,其中的內容為數字。通過點擊 button,可以使列表中的所有數字進行平方。另外有一個按鈕,點擊可以調節字體大小。

頁面渲染完成后,就會初始化生成一個`fiber-tree`,這一過程與初始化 Virtual DOM Tree 類似。

同時,React 還會維護一個`workInProgressTree`,`workInProgressTree`用于計算更新,完成 reconciliation 過程。

用戶點擊平方按鈕后,利用各個元素平方后的 list 調用 setState,React 會把當前的更新送入 list 組件對應的`update queue`中。但是 React 并不會立即執行對比并修改 DOM 的操作。而是交給 scheduler 去處理。
scheduler 會根據當前主線程的使用情況去處理這次 update。為了實現這種特性,使用了`requestIdelCallback`API。對于不支持這個 API 的瀏覽器,react 會加上 pollyfill。
總的來講,通常,客戶端線程執行任務時會以幀的形式劃分,大部分設備控制在 30-60 幀是不會影響用戶體驗;在兩個執行幀之間,主線程通常會有一小段空閑時間,`requestIdleCallback`可以在這個 **空閑期(Idle Period)** 調用 **空閑期回調(Idle Callback)**,執行一些任務

1、低優先級任務由`requestIdleCallback`處理;
2、高優先級任務,如動畫相關的由`requestAnimationFrame`處理;
3、`requestIdleCallback`可以在多個空閑期調用空閑期回調,執行任務;
4、`requestIdleCallback`方法提供 deadline,即任務執行限制時間,以切分任務,避免長時間執行,阻塞 UI 渲染而導致掉幀;

整個過程,簡單來說,先通過`requestIdleCallback`獲得可用的時間片,然后檢查節點的`update queue`看是否需要更新,每處理完一個節點都會檢查時間片是否用完,如果沒用完,根據其保存的下一個工作單元的信息處理下一個節點。詳細過程見第四個參考鏈接。
# 參考資料
[React Hook 探究](https://www.jianshu.com/p/d6e2bd342476)
[官方文檔](https://react.docschina.org/docs/hooks-reference.html)
[React Fiber 原理](https://segmentfault.com/a/1190000018250127?utm_source=tag-newest)
[https://juejin.im/post/5ab7b3a2f265da2378403e57#heading-2](https://juejin.im/post/5ab7b3a2f265da2378403e57#heading-2)
[https://usehooks.com/ 其他人使用 Hook 的小技巧](https://usehooks.com/)
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs