最近在项目中基本上所有使用了React Hooks,历史项目也用React Hooks重写了一遍,相比于Class组件,React Hooks的优势能够一句话来归纳:就是简单,在React hooks中没有复杂的生命周期,没有类组件中复杂的this指向,没有相似于HOC,render props等复杂的组件复用模式等。本篇文章主要总结一下在React hooks工程实践中的经验。react
- React hooks中的渲染行为
- React hooks中的性能优化
- React hooks中的状态管理和通讯
原文首发至个人博客: https://github.com/forthealll...ios
理解React hooks的关键,就是要明白,hooks组件的每一次渲染都是独立,每一次的render都是一个独立的做用域,拥有本身的props和states、事件处理函数等。归纳来说:git
每一次的render都是一个互不相关的函数,拥有彻底独立的函数做用域,执行该渲染函数,返回相应的渲染结果es6
而类组件则不一样,类组件中的props和states在整个生命周期中都是指向最新的那次渲染.github
React hooks组件和类组件的在渲染行为中的区别,看起来很绕,咱们能够用图来区别,redux
上图表示在React hooks组件的渲染过程,从图中能够看出,react hooks组件的每一次渲染都是一个独立的函数,会生成渲染区专属的props和state. 接着来看类组件中的渲染行为:数组
类组件中在渲染开始的时候会在类组件的构造函数中生成一个props和state,全部的渲染过程都是在一个渲染函数中进行的而且,每一次的渲染中都不会去生成新的state和props,而是将值赋值给最开始被初始化的this.props和this.state。缓存
理解了React hooks的渲染行为,就指示了咱们如何在工程中使用。首先由于React hooks组件在每一次渲染的过程当中都会生成独立的所用域,所以,在组件内部的子函数和变量等在每次生命的时候都会从新生成,所以咱们应该减小在React hooks组件内部声明函数。性能优化
写法一:app
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.
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>
<p>You clicked 1 times</p>
也就是说每次渲染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中的渲染行为,也初步
提到了说将与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,那么如何解决组件间的通讯问题。
最基础的想法可能就是经过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的使用复杂度。
hooks组件间的通讯,一样可使用redux来实现。也就是说:
在React hooks中,redux也有其存在的意义
在hooks中存在一个问题,由于不存在相似于react-redux中connect这个高阶组件,来传递mapState和mapDispatch, 解决的方式是经过redux-react-hook或者react-redux的7.1 hooks版原本使用。
在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 的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不改变,就不会去从新计算.