最近在项目中基本上全部使用了React Hooks,历史项目也用React Hooks重写了一遍,相比于Class组件,React Hooks的优点可以一句话来概括:就是简单,在React hooks中没有复杂的生命周期,没有类组件中复杂的this指向,没有类似于HOC,render props等复杂的组件复用模式等。本篇文章主要总结一下在React hooks工程实践中的经验。
- React hooks中的渲染行为
- React hooks中的性能优化
- React hooks中的状态管理和通信
原文首发至我的博客: https://github.com/forthealll...
一、React hooks中的渲染行为
1.React hooks组件是如何渲染的
理解React hooks的关键,就是要明白,hooks组件的每一次渲染都是独立,每一次的render都是一个独立的作用域,拥有自己的props和states、事件处理函数等。概括来讲:
每一次的render都是一个互不相关的函数,拥有完全独立的函数作用域,执行该渲染函数,返回相应的渲染结果
而类组件则不同,类组件中的props和states在整个生命周期中都是指向最新的那次渲染.
React hooks组件和类组件的在渲染行为中的区别,看起来很绕,我们可以用图来区别,
上图表示在React hooks组件的渲染过程,从图中可以看出,react hooks组件的每一次渲染都是一个独立的函数,会生成渲染区专属的props和state. 接着来看类组件中的渲染行为:
类组件中在渲染开始的时候会在类组件的构造函数中生成一个props和state,所有的渲染过程都是在一个渲染函数中进行的并且,每一次的渲染中都不会去生成新的state和props,而是将值赋值给最开始被初始化的this.props和this.state。
2.工程中注意React hooks的渲染行为
理解了React hooks的渲染行为,就指示了我们如何在工程中使用。首先因为React hooks组件在每一次渲染的过程中都会生成独立的所用域,因此,在组件内部的子函数和变量等在每次生命的时候都会重新生成,因此我们应该减少在React hooks组件内部声明函数。
写法一:
function App() {
const [counter, setCounter] = useState(0);
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={() => setCounter(prevState => ++prevState)}>
Increment
</button>
</div>
);
}
写法二:
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
function App() {
const [counter, setCounter] = useState(0);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={()=>onClick(setCounter)}>
Increment
</button>
</div>
);
}
App组件是一个hooks组件,我们知道了React hooks的渲染行为,那么写法1在每次render的时候都会去重新声明函数formatCounter,因此是不可取的。我们推荐写法二,如果函数与组件内的state和props无相关性,那么可以声明在组件的外部。如果函数与组件内的state和props强相关性,那么我们下节会介绍useCallback和useMemo的方法。
React hooks中的state和props,在每次渲染的过程中都是重新生成和独立的,那么我们如果需要一个对象,从开始到一次次的render1 , render2, ...中都是不变的应该怎么做呢。(这里的不变是不会重新生成,是引用的地址不变的意思,其值可以改变)
我们可以使用useRef,创建一个“常量”,该常量在组件的渲染期内始终指向同一个引用地址。
通过useRef,可以实现很多功能,比如在某次渲染的时候,拿到前一次渲染中的state。
function App(){
const [count,setCount] = useState(0)
const prevCount = usePrevious(count);
return (
<div>
<h1>Now: {count}, before: {prevCount}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
上述的例子中,我们通过useRef()创建的ref对象,在整个usePrevious组件的周期内都是同一个对象,我们可以通过更新ref.current的值,来在App组件的渲染过程中,记录App组件渲染中前一次渲染的state.
这里其实还有一个不容易理解的地方,我们来看usePrevious:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
这里的疑问是:为什么当value改变的时候,返回的ref.current指向的是value改变之前的值?
也就是说:
为什么useEffect在return ref.current之后才执行?
为了解释这个问题,我们来聊聊神奇的useEffect.
3.神奇的useEffect
hooks组件的每一次渲染都可以看成一个个独立的函数 render1,render2 ... rendern,那么这些render函数之间是怎么关联的呢,还有上小节的问题,为什么在usePrevious中,useEffect在return ref.current之后才执行。带着这两个疑问我们来看看在hooks组件中,最为神奇的useEffect。
用一句话概括就是:
每一渲染都会生成不同的render函数,并且每一次渲染通过useEffect会生成一个不同的Effects,Effects在每次渲染后声效。
每次渲染除了生成不同的作用域外,如果该hooks组件中使用了useEffect,通过useEffect还会生成一个独有的effects,该effects在渲染完成后生效。
举例来说:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
上述的例子中,完成的逻辑是:
- 渲染初始的内容:
<p>You clicked 0 times</p>
- 渲染完成之后调用这个effect:{ document.title = 'You clicked 0 times' }。
- 点击Click me
- 渲染新的内容渲染的内容:
<p>You clicked 1 times</p>
- 渲染完成之后调用这个effect:() => { document.title = 'You clicked 1 times' }。
也就是说每次渲染render中,effect位于同步执行队列的最后面,在dom更新或者函数返回后在执行。
我们在来看usePrevious的例子:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
因为useEffect的机制,在新的渲染过程中,先返回ref.current再执行deps依赖更新ref.current,因此usePrevios总是返回上一次的值。
现在我们知道,在一次渲染render中,有自己独立的state,props,还有独立的函数作用域,函数定义,effects等,实际上,在每次render渲染中,几乎所有都是独立的。我们最后来看两个例子:
(1)
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
(2)
function Counter() {
const [count, setCount] = useState(0);
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
这两个例子中,我们在3内点击5次Click me按钮,那么输出的结果都是一样的。
You clicked 0 times
You clicked 1 times
You clicked 2 times
You clicked 3 times
You clicked 4 times
You clicked 5 times
总而言之,每一次渲染的render,几乎都是独立和独有的,除了useRef创建的对象外,其他对象和函数都没有相关性.
二、React hooks中的性能优化
前面我们讲了React hooks中的渲染行为,也初步
提到了说将与state和props无关的函数,声明在hooks组件外面可以提高组件的性能,减少每次在渲染中重新声明该无关函数. 除此之外,React hooks还提供了useMemo和useCallback来优化组件的性能.
(1).useCallback
有些时候我们必须要在hooks组件内定义函数或者方法,那么推荐用useCallback缓存这个方法,当useCallback的依赖项不发生变化的时候,该函数在每次渲染的过程中不需要重新声明
useCallback接受两个参数,第一个参数是要缓存的函数,第二个参数是一个数组,表示依赖项,当依赖项改变的时候会去重新声明一个新的函数,否则就返回这个被缓存的函数.
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
function App(props) {
const [counter, setCounter] = useState(0);
const onClick = useCallback(()=>{
setCounter(props.count)
},[props.count]);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={onClick}>
Increment
</button>
</div>
);
}
上述例子我们在第一章的例子基础上增加了onClick方法,并缓存了这个方法,只有props中的count改变的时候才需要重新生成这个方法。
(2).useMemo
useMemo与useCallback大同小异,区别就是useMemo缓存的不是函数,缓存的是对象(可以是jsx虚拟dom对象),同样的当依赖项不变的时候就返回这个被缓存的对象,否则就重新生成一个新的对象。
为了实现组件的性能优化,我们推荐:
在react hooks组件中声明的任何方法,或者任何对象都必须要包裹在useCallback或者useMemo中。
(3)useCallback,useMemo依赖项的比较方法
我们来看看useCallback,useMemo的依赖项,在更新前后是怎么比较的
import is from 'shared/objectIs';
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
if (nextDeps.length !== prevDeps.length) {
return false
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
其中is方法的定义为:
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
);
}
export default (typeof Object.is === 'function' ? Object.is : is);
这个is方法就是es6的Object.is的兼容性写法,也就是说在useCallback和useMemo中的依赖项前后是通过Object.is来比较是否相同的,因此是浅比较。
三、React hooks中的状态管理和通信
react hooks中的局部状态管理相比于类组件而言更加简介,那么如果我们组件采用react hooks,那么如何解决组件间的通信问题。
(1) UseContext
最基础的想法可能就是通过useContext来解决组件间的通信问题。
比如:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContext(null)
function CounterDisplay() {
let counter = useContext(Counter)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
function App() {
let counter = useCounter()
return (
<Counter.Provider value={counter}>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
在这个例子中通过createContext和useContext,可以在App的子组件CounterDisplay中使用context,从而实现一定意义上的组件通信。
此外,在useContext的基础上,为了其整体性,业界也有几个比较简单的封装:
https://github.com/jamiebuild...
https://github.com/diegohaz/c...
但是其本质都没有解决一个问题:
如果context太多,那么如何维护这些context
也就是说在大量组件通信的场景下,用context进行组件通信代码的可读性很差。这个类组件的场景一致,context不是一个新的东西,虽然用了useContext减少了context的使用复杂度。
(2) Redux结合hooks来实现组件间的通信
hooks组件间的通信,同样可以使用redux来实现。也就是说:
在React hooks中,redux也有其存在的意义
在hooks中存在一个问题,因为不存在类似于react-redux中connect这个高阶组件,来传递mapState和mapDispatch, 解决的方式是通过redux-react-hook或者react-redux的7.1 hooks版本来使用。
- redux-react-hook
在redux-react-hook中提供了StoreContext、useDispatch和useMappedState来操作redux中的store,比如定义mapState和mapDispatch的方式为:
import {StoreContext} from 'redux-react-hook';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root'),
);
import {useDispatch, useMappedState} from 'redux-react-hook';
export function DeleteButton({index}) {
// Declare your memoized mapState function
const mapState = useCallback(
state => ({
canDelete: state.todos[index].canDelete,
name: state.todos[index].name,
}),
[index],
);
// Get data from and subscribe to the store
const {canDelete, name} = useMappedState(mapState);
// Create actions
const dispatch = useDispatch();
const deleteTodo = useCallback(
() =>
dispatch({
type: 'delete todo',
index,
}),
[index],
);
return (
<button disabled={!canDelete} onClick={deleteTodo}>
Delete {name}
</button>
);
}
- react-redux 7.1的hooks版
这也是官方较为推荐的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()这3个主要方法,分别对应与mapState、mapDispatch以及直接拿到redux中store的实例.
简单介绍一下useSelector,在useSelector中除了能从store中拿到state以外,还支持深度比较的功能,如果相应的state前后没有改变,就不会去重新的计算.
举例来说,最基础的用法:
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}
实现缓存功能的用法:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfDoneTodos = createSelector(
state => state.todos,
todos => todos.filter(todo => todo.isDone).length
)
export const DoneTodosCounter = () => {
const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
return <div>{NumOfDoneTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<DoneTodosCounter />
</>
)
}
在上述的缓存用法中,只要todos.filter(todo => todo.isDone).length不改变,就不会去重新计算.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。