一:简介
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 [a, setA] = useState(0)
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的时候执行 |