Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。html
Hook 解决了如下问题react
使用 Hook 从组件中提取状态逻辑,使得这些逻辑能够单独测试并复用。Hook 使你在无需修改组件结构的状况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。typescript
Hook 将组件中相互关联的部分拆分红更小的函数(好比设置订阅或请求数据),而并不是强制按照生命周期划分。你还可使用 reducer 来管理组件的内部状态,使其更加可预测。编程
this
的工做方式,与其余语言存在巨大差别。Hook 使你在非 class 的状况下可使用更多的 React 特性,它拥抱了函数,同时没有牺牲 React 的精神原则。无需学习复杂的函数式或响应式编程技术。数组
为了让你们快速了解 hook,如下内容涵盖了大部分功能应用场景。性能优化
更多更详细的用法请阅读官方文档。数据结构
如下是一段 useState
的示例,一个计数器组件:异步
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 示例为:ide
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>
);
}
}
复制代码
useState
是一种新方法,它与 class 里面的 this.state
提供的功能彻底相同。useState
可使用数字或字符串对其进行赋值,并不必定是对象。保存多个状态能够屡次调用 useState
。useState
返回当前 state 以及更新 state 的函数,使用数组解构的方式定义这两个变量。useState
返回的更新 state 的函数的标识是稳定的,而且不会在组件从新渲染时发生变化。Effect Hook 可让你在函数组件中执行反作用操做(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于反作用)函数
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
复制代码
咱们为计数器增长了一个小功能:将 document 的 title 设置为包含了点击次数的消息。
useEffect
用于定义每次 React 更新 DOM 以后执行的反作用。useEffect
接收数组做为第二个参数,经过对比数组中的参数发生改变来决定是否执行传给 useEffect
的函数。不传入第二个参数则每次渲染后都执行,传入空数组则表明只在第一次渲染后执行。useEffect
接收的函数能够返回一个函数用于清除反作用。(例如,取消订阅,清理计时器)useEffect
执行前会先清除上一次渲染的反作用。useEffect
实际上涵盖了 class 组件的绝大部分生命周期,详情查看 useEffect 与class 组件生命周期映射关系 一节。
const value = useContext(MyContext);
复制代码
useContext
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。useContext
所在的组件在 provider 的值发生变化时会从新渲染,即使祖先元素使用了 React.memo
或 shouldComponentUpdate
。useContext
仅仅增长了一种使用 Context 的方式,功能上并没有区别。useState
的替代方案。
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() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); } 复制代码
useReducer
和 Redux 很像,接收 reducer 函数、初始值、初始化函数并返回一个完整的 state 和 dispatch 函数。useReducer
适用于管理逻辑复杂且包含多个子值的 state,或者下一个 state 依赖以前的 state 等。useReducer
能给触发深更新的组件作性能优化。(向子组件传递 dispatch 而不是回调函数)dispatch
函数的标识是稳定的,而且不会在组件从新渲染时发生变化。这两个有类似关联之处,放在一块儿。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
复制代码
useCallback
接收内联回调函数和依赖项数组做为参数,它返回该函数的 memoized 版本,回调函数仅在某个依赖项改变时才会更新。useCallback
返回的函数传递给使用 shouldComponentUpdate
或 React.memo
的子组件时能够避免非必要的渲染。useCallback(fn, deps)
至关于 useMemo(() => fn, deps)
。const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码
useMemo
接收 ”计算“ 函数和依赖项数组做为参数,它返回一个 memoized 值,仅在某个依赖项改变时才会从新计算 memoized 值。useMemo
返回的值传递给使用 shouldComponentUpdate
或 React.memo
的子组件时能够避免非必要的渲染。useMemo
有助于避免在每次渲染时都进行高开销的计算。这意味着若是是很简单的计算请谨慎考虑是否使用。useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。 本质上,useRef
就像是能够在其 .current
属性中保存一个可变值的 “盒子”。
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。然而 useRef
不止于此,它比 ref 属性更有用。详情查看 useRef 用例 一节。
请移步官方文档:Hook 规则
实质上这是个错误的标题,仅为了更好的理解 Hook。在行为上等效,并无实质关系。
useEffect
真正实现了让相关联的逻辑都在一处的想法,咱们能够在 useEffect
中设置定时器,在返回的清理函数中清除定时器。没必要像 class 组件同样将这些本应该在一块儿的逻辑分散在各个生命周期中。除此以外,相同的抽象逻辑能够被抽离出来在不一样的函数组件内复用。
如下是生命周期对照表:
class 组件生命周期 | useEffect 示例代码 |
---|---|
componentDidMount | useEffect(() => { // effect here }, []) |
componentDidMount & componentDidUpdate | useEffect(() => { // effect here }) |
componentDidUpdate | useEffect(() => { if (firstRef.current) return; // effect here }) |
componentWillUnMount | useEffect(() => () => { // clear effect here }, []) |
这是咱们最熟悉的使用方式。
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>
</>
);
}
复制代码
function LifeCycleExample() {
const firstMountRef = useRef(true);
useEffect(() => {
if (firstMountRef.current) {
firstMountRef.current = false;
} else {
// effect here
}
})
return (<p>LifeCycleExample</p>);
}
复制代码
若是频繁使用,则能够包装成自定义 Hook:
function useUpdateEffect(effect) {
const firstMountRef = useRef(true);
useEffect(() => {
if (firstMountRef.current) {
firstMountRef.current = false;
} else {
effect();
}
});
}
function LifeCycleExample() {
useUpdateEffect(() => {
// effect here
})
return (<p>LifeCycleExample</p>);
}
复制代码
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return <h1>Now: {count}, before: {prevCount}</h1>;
}
复制代码
若是频繁使用,则能够包装成自定义 Hook:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
复制代码
function Timer() {
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
// ...
});
});
return (
<> <button onClick={() => clearInterval(intervalRef.current)}>中止</button> </> ); } 复制代码
以上只为举例,并不局限于这几种使用方式。
自定义 Hook 是一种天然遵循 Hook 设计的约定,而并非 React 的特性。自定义 Hook 必须使用 use 开头的方式命名。经过自定义 Hook,能够将组件逻辑提取到可重用的函数中。实际上在上一节 useRef 用例 中已经使用了自定义 Hook。
自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定很是重要。不遵循的话,因为没法判断某个函数是否包含对其内部 Hook 的调用,React 将没法自动检查你的 Hook 是否违反了 Hook 规则。
这个很少说,不须要本身实现,官方直接提供。
这一层在工程层面实现,咱们使用 immutable 库将官方提供的基础 Hook 包装一层,便于使用。immutable 并非为 Hook 专门准备的,在 class 组件中咱们也能够用相似的库对状态进行包装。可是 Hook 这种能够将状态逻辑和组件分离的能力,提供了更好的封装的可能性。这一层将会很优雅,不会增长开发中的理解难度。
一般咱们须要这样更新深层次的状态:
setState((oldValue) => ({
...oldValue,
foo: {
...oldValue.foo,
bar: {
...oldValue.foo.bar,
alice: newAlice
},
},
}));
复制代码
封装后,直接修改状态由 immer 保证数据不可变性:
const [state, setState] = useImmerState({foo: {bar: 1}});
setState(s => s.foo.bar++);
复制代码
const [state, dispatch] = useImmerReducer(
(state, action) => {
case 'ADD':
state.foo.bar += action.payload;
case 'SUBTRACT':
state.foo.bar -= action.payload;
default:
return;
},
{foo: {bar: 1}}
);
dispatch('ADD', {payload: 2});
复制代码
这个很好理解,好比将 Array
、Map
、Set
等复杂数据结构封装为 hook。
这里使用一个 typescript 的接口定义来体现:
const [list, methods, setList] = useArray([]);
interface ArrayMethods<T> {
push(item: T): void;
unshift(item: T): void;
pop(): void;
shift(): void;
slice(start?: number, end?: number): void;
splice(index: number, count: number, ...items: T[]): void;
remove(item: T): void;
removeAt(index: number): void;
insertAt(index: number, item: T): void;
concat(item: T | T[]): void;
replace(from: T, to: T): void;
replaceAll(from: T, to: T): void;
replaceAt(index: number, item: T): void;
filter(predicate: (item: T, index: number) => boolean): void;
union(array: T[]): void;
intersect(array: T[]): void;
difference(array: T[]): void;
reverse(): void;
sort(compare?: (x: T, y: T) => number): void;
clear(): void;
}
复制代码
在有了基本的数据结构后,能够对场景进行封装,如 useVirtualList 就是一个价值很是大的场景的封装。须要注意的是,场景的封装不该与组件库耦合,它应当是业务与组件之间的桥梁,不一样的组件库使用相同的 Hook 实现不一样的界面,这才是一个理想的模式。
业务中比较经常使用的场景 Hooks:
上一节中 Hook 分层设计 已经从某种程度上解决了一部分 Hook 使用的粒度问题。这里简单补充一下:
若是仅仅将 class 中的 state 平移过来当作一整个状态,那分离状态,将状态复用的好处将彻底得不到体现。不相关的状态堆砌在一块儿,不只彻底没法复用,还会隐藏其中通用的状态。再者,若是每一个 state 都被单独拆分出来,在一次触发好几个状态变动时,咱们须要分别对其进行更新。代码变的难以理解,增长维护难度。
咱们应从 Hook 的动机入手,实现关注点分离,将关联的逻辑和状态放在一块儿。以可以拆分红自定义的 Hook 达到复用的目的来设计 Hook 的状态粒度。
其实在上面对各项 Hook 作介绍时,咱们已经提到了几种优化方式。在此处作一下总结。
跟 class 组件相似,性能优化的思路都是经过如下两个方面入手:
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
复制代码
React.memo
实际上和 Hook 关系不大,它是针对函数组件的一种性能优化方式。它于 React.PureComponent
很是类似,但只适用于函数组件。默认状况下 React.memo
只对 props 和 prevProps 作浅层比较,但咱们能够经过传入第二个参数来控制比较过程。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/* 若是把 nextProps 传入 render 方法的返回结果与 将 prevProps 传入 render 方法的返回结果一致则返回 true, 不然返回 false */
}
export default React.memo(MyComponent, areEqual);
复制代码
能够将 areEqual
理解为 React.Component
中由开发者本身控制的 shouldComponentUpdate
。
当使用 React.memo
或 shouldComponentUpdate
来决定是否进行从新渲染时,则强烈依赖外部传入的 props 的稳定性。因为函数组件每次渲染都会执行一次,其内部定义的回调函数每次都是新的。这些回调函数传递给子组件时,即使使用 React.memo
或 shouldComponentUpdate
也没法实现指望的效果。
const Child = React.memo(function (props) {
return (<button onClick={props.onClick}>点击</button>)
})
function Parent() {
// 经过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
const handleClick = useCallback(() => {
console.log('clicked');
}, /** deps = */ [])
return (
<>
<p>Parent</p>
<Child onClick={handleClick} />
</>
);
}
复制代码
经过使用 useCallback
,在依赖项没有发生变化时,React 会确保 handleClick
函数的标识是稳定的,而且不会在组件从新渲染时发生变化。这样 Child
接收到的 props 在 React.memo
对比中则没有变化,Child
不会触发从新渲染,达到性能优化的目的。
const Child = React.memo(function ({ button }) {
return (<button>{button.text}</button>)
})
function Parent() {
const button = React.useMemo(() => {
// 此处有高开销的计算过程
return {
text: '保存',
// ...
}
}, [])
return (
<>
<p>Parent</p>
<Child button={button} />
</>
);
}
复制代码
和 useCallback
类似,为了不从新渲染,咱们可使用 useMemo
记忆计算过的值。当依赖项没有发生变化时,高开销的计算过程将会被跳过,useMemo
将返回相同的值(若是是引用值,则是相同的引用标识)。这样 Child
接收到的 props 在 React.memo
对比中则没有变化,Child
不会触发从新渲染,达到性能优化的目的。
注意事项:使用 useMemo
在每次渲染时都会有函数从新定义的过程,计算量若是很小的计算函数,也能够选择不使用 useMemo
,由于这点优化并不会做为性能瓶颈的要点,反而可能使用错误还会引发一些性能问题。