HOOKS
Hook 是 React 16.8 中的新增功能。它们允许您在不编写类的情况下使用状态和其他 React 功能。HOOKS 只能在函数组件中使用
memo(reactComp, compare)
React.memo 是一个高阶的组件。它类似于 React.PureComponent 。不同的是,它只对组件接受的 props 进行浅比较,从而判断是否需要重新渲染的。
如果我们希望自己定义这个过程,这个时候我们可以借助第二个参数来实现;此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。
function Foo (props) {
...
}
export default React.memo(Foo)
React.PureComponent 如何实现性能优化的
我们都知道类组件的中有个 shouldComponentUpdate 生命周期函数。当这个函数返回 false 时,表示组件不会重新渲染;当返回 true 时,表示组件会重新渲染。PureComponent 组件就是在 shouldComponentUpdate 函数中对组件接受的 props 进行比较(props 和 nextProps 进行比较)如果发生变化就返回 true。同时还有自身的 state 数据也会进行一个比较(state 和 nextState),如果发生变化就返回 true。如果上面两种数据的个自比较都返回 false,那么组件就不会发生渲染,减少不必要的渲染,从而达到性能的优化。推荐一篇文章React PureComponent 源码解析大家自己看看。
下面是我自己实现的一个 shallowEqual 方法(理解源码的大致思路就可以了)
function is(v1, v2) {
if (v1 === 0) {
return 1 / v1 === 1 / v2;
}
if (v1 !== v1) {
return v2 !== v2;
}
return v1 === v2;
}
function shallowEqual(pre, next) {
if (is(pre, next)) return true;
if (typeof pre !== 'object' || typeof next !== 'object') return false;
let preKeys = Object.keys(pre);
let nextKeys = Object.keys(next);
if (preKeys.length !== nextKeys.length) return false;
for (let i = 0; i < preKeys.length; i++) {
if (!next.hasOwnProperty(preKeys[i]) || pre[preKeys[i]] !== next[preKeys[i]]) return false;
}
return true;
}
useState
类似于类组件中的state,不同的是 useState 接受一个任意类型的值 string, array, object, bool... 作为参数并返回一个数组,且 useState 只会在组件初始化的时候执行。
// 初始化的时候,age的值就是useState中参数的值
const [ age, setAge ] = useState(20);
const [ visible, setVisible ] = useState(props.visible);
数组中的第一个元素是状态值,组件在运行过程中会保留这个状态值,类似于 this.state
数组中的第二个元素是改变这个状体值的函数,类似于 this.setState()
function Hooks(props) {
const [ age, setAge ] = useState(20);
const [ visible, setVisible ] = useState(props.visible);
return (
<div className="">
<p>我的年龄是{age}岁</p>
<button onClick={() => setAge(age + 1)}>点击</button>
<p>{`${visible}`}</p>
</div>
);
};
我们可以在函数组件中多次使用 useState,来创建多个状态值供我们使用。但是,必须在函数作用域的最顶层使用 useState,不能嵌套在循环内部或者其他函数作用域内部或者是块级作用域中。
useState 接受一个函数作为参数时;
// 你应该注意到了,这是一个自定义的钩子
function useReducer(reduce, initData) {
// 接受一个函数
const [state, setState] = useState(() => initData);
function dispatch(action) {
if (typeof action === 'function') {
setState(preState => reduce(preState, action(preState)));
} else {
setState(reduce(state, action));
}
}
return [state, dispatch];
}
上面这种情况一般出现在,initData 数据结构复杂数据量比较多时,我们可以使用函数调用的形式生成。这样,这个函数只会在组件初始化的时候调用,避免每次渲染的时候重新生成。
useEffect
对于使用过类组件的同学来说,我们可以理解为 useEffect 是类组件中 componentDidMount 和 componentDidUpdate 两个生命周期的一个集合。默认情况下每次当函数组件挂载成功或者重新渲染完成后都会调用 useEffect 。 不同的地方在于,useEffect 有延迟,这个触发的时间节点大约在父组件 didMount 或 didUpdate 后,但在任何新渲染之前触发。所以每当 effect 运行时,DOM 都已经更新完毕。useEffect 可以在组件中使用多次, 和 useState 使用一样。
useEffect 还可以返回一个函数,并在组件即将销毁时调用这个返回函数,是的,和类组件的 componentWillUnmount 一样。我们通过它来取消在 useEffect 中绑定的事件监听等行为。
一般情况下,我们可以将一个行为事件的绑定和取消绑定放在同一个 useEffect 中,这样代码的可读性和维护行会更强一些。
function Hooks(props, ref) {
const boxRef = useRef(null);
useEffect(() => {
function handle() {
console.log(123456)
}
boxRef.current.addEventListener('click', handle, false);
return () => {
boxRef.current.removeEventListener('click', handle, false);
}
}, []);
return (
<div ref={boxRef}>
12344556
</div>
);
}
通过下面的这张图,可以看出来 useEffect 的有一个延迟

useEffect也可以接收一个数组作为第二个参数
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。
当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
如果参数中有多个元素 [ age, visible ] ,组件渲染时通过比较后只要有一个元素发生变化,useEffect 就会执行。当参数是一个空数组时,那么这个时候 useEffect 就和类组件中的 componentDidMount 一样,只在组件挂载成功后调用一次。 useEffect 函数中 return 的函数,不受第二个参数的影响,仍在组件即将销毁的时候调用。
无论是否接受了第二个参数,useEffect 总会在组件挂载成功后调用一次,这一点不能忘记。
不要在循环条件或嵌套函数中调用 Hook。相反,始终在 React 函数的顶层使用 Hooks。通过遵循此规则,您可以确保每次组件呈现时都以相同的顺序调用Hook 。这就是React 允许多个 useState 和 useEffect 调用之间正确保留 Hook 状态的原因。
useLayoutEffect
和 useEffect 使用原理相同,但是唯一的区别在于 useLayoutEffect 不会延迟触发,和类组件的 componentDidMount 和 componentDidUpdate 这两个生命周期函数基本处于同步的状态。但都需要注意下面的几个问题:
useLayoutEffect 和 useEffect 返回的函数是会在该 effect 下次需要重新渲染之前执行一次。(这可能不是我们预期的)
useLayoutEffect(() => {
console.log('useLayoutEffect');
// 当 visible 发生变化,该钩子会重新渲染一次。函数 cb 将在该钩子渲染前执行。
return function cb () {
console.log('@@@@@@@@@@@@@@@@@@')
}
}, [visible]);
在 effect 中调用了一个外部声明的函数,而且该函数依赖 props 或 state,这时会发现得不到预期的结果,怎么办???
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp);
}
doSomething();
}, [someProp]); // 安全(我们的 effect 仅用到了 `someProp`)
}
只有当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。
如果我的 effect 的依赖频繁变化,我该怎么办?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}
上面的 demo 是官网的一个案例;我觉的很经典,借鉴一下;
传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。
指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置(effect 重新渲染前会执行一次它返回的函数)。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该如何改变而不用引用当前 state
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // 在这不依赖于外部的 `count` 变量
}, 1000);
return () => clearInterval(id);
}, []); // 我们的 effect 不适用组件作用域中的任何变量
return <h1>{count}</h1>;
}
customize hooks
自定义 Hook 是一个 JavaScript 函数,其名称以 "use" 开头,可以调用其他 Hook。构建自己的 Hook 可以将组件逻辑提取到可重用的函数中
,确保只在自定义 Hook 的顶层无条件地调用其他 Hook。与 React 组件不同,自定义 Hook 不需要具有特定签名。我们可以决定它作为参数需要什么,以及它应该返回什么(如果有的话)
// useVisibleStatus 是一个自定义的钩子,我们在函数中调用的useEffect
function useVisibleStatus(isShow) {
const [ visible, setVisible ] = useState(isShow);
useEffect(() => {
setVisible(isShow);
}, [ isShow ]);
return visible;
};
function Hooks(props) {
const [ count ] = useState(props.count);
const visible = useVisibleStatus(props.visible);
return (
<div className="">
<h2>{count}</h2>
<button onClick={() => setCount(count + 1)}>点击</button>
<h2>{`${visible} ${props.count}`}</h2>
</div>
);
}
我们也可以将一些复杂或者重复的逻辑提取提取到自定义的 Hook 函数中,从而简化我们的代码。其实自定义 hook 和函数组件没有多大区别。
useReducer
当 useState 复杂的状态逻辑涉及多个子值或下一个状态取决于前一个状态时,通常 useReducer 更可取。useReduce 还可以让您优化触发深度更新的组件的性能
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
我们看看 useReducer 具体的实现(自定义一个 useReducer 钩子):
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
// 这里我们也模拟一下 setState 的函数式更新形式
// 这样就可以不需要在 effect 中使用时强制加上依赖
if (typeof action === 'function') {
setState(preState => reducer(preState, action(preState)))
} else {
setState(reducer(state, action));
}
}
return [state, dispatch];
}
useImperativeHandle
可以通过 useImperativeHandle ,给 ref 上绑定一些自定的事件暴露给父组件,前提是我们必须联合 forwardRef 一起使用,注意所有的事件都是绑定在 ref 的 current 属性上。看下面的例子
// hook.js
function Hooks(props, ref) {
const [ count, setCount ] = useState(props.count);
useImperativeHandle(ref, () => ({
// 自定义一些事件
click: () => {
setCount(count + 1);
},
}), [count]); // 我们可以指定依赖项,也可以不指定。这和 useCallback | useMemo 等 Hooks 的依赖项作用一样
return (
<div className="">
<h2>{count}</h2>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
};
export default React.forwardRef(Hooks);
// Application.js
export default class App extends PureComponent {
componentDidMount() {
this.ref = React.createRef();
}
return (
<div
onClick={() => this.ref.current.click()}
>
// ...
<Hooks ref={this.ref} count={this.state.count} visible={this.state.visible}/>
// ...
</div>
);
}
也可以这样
function FancyInput(props, ref) {
// 获取真是DOM节点
const inputRef = React.useRef();
useImperativeHandle(ref, () => ({
// 自定义一些事件
focus: () => {
// 在DOM节点执行一些操作都可以
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
FancyInput = React.forwardRef(FancyInput);
useRef
useRef 返回一个可变的ref对象,其 current 属性值为初始化传递的参数(initialValue)。返回的对象将持续整个组件的生命周期。和类组件中的实例属性很像
const ref = usRef(20);
console.log(ref.current) // 20
// 可以重新赋值
ref.current = 200;
当然最常见的就是访问一个元素节点
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useMemo
使用的场景:函数组件中,我们定义了一些方法,但是我们并不希望每次组件更新的时候都重新计算一个值,那么这个时候我们就可以使用 useMemo。有个地方需要注意点那就是,useMemo 会在组件挂载完成之前执行一次,这个时间节点类似 class 组件中的 componentWillMount 类似。
当 useMemo 只接受了一个参数时,那么每次函数组件更新时,useMemo 都会重新计算。看下面
// 每次都会重新计算 count
const count = useMemo(() => {
return (state.count || 0).toFixed(2);
});
当 useMemo 接受了两个参数时。也就是说有依赖项了,那么只要依赖项发生变化,useMemo都会重新计算
// 当 state.count 发生变化时,从新计算 count
const count = useMemo(() => {
return (state.count || 0).toFixed(2);
}, [state.count]);
如果依赖项为 []。 那么只会在组件刚开始渲染时计算一次。
可以查看的我们demo
// 组件初始化的时候会调用 `Func`,类似 `componentWillMount``
// 当依赖项元素中的值发生改变,那么就会调用 `Func`,这个条件 a 和 b 有一个发生变化的时候 就会触发 `useMemo`
useMemo(() => Func(a, b), [a, b]);
在看这个带返回值的
function Hooks(props) {
const [ count, setCount ] = useState(props.count);
useLayoutEffect(() => {
console.log('useLayoutEffect 后执行');
setCount(props.count);
}, [ props.count ]);
const dom = useMemo(() => {
console.log('useMemo 优先执行');
return <h2>{count * 10}</h2>;
}, [count]);
return (
<div className="">
<h2>{count}</h2>
<button onClick={() => setCount(count + 1)}>点击</button>
{dom}
</div>
);
}
注意:传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffec 的适用范畴,而不是 useMemo
useCallback
useCallback 的使用和 useMemo 是一样的,且 useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
这是我的demo
useContext
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
调用了 useContext 的组件总会在 context 值变化时重新渲染
const Thems = {
light: {
color: '#f90',
},
dack: {
color: '#222',
}
}
const ThemsContext = React.createContext({
them: Thems.light,
toggleTheme: () => {},
})
class Detail extends PureComponent {
state = {
context: {
them: Thems.light.color,
toggleTheme: this.handle,
}
}
handle = () => {
const { context: { them } } = this.state;
let color = Thems.light.color;
if (them === Thems.light.color) {
color = Thems.dack.color
}
this.setState({
context: {
...this.state.context,
them: color,
},
});
}
render() {
return (
<ThemsContext.Provider value={this.state.context}>
<Hooks/>
</ThemsContext.Provider>
);
}
}
function Hooks(props, ref) {
const themContext = React.useContext(ThemsContext);
return (
<div
style={{ background: themContext.them }}
onClick={() => {
themContext.toggleTheme()
}}
>
1234567890
</div>
);
}
上面的这个 demo 中,当 ThemsContext.Provider 的 value props 发生变化时 Hooks 组件就会发生重新渲染。所以说这个时候我们的目的就已经达到了。