一:簡介
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
hook 引入時間 (react 版本更新大事記)
- 2013年05月29日 v0.3.0 Facebook 開源了React
…..
- 2014年10月28日 v0.12.0 使用BSD協定+附加專利授權協定- 2016年03月30日 v0.14.0 拆分成React 和 ReactDOM
- 2016年04月09日 v15.0.0 挂載元件方式改動、SVG相容等
- 2017年09月25日 v15.6.2 開源協定改為MIT
- 2017年09月26日 v16.0.0 引入Fiber
- 2019年02月06日 v16.8.0 Hook 引入
- 2020年10月20日 v17.0.0 Concurrent Mode、底層技術改造、解決一些曆史包袱
在Hook之前
函數元件
function Welcome(props) {
return <h1>hello, {props.name}</h1>
}
類元件
有了Hook後
函數元件能夠完成和類元件一樣的功能,有自己的狀态,生命周期以及狀态控制能力
let timer = null
export default function Clock() {
const [date, setDate] = useState(new Date())
useEffect(() => {
timer = setInterval(() => {
setDate(new Date())
}, 1000)
return () => {
clearTimeout(timer)
};
}, [])
return <div>
<h1>Hello,world!</h1>
<h2>It is {date.toLocaleTimeString()}.</h2>
</div>
}
二:React 提供的内置Hook API清單
useState
useEffect
useContext
useRe
useCallback
useMemo
useReducer
useLayoutEffect
useImperativeHandle
useDebugValue
useTransition
useDeferredValue
三:Hook API詳解
-
useState
源碼定義
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
使用示例
const [data, setData] = useState(function () {
return JSON.parse(bigData)
})
如果傳入函數,函數隻在第一此初始化元件的時候執行,後面元件update的時候不會重複計算,适合對初始值需要進行大量計算的場景的優化的時候使用
- useEffect
源碼定義
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
功能:
- 處理副作用
- 實作生命周期
- 粒度可控
使用示例
useEffect 作為副作用處理函數,可以模拟 componentDidMount 已經componentDidUpdate等生命周期,
- useLayoutEffect
源碼定義
export function useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useLayoutEffect(create, deps);
}
功能:
- 處理副作用
- 實作生命周期
- 粒度可控
使用方法和useEffect一緻
差別:會阻塞浏覽器渲染,比useEffect先執行
使用場景:
不想讓使用者看到dom變化過程,副作用回調函數裡有dom更新操作适合使用
示例代碼
對比下面兩組代碼,一個是使用的useEffect ,一個是使用的useLayoutEffect
import { React } from '../adaptation'
// import { useEffect } from './react/packages/react'
const { useLayoutEffect, useEffect, useState } = React
export default function TestUseLayoutEffect(props) {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(0)
}
useLayoutEffect(() => {
if (count === 0) {
// 耗時操作 start
const arr = []
for (let i = 0; i < 100000000; i++) {
arr.push(i)
}
// 耗時操作 end
setCount(Math.random())
}
}, [count])
return <div >
{count}
<button onClick={handleClick}>改變</button>
</div>
}
import { React } from '../adaptation'
// import { useEffect } from './react/packages/react'
const { useEffect, useState } = React
export default function TestUseEffect(props) {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(0)
}
useEffect(() => {
if (count === 0) {
// 耗時操作 start
const arr = []
for (let i = 0; i < 100000000; i++) {
arr.push(i)
}
// 耗時操作end
setCount(Math.random())
}
}, [count])
useEffect(() => {
console.log('xxx123')
})
return <div >
{count}
<button onClick={handleClick}>改變</button>
</div>
}
- useRef
源碼定義
export function useRef<T>(initialValue: T): {| current: T |} {
const dispatcher = resolveDispatcher();
return dispatcher.useRef(initialValue);
}
示例
const { useRef } = React
export default function TestRef() {
const myRef = useRef(null)
const handleClick = () => {
console.log(myRef.current)
}
return <div>
<button ref={myRef} onClick={handleClick}>click</button>
</div>
}
useRef vs createRef()
功能類似
useRef 會在整個生命周期中保持引用修改useRef current的值不會觸發重新渲染
createRef 每次重新渲染值都會變化
- useMemo 和 useCallback
源碼定義
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}
useMemo 避免更新時重複計算值
useCallback 避免重新定義函數
示例:
下面這個例子,無論更新多少次,隻要name不變,就不會執行genWelcomeStr方法
import { React } from '../adaptation'
const { useMemo } = React
export default function TestMemo(props) {
const { name, count } = props
const genWelcomeStr = () => {
console.log('genWelcomeStr called')
return 'Hello,' + name
}
const welcomeStr = useMemo(genWelcomeStr, [name])
// const welcomeStr = genWelcomeStr()
return <div>
{count}
{welcomeStr}
</div>
}
下面兩組代碼,通過父子元件 useCallback 和useMemo的配合,隻要name不變,useCallback包裹的函數指向就不會變,子元件中通過useMemo來監聽傳入的方法,隻要方法指向不變就不會重複計算
import { React } from '../adaptation';
// import TestMemo from './TestComponents/TestMemo'
import TestUseCallback from './TestUseCallback';
const { useState, useCallback } = React
function TestUseCallbackInApp() {
const [count, setCount] = useState(0)
const [name, setName] = useState('bob')
const handleClick = (count) => {
setCount(++count)
}
const handleGenStr = useCallback(() => {
return 'name: ' + name
}, [name])
return <div className="App">
<TestUseCallback handleGenStr={handleGenStr} />
{count}
<button onClick={() => handleClick(count)}>add</button>
<button onClick={() => setName('jack' + Date.now())}>changeName</button>
</div>
}
export default TestUseCallbackInApp;
import { React } from '../adaptation'
const { useMemo } = React
export default function TestUseCallback(props) {
const { handleGenStr } = props
const renderStr = useMemo(() => {
console.log('handleGenStr called')
return handleGenStr()
}, [handleGenStr])
console.log('child comp render')
return <div >
{renderStr}
</div>
}
useMemo和useCallback 适合在大資料量等極端情況來做優化,否則普通資料量下,優化效果不大,甚至由于多了很多監聽邏輯,反而使代碼體積和性能變得更糟,甚至引起bug,底層引擎的速度可以抵消這一點性能差異。
-
useReducer
源碼定義
export function useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
示例
- useContext
源碼定義
export function useContext<T>(
Context: ReactContext<T>,
unstable_observedBits: number | boolean | void,
) {
const dispatcher = resolveDispatcher();
if (__DEV__) {
if (unstable_observedBits !== undefined) {
console.error(
'useContext() second argument is reserved for future ' +
'use in React. Passing it is not supported. ' +
'You passed: %s.%s',
unstable_observedBits,
typeof unstable_observedBits === 'number' && Array.isArray(arguments[2])
? '\n\nDid you call array.map(useContext)? ' +
'Calling Hooks inside a loop is not supported. ' +
'Learn more at https://fb.me/rules-of-hooks'
: '',
);
}
// TODO: add a more generic warning for invalid values.
if ((Context: any)._context !== undefined) {
const realContext = (Context: any)._context;
// Don't deduplicate because this legitimately causes bugs
// and nobody should be using this in existing code.
if (realContext.Consumer === Context) {
console.error(
'Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' +
'removed in a future major release. Did you mean to call useContext(Context) instead?',
);
} else if (realContext.Provider === Context) {
console.error(
'Calling useContext(Context.Provider) is not supported. ' +
'Did you mean to call useContext(Context) instead?',
);
}
}
}
return dispatcher.useContext(Context, unstable_observedBits);
}
示例
解決函數元件狀态共享問題
避免了層層傳遞
- 其他
- useDebugValue 給hook 加一個調試專用的名字
function useMyCount(num) {
const [ count, setCount ] = useState(0);
useDebugValue('myHook');
const myCount = () => {
setCount(count + 2);
}
return [ count, myCount ];
}
- useImperativeHandle 限制ref對應dom執行個體對外暴露的内容
import { React } from '../adaptation'
const { useRef,useImperativeHandle,forwardRef } = React
const TestImperative = forwardRef((props, ref) => {
const myRef = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => {
myRef.current.focus()
}
}))
return <div>
<input ref={myRef}/>
</div>
})
export default TestImperative
import { React } from '../adaptation';
// import TestMemo from './TestComponents/TestMemo'
import TestImperative from './TestUseImperative';
const { useEffect, useRef } = React
function TestUseImperativeInApp() {
const myRef = useRef(null)
useEffect(() => {
console.log(myRef)
myRef.current.focus()
}, [])
return <div className="App">
<TestImperative ref={myRef}/>
</div>
}
export default TestUseImperativeInApp;
運作結果
- useTransition 元件延遲過渡鈎子
- useDeferredValue 監聽狀态,延遲過度更新
四:常見問題
- 為什麼不能在條件、嵌套或者循環語句中定義hook
這是hook連結清單的基礎結構:
type Hooks = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any> | null
queue: UpdateQueue<any> | null
next: Hook | null, // link 到下一個 hooks,通過 next 串聯每一 hooks
}
僞代碼描述代碼和hook資料結構的對應關系:
hook是初始化的時候按照連結清單的方式逐行設定next,如果中途插入了條件語句,第一次初始化生成的連結清單順序 跟更新時候的實際useState定義的順序可能會不一緻,導緻bug出現。
-
擁有Hook的函數元件是否可以完全替代Class元件?
“官方推薦使用hook來編寫元件”
- hook 能模拟class 元件的所有生命周期嗎?參考下圖:
class元件 | Hooks元件 |
---|---|
constructor | useState的時候 |
getDerivedStateFromProps | useEffect 配置依賴的變量,會自定按照依賴的新舊對比控制更新 |
shouldComponentUpdate | React.memo 緩存render的值,配置依賴 |
render | 函數傳回的内容 |
componentDidMount | useEffect 依賴配置成空數組 |
componentDidUpdate | useEffect 通過useRect 緩存一個記錄第一次更新的變量來控制隻在更新的時候執行 |
componentWillUnmount | useEffect第一個參數傳回一個函數,這個函數将在unmount的時候執行 |