本文参考 React Hooks 解析(上):基础、React Hooks 解析(下):进阶、React Hooks 入门教程、超性感的React Hooks(十一)useCallback、useMemo、react 官网
React v16.8 版本引入的新 api,目的是增强函数组件,使函数组件具备状态管理的能力
State hook (useState)
const [state, setState] = useState(initialState);
获取需要的 state 和 更新 state 的方法- initialState 当前 state 的值,可以是一个函数,函数的返回值将作为 state 的值,参数只会在组件的初始渲染中起作用
- 返回值:返回的是一个数组,第一个是当前 state 的值,第二个是更新 state 的方法
/// index.jsx
import React, { useState, useEffect } from 'react';
const Index = () => {
const [count, setCount] = useState(0); //
useEffect(() => {
console.log(`count: ${count}, age: ${age}, work: ${work}`);
})
return (
<div>
<p>hooks</p>
<p>{count}</p>
<button onClick={() => {setCount(count+1)}}>增加</button>
</div>
);
}
export default Index;
Effect Hook
useEffect
就是一个 Effect Hook
,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API
useEffect 会在每次 DOM 渲染后执行,不会阻塞页面渲染。
在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。
useEffect(() => { // Async Action }, [dependencies])
默认情况下,它在第一次渲染之后和每次更新之后都会执行- 第一个参数是一个函数,通常用来编写异步操作的代码。如果这个函数返回了一个函数,那么这个返回的函数,会在组件卸载前执行。
- 第二个参数是一个数组,用于给出 Effect 的依赖项,只有当数组发生变化时,
useEffect()
才会执行。第二个参数如果省略,这时组件只要发生了渲染,就会执行useEffect()
。如果传入的是一个[]
数组,只会在第一次挂载和卸载是调用useEffect()
/// index.jsx
import React, { useState, useEffect } from 'react';
const List = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('list 显示');
return () => {
console.log('list 隐藏');
} // return 的代码会在卸载前执行
}, [count]); // 这里控制只有在 count 改变时,执行 useEffect 方法
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count+1) }>增加</button>
</div>
)
}
const Index = (props) => {
const [showList, setShowList] = useState(true)
return (
<div>
<p>hooks</p>
{showList && <List/>}
<button onClick={() => {setShowList(false)}}>隐藏</button>
<button onClick={() => {setShowList(true)}}>显示</button>
</div>
);
}
export default Index;
useContext
const value = useContext(MyContext)
共享状态钩子- MyContext 是 React.createContext 的返回值
const context = React.createContext()
返回具有两个值的对象{ Provider, Consumer }
- 返回值 context 中的内容
- MyContext 是 React.createContext 的返回值
/// index.js
import React, {createContext} from 'react';
import ReactDOM from 'react-dom';
import Index from './pages/index'
const appContext = createContext({}); // 创建一个 context 对象
ReactDOM.render(
<appContext.Provider value={{ // appContext.Provider 提供了一个 Context 对象,这个对象可以被子组件共享,共享的值由 value 设定
userName: 'app'
}}>
<Index context={appContext} /> // 由于 useContext 需要指明从哪获取 context值,所以把 context 对象,当作 props 值往子组件中传递
</appContext.Provider>,
document.getElementById('root')
);
...
/// index.jsx
import React, { useContext } from 'react';
const Index = (props) => {
console.log(props);
const context = useContext(props.context); // useContext 需要传入一个 context 对象,这里是通过组件的属性进行传递
console.log(context);
return (
<div>
<p>hooks</p>
<p>{context.userName}</p>
</div>
);
}
export default Index;
如果是那种父子组件需要多层传递数据,上面的写法就会显得比较麻烦,可以通过导出 context 对象的方式来优化代码
/// index.js
export const appContext = createContext({}); // 导出 context 对象
ReactDOM.render(
<appContext.Provider value={{
userName: 'app'
}}>
<Index /> // 删除 props
</appContext.Provider>,
document.getElementById('root')
);
...
/// index.jsx
import { appContext } from '../index' // 引入刚刚导出的 context 对象
...
const context = useContext(appContext); // 这样也可以获取 context 对象的值
useReducer action 钩子
const [state, dispatch] = useReducer(reducer, initialState);
- reducer: Reducer 函数
- initialState: 状态初始值
- 返回值 返回一个数组,数组的第一额成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数
/// 使用时,可以完全按照 redux 的思路进行管理
/// store/types.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
...
/// store/reducers/index.js
import {INCREMENT, DECREMENT} from '../types';
export const defaultState = { // 注意此处和普通 redux 文件的不同,普通 redux,多半会把这个值当成 state 的默认值
name: 'main',
count: 0
}
export default (state, action) => { // 这里没有默认值,useReducer 要求默认值通过第二个参数传递
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + 1
}
case DECREMENT:
return {
...state,
count: state.count - 1
}
default:
return state;
}
}
...
/// store/actionCreator.js action的工厂函数
import { INCREMENT, DECREMENT } from './types';
export const increment = () => ({
type: INCREMENT
})
export const decrement = () => ({
type: DECREMENT
})
...
/// pages/index.jsx
import React, { useReducer } from 'react';
import reducer, { defaultState } from '../store/reduers/index';
import { increment, decrement } from '../store/actionCreator';
console.log(reducer);
const Index = (props) => {
const [state, dispatch] = useReducer(reducer, defaultState); // 第一个参数是 reducer 函数,第二个是默认值
return (
<div>
<p>hooks</p>
<p>{state.count}</p>
<button onClick={() => {dispatch(increment())}}>增加</button>
<button onClick={() => {dispatch(decrement())}}>减少</button>
</div>
);
}
export default Index;
useLayoutEffect
使用方法和 useEffect
一致,唯一的区别在于执行时机。 useEffect
是非阻塞会在DOM渲染完后执行,useLayoutEffect
会阻塞 DOM 渲染,方法执行完成后,继续 DOM 渲染。
useMemo、useCallback
这两个 hook 都是为了减少不必要的子组件渲染,这类问题进行的两种优化手段
传入的参数一样,得到的结果必定也是一样
import React, {useState, useMemo, useCallback, useEffect, memo} from 'react';
const ShowText = memo(({expensive}) => { // 一个ui展示的组件,memo 的作用就是只有 props 更新时,组件才会重新渲染
console.log('text');
const [count, setCount] = useState(expensive());
useEffect(() => {
setCount(expensive());
}, [expensive]);
return <>
{count}
</>
});
const List = () => {
const [count, setCount] = useState(0);
const [value, setValue] = useState(1); // 有两个state值
function expensive() {
console.log("computed expensive");
let sum = 0;
for (let i = 0; i < count * 10; i++) {
sum += i;
}
return sum;
} // expensive 是依赖 count 计算出的一个“高消耗“的值
console.log('list');
return (
<div>
<p>
{count}-{value}-<ShowText expensive={expensive}/>
</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
增加
</button>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
</div>
);
};
export default List;
按现在的代码运行,只要 count 或 value 更新,都会触发 List 组件的重新渲染,由于父组件的更新,自然就触发了字组件的更新,只是现在的子组件关联的数据其实只有 count(expensive是根据这个值计算而来),也只希望这个值更新时,才需要去触发子组件的更新。
useMemo(() => computeExpensiveValue(a, b), [a, b])
- useMemo缓存计算结果。它接收两个参数,第一个参数为计算过程(回调函数,必须返回一个结果),第二个参数是依赖项(数组),当依赖项中某一个发生变化,结果将会重新计算
- 这种优化有助于避免在每次渲染时都进行高开销的计算。
const callBack = useMemo(expensive, [count]); // useMemo是缓存函数的计算结果,如果 count 更新,才会重新去调用 expensive 函数
return (
<div>
<p>
{count}-{value}-<ShowText expensive={callBack}/>
...
// 由于传入的是具体值,相关使用调整下,这样props不变,使用了 memo 的关系,子组件也不会重新渲染,如果不使用memo,虽然 callBack 不会执行,但子组件还是会重新渲染下
const ShowText = memo(({expensive}) => {
console.log('text');
const [count, setCount] = useState(expensive);
useEffect(() => {
setCount(expensive);
}, [expensive]);
return <>
{count}
</>
});
useCallback
- useCallback 的使用几乎与 useMemo 一样,不过 useCallback 缓存的是一个函数体,当依赖项中的一项发现变化,函数体会重新创建
const callBack = useCallback(expensive, [count]); // 和 useMemo 一样的使用方式,这个方法,返回是方法缓存
...
const ShowText = memo(({expensive}) => {
console.log('text');
const [count, setCount] = useState(expensive()); // 把传入的值当作函数使用
useEffect(() => {
setCount(expensive());
}, [expensive]);
return <>
{count}
</>
});
useMemo 和 useCallback 都是记忆函数,记忆函数会利用闭包,在确保返回结果一定正确的情况下,减少了重复冗余的计算过程。只不过记忆函数会造成额外的内存消耗,所以在使用时要考虑清楚,这种消耗带来的收益划不划算,否则达不到优化的目的
useRef
用来获取dom元素,或跨渲染周期保存数据
import React, {useState, useRef, useEffect} from 'react';
const List = () => {
const [count, setCount] = useState(0);
const [value, setValue] = useState(1);
const inputElem = useRef(); // 设定一个空的 useRef
const stayTime = useRef(1); // 内部可以给定一个初始值
const timeId = useRef();
// useRef上可以存储不影响组件渲染的变量值,而且期间不受组件重绘的影响,比如这里设置了一些条件,那么后续在 10 秒的范围内无论你点击多次下按钮,下面这部分的定时不受影响,所以如果你需要一些在初始化后在组件内一直有效的变量,就可以使用 useRef 进行定义
useEffect(() => {
console.log(stayTime.current);
if (stayTime.current === 1) {
timeId.current = setInterval(() => {
stayTime.current = stayTime.current + 1;
if (stayTime.current > 10) {
clearInterval(timeId.current);
console.log("等待10秒");
}
}, 1000);
}
});
useEffect(() => {
console.log(inputElem.current); // 这是普通用法,可以直接获取对应的 DOM 元素
})
return (
<div>
<p>
{count}-{value}
</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
增加
</button>
<input
type="text"
value={value}
ref={inputElem} // 使用定义的 useRef
onChange={e => setValue(e.target.value)}
/>
</div>
);
}
export default List;
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
使用 ref 时自定义暴露给父组件的实例值,这个 hook 应当和 React.forwardRef(这个方法可以简单的理解为解决组件件传递 ref 的问题) 配合使用(这样做会减少暴露给父组件的属性)- ref 通过 forwardRef 引用父组件的 ref 实例
- createHandle 回调函数,返回一个对象,对象里存储要暴露给父组件的属性或方法
import React, {useRef, forwardRef, useEffect} from 'react';
const FancyInput = forwardRef((props, ref) => (
<input ref={ref} type="text" placeholder='输入姓名' />
))
const List = () => {
const inputElem = useRef();
useEffect(() => {
inputElem.current.focus(); // 这里可以通过 ref 的能力,直接获取到组件中的 DOM 元素
})
return (
<div>
<FancyInput ref={inputElem} />
</div>
);
}
export default List;
不过现在有个问题,就是子组件无法指定自己希望暴露出哪些方法或者属性。此时FancyInput 组件并没有进行任何设置,而 List 组件中直接就使用了 DOM 元素的方法和属性。所以当希望控制父组件的调用范围,或者对某些方默认方法进行重写覆盖,就可以使用 useImperativeHandle
import React, {useRef, forwardRef, useEffect, useImperativeHandle} from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(); // 组件内建立一个 ref
useImperativeHandle(ref, () => ({ // 父组件传入的 ref ,这里作为第一个参数使用,第二个参数就是暴露出给父组件使用的方法属性
focus: () => {
console.log('子组件中的事件行为');
inputRef.current.focus()
}
}))
return (
<input ref={inputRef} type="text" placeholder='输入姓名' />
)
});
const List = () => {
const inputElem = useRef();
useEffect(() => {
inputElem.current.focus(); // 这样就控制了父组件中的行为,此时只能调用子组件主动暴露出的方法,如果尝试使用其他方法,会直接报错
})
return (
<div>
<FancyInput ref={inputElem} />
</div>
);
}
export default List;
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签,开发调试中使用
自定义hook
普通的函数,只是其中可能引用了其他 hook,完成特定功能
// 比如这,都可以叫做一个自定义 hook
const useCount = count => {
useEffect(() => {
console.log("use ", count);
});
};