- Published on
ReactHook 手记
- Authors
- Name
- Hansuku
React Hook
React Hook 是 2019 年 React 生态圈里最火的一个新特性,改变了我们传统使用 React 类来开发的方式,改用函数式写法,改变复杂的状态操作、改变状态组件的复用性。 首先通过一个简单的 demo 来看看 React Hook 长什么样 我们写一个简单的计次器,传统的代码张这样
import React, { Component } from 'react';
class Example extends Component {
constructor(props) {
super(props);
this.state = { count:0 }
}
render() {
return (
<div>
<p>You clicked { this.state.count } times</p>
<button onClick={this.addCount.bind(this)}>Click me</button>
</div>
)
}
addCount() {
this.setState({ count: this.state.count + 1 })
}
}
使用 React Hook 写 他张这样
import React, { useState } from 'react';
function Example(){
const [ count,setCount ] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() =>{ setCount(count +1 ) }}>click me</button>
</div>
)
}
export default Example;
useState
通过上面的代码我们可以知道,以前我们定义在this.state
的方法被取代变成了 React 内置的一个方法useState
const [ count, setCount ] = useState(0)
我们先看右边,useState
中我们传递了一个 0,代表着一个默认值,左边使用数组则是借助 ES6 的数组解构赋值,实际上他简化了我们这样的代码操作
// const [ count,setCount ] = useState(0)
let _userState = useState(0)
let count = _userState[0]
let setCount = _userState[1]
我们看到这个代码的时候不能理解为什么react
可以把count
绑定到vdom
上,以及并不知道setCount
是如何成为一个可执行的方法
实际上useState
是依靠传入的顺序来做区分,我们来做一个简单的小测试
const [ count, setCount] = useState(0)
console.log(count) //0
console.log(setCount) // dispatchAction function
const [ setCount, count ] = useState(0)
console.log(setCount) //0
console.log(count) // dispatchAction function
通过上面的代码可以了解到,useState 默认把数组内第一个值当做是属性,第二个当做是修改状态的方法。
对没错,是修改状态,如果学习过Vuex
或者Redux
的同学能看出来,他更像是以前我们在数据流管理中的状态的概念,包括我们console.log(setCount)
也能看到他打印出来得是一个dispatchAction
的方法。React Hook 通过这种能力补足纯函数组件没有实例、没有状态的情况,当然后面也会有useEffect
、useContext
、useReducer
、useMemo
。
useEffect
有小伙伴要问了,如果写成函数式的,那么以前我们的生命周期函数在那些呢?useEffect
就提供了这样一套方法来实现生命周期函数。
传统的一个生命周期示例
import React, { Component } from 'react';
class Example extends Component {
constructor(props) {
super(props);
this.state = { count: 0 }
}
componentDidMount() {
console.log(`componentDidMount => You clicked ${this.state.count}`)
}
componentDidUpdate() {
console.log(`componentDidUpdate => You clicked ${this.state.count}`)
}
render() {
return (
<div>
<p>You clicked { this.state.count } times</p>
<button onClick={ this.addCount.bind(this) }>Click me</button>
</div>
)
}
addCount() {
this.setState({count: this.state.count + 1})
}
}
export default Example;
如果改写成useEffect
是这样的
import React, { useState, useEffect } from 'react';
function Example(){
const [ count,setCount ] = useState(0)
useEffect(() =>{
console.log(`useEffect => You clicked ${count}`)
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() =>{ setCount(count +1 ) }}>click me</button>
</div>
)
}
export default Example;
借助官方的示例,在useEffect
中我们可以有一个副作用,比如
import React, { useState, useEffect } from 'react';
function Example(){
const [ count,setCount ] = useState(0)
useEffect(() =>{
console.log(`useEffect => You clicked ${count}`)
document.title = `You clicked ${count} times`
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() =>{ setCount(count +1 ) }}>click me</button>
</div>
)
}
export default Example;
借助上面useEffect
里的方法,我们在每次状态被更新的时候都可以改变浏览器的标题,这就是一个副作用。
我们可以把useEffect
Hook 视作componentDidMount
、componentDidUpdate
和 componentWillUnmount
的组合体。这里我们看一下组件卸载时如何用 useEffect 实现,以及一起讲讲副作用这个东西。
我们装上路由,先来写一段
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
function Index(){
useEffect(() =>{
console.log('useEffect=> 首页开始挂载了兄弟')
return () => {
console.log('useEffect=> 首页卸载GG了兄弟')
}
},[])
return <h2>Index page</h2>
}
function List(){
useEffect(() =>{
console.log('useEffect=> 列表开始挂载了兄弟')
return () => {
console.log('useEffect=> 列表卸载GG了兄弟')
}
},[])
return <h2>List Page</h2>
}
function Example(){
const [ count,setCount ] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() =>{ setCount(count +1 ) }}>click me</button>
<Router>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/list/">列表</Link></li>
</ul>
<Route path="/" exact component={Index}></Route>
<Route path="/List" exact component={List}></Route>
</Router>
</div>
)
}
export default Example;
useEffect
中提供了return
一个匿名函数来作为组件被卸载的生命周期,上面的例子中我们每次从首页切换到列表都会先打印一遍useEffect=> 首页卸载GG了兄弟
然后再打印useEffect=> 列表开始挂载了兄弟
。
但是问题也来了,我们发现哪怕页面打印说是卸载了,但是我们点击 click me 按钮的时候,发现当前页面会走一次卸载的console
,这就是我们的卸载副作用。
针对上面的问题,我们就需要用到useEffect
函数的第二个值,我们只需要给useEffect
再传入一个空数组即可
function Index(){
useEffect(() =>{
console.log('useEffect=> 首页开始挂载了兄弟')
return () => {
console.log('useEffect=> 首页卸载GG了兄弟')
}
},[])
return <h2>Index page</h2>
}
function List(){
useEffect(() =>{
console.log('useEffect=> 列表开始挂载了兄弟')
return () => {
console.log('useEffect=> 列表卸载GG了兄弟')
}
},[])
return <h2>List Page</h2>
}
这样就可以保证 return 后的匿名函数只在真正卸载时执行。 那么第二个参数只能传递空数组吗?并不,我们回到之前的例子
import React, { useState, useEffect } from 'react';
function Example(){
const [ count,setCount ] = useState(0)
useEffect(() =>{
console.log(`useEffect => You clicked ${count}`)
document.title = `You clicked ${count} times`
return () => {
console.log('Example被卸载啦')
}
},[])
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() =>{ setCount(count +1 ) }}>click me</button>
</div>
)
}
export default Example;
此时我们点击 click me 按钮,发现 chrome 控制台会给到一个警告
Line 11: React Hook useEffect has a missing dependency: 'count'.Either include it or remove the dependency array react-hooks/exhaustive-deps
这里告诉我们,useEffect
缺少依赖项count
,要么我们在 deps(即我们在useEffect
传入的第二个参数)包含count
,要么删除 deps。
那么我们加上看看
useEffect(() =>{
console.log(`useEffect => You clicked ${count}`)
document.title = `You clicked ${count} times`
return () => {
console.log('Example被卸载啦')
}
},[count])
这时我们看到,每次 count 更新的时候,都会打印Example被卸载啦
。
useContext
useState
和useEffect
可以解决我们大部分的业务场景了,而useContext
主要是用来解决父子组件传值的问题。
import React, { useState, createContext, useContext } from 'react';
const CountContext = createContext();
function Counter() {
let count = useContext(CountContext)
return (<h2>{count}</h2>)
}
function Example(){
const [ count,setCount ] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() =>{ setCount(count + 1 ) }}>click me</button>
<CountContext.Provider value={count}>
<Counter />
</CountContext.Provider>
</div>
)
}
export default Example;
上面我们看到,我们创建了父组件Example
和子组件Counter
,需要传递的数据是count
。我们把子组件包裹在了一个provider
中,这个provider
由react
内的createContext
创建上下文,再通过provider
组件传递,在子组件Counter
中通过useContext
获取上下文从而拿到值。
useReducer
useReducer
的思想和redux
中的reducer
基本一致,我们先来看看一个简单的reducer
是怎么实现的
function countReducer (state, action) {
switch(action.type) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}
相信上面的代码大家都能看得懂,他的核心思维就是传递值和控制值。
那么如果使用useReducer
来实现上面的代码是怎么样的呢?
import React, { useReducer } from 'react';
function ReducerDemo(){
const [count, dispatch] = useReducer((state, action) => {
switch(action) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}, 0)
return (
<div>
<h2>now times:{count}</h2>
<button onClick={() => {dispatch('add')}}>add</button>
<button onClick={() => {dispatch('sub')}}>sub</button>
</div>
)
}
export default ReducerDemo
这里主要是两部分,一是useReducer
传入两个参数,跟 useState 类似,第一个是属性名,第二个是派发器,对没错就跟 redux 中dispatch
是一个意思。useReucer 内部传入两个参数,第一个是每次接受到dispatch
指令的方法,方法有state
和action
两个值,state
与count
对应,action
则是传入的不同方法,方便下面switch...case
的时候做不同的处理。useReducer
第二个参数我们传了个 0,这里代表着count
的默认值。
useContext 和 useReducer 的区别
区别在哪呢?useContext
专注于处理组件传值,而useReducer
更偏向redux
,属于共享状态。两者虽效果不同但是可以互相配合来完成以前在redux
中完成的工作。
接下来我们将通过一个小 demo 来体验一下如何用userReducer
和userContext
替代redux
,以下会分为几个文件,全部都在一个文件夹下
// showArea.js
// 显示区域,通过button.js改变reducer里的颜色值来改变页面上显示的颜色
import React, { useContext } from 'react';
// 导入颜色控制器
import { ColorContext } from './color';
function ShowArea(){
// 取出颜色
const { color } = useContext(ColorContext)
return (<div style={{color: color}}>字体颜色为{color}</div>)
}
export default ShowArea
// button.js
// 按钮组件,通过这里的事件派发到reducer然后改变颜色在showArea中显示
import React, {useContext} from 'react';
// 导入颜色控制器和改变方法的标识
import { ColorContext, UPDATE_COLOR } from './color';
// 定义两个按钮组件
function Buttons(){
const { dispatch } = useContext(ColorContext)
return(
<div>
<button onClick={() => {dispatch({type: UPDATE_COLOR, color: 'red'})}}>红色</button>
<button onClick={() => {dispatch({type: UPDATE_COLOR, color: 'yellow'})}}>黄色</button>
</div>
)
}
export default Buttons
// color.js
// 颜色控制器,负责接收改变颜色的事件和向外输出颜色
import React, { createContext, useReducer } from 'react';
// 创建颜色上下文
export const ColorContext = createContext({})
// 定义改变颜色的方法标识
export const UPDATE_COLOR = "UPDATE_COLOR"
// 定义reducer的方法
const reducer = (state, action) => {
switch (action.type) {
case UPDATE_COLOR:
return action.color
default:
return state
}
}
// 颜色组件
export const Color = props => {
const [color, dispatch] = useReducer(reducer, 'blue')
return (
<ColorContext.Provider value={{ color, dispatch }}>
{props.children}
</ColorContext.Provider>
)
}
// index.js
// 串联上面所有的组件
import React from 'react';
import ShowArea from './showArea';
import Buttons from './Button';
import { Color } from './color';
function Example(){
return (
<div>
<Color>
<ShowArea></ShowArea>
<Buttons></Buttons>
</Color>
</div>
)
}
export default Example
整体来说还是很好理解的,聪明的小伙伴应该看到这里我们多了一个事件标识,并且是大写的常量,是不是有一些似曾相识呢?对这就是我们redux
中的大写常量标识。
useMemo
useMemo
的存在主要是为了解决 React Hook 的性能问题,那么 React Hook 有啥性能问题呢?在以往的 React 中我们有一个showComponentUpdate
生命周期,在组件更新之前会被调用,但在 React Hook 中的useEffect
是没有这个生命周期的,这样会导致一个问题,假设我们有较多的父子组件,如果父组件更新了,子组件内所有的方法都会再执行一次,造成严重的性能浪费,甚至会导致程序崩溃。
我们来看一个略微有点复杂的 demo
import React, {useState} from 'react';
export default function Example() {
const [boduo, setBoduo] = useState('波多野结衣在线发牌,')
const [cangjing, setCangjing] = useState('苍井空在线发牌,')
return (
<>
<button onClick={() => {setBoduo(new Date().getTime())}}>波多野结衣</button>
<button onClick={() => {setCangjing(new Date().getTime()+'苍井空向我们走来')}}>苍井空</button>
<ChildComponent name={boduo}></ChildComponent>
</>
)
}
function ChildComponent({name}) {
function changeBoduo() {
console.log('波多野结衣来啦!!!')
return name + '波多野结衣向我们走来'
}
const actionBoduo = changeBoduo(name)
return (
<>
<div>{actionBoduo}</div>
</>
)
}
上面我们定义了一个父组件两个状态,两个 button,和一个子组件。
子组件中,我们只传入了boduo
这个状态,子组件里也只引用了这个状态,那么在我们传统的概念中,changeBoduo
方法应该只执行一次,但是现实是,无论我们点击波多野结衣还是苍井空的按钮,都会导致changeBoduo
被执行。
那么我们用上useMemo
,并且也把cangjing
也传入子组件试试
import React, {useState, useMemo} from 'react';
export default function Example() {
const [boduo, setBoduo] = useState('波多野结衣在线发牌,')
const [cangjing, setCangjing] = useState('苍井空在线发牌,')
return (
<>
<button onClick={() => {setBoduo(new Date().getTime())}}>波多野结衣</button>
<button onClick={() => {setCangjing(new Date().getTime()+'苍井空向我们走来')}}>苍井空</button>
<ChildComponent boduo={boduo} cangjing={cangjing}></ChildComponent>
</>
)
}
function ChildComponent({boduo, cangjing}) {
function changeBoduo() {
console.log('波多野结衣来啦!!!')
return boduo + '波多野结衣向我们走来'
}
const actionBoduo = useMemo(() => changeBoduo(boduo),[boduo])
return (
<>
<div>{actionBoduo}</div>
<div>{cangjing}</div>
</>
)
}
现在我们发现,只有点击波多野结衣的时候,changeBoduo
会被执行,而点击苍井空的时候则不会。
useMemo
的用法实际上与useState
类似,第一个传入你要的方法,第二个传入他在什么状态下改变才会触发方法的值,比如我们绑定了boduo
,那么只有当boduo
改变才会触发changeBoduo
,这样就起到了showComponentUpdate
的效果。
再来看另外一个栗子
import React, { useState } from 'react';
export default function WithoutMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
function expensive() {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return <>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</>;
}
我们把上面的代码丢进去跑, 点击 +c1 的按钮,{count}
和{expensive()}
会改变,这是符合逻辑的,因为expensive()
使用了 count
变量。然后我们在 input 框输入,val
发生改变,这个时候我们打开控制台,发现expensive()
中的 console 被打印了出来,明明我们expensive()
方法中没有使用 val
变量,理应expensive()
不执行的,这样就导致了性能浪费。
为了保护性能我们使用useMemo
来包裹expensive()
方法
import React, { useState, useMemo } from 'react';
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return <>
<h4>{count}-{val}-{expensive}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</>;
}
这样我们在 input 框输入的时候,就发现不会再去触发 console
了。
useRef
useRef
与原生ref
思想和用法基本一致,我们通过一个简单的 demo 来看看
import React, { useRef } from 'react'
function Example() {
const inputEl = useRef(null)
const onButtonClick = () => {
inputEl.current.value = "Hello"
console.log(inputEl)
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>在 input 上展示文字</button>
</>
)
}
export default Example
useCallback
useCallback
和useMemo
类似,但它返回的是缓存的函数。
import React, { useState, useCallback } from 'react';
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return <>
<h4>{count}</h4>
<h4>{set.size}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</>;
}
刚看上面的代码会有点蒙蔽,首先要理解 Set
是什么东西,不懂的同学可以看一下阮一峰老师的 ES6 Set,简单来说他就是一个不会重复的数组。
我们点击+的按钮,会让 count
变更,每次 count
变更,都会往 set
里新增一个数组元素(即 useCallback
的方法),这个时候通过set.size
类似于数组的 arr.length
来获取 set
的长度,就会跟count
同步数字,而当val
变更时,他也跟useMemo
一样不会去响应这个方法。
这样做有什么意义呢?有一个场景是,一个父组件,其中包含子组件,子组件接收一个函数作为 props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助 useCallback 来返回函数,然后把这个函数作为 props 传递给子组件;这样,子组件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react';
/**
* 父组件
*
* @export
* @returns
*/
export default function Parent() {
// 初始化 count
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
/*
* 定义 useCallback
* 返回父组件的 count 给子组件
*/
const callback = useCallback(() => {
return count;
}, [count]);
return <>
<h4>{count}</h4>
<Child callback={callback} />
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)} />
</div>
</>;
}
/**
* 子组件
*
* @param {*} { callback } 上层传递的 props 参数
* @returns
*/
function Child({ callback }) {
// 子组件并不是直接拿着父组件的值就过来用,而是通过从 callback 初始化值
const [count, setCount] = useState(() => callback());
// setCount 改值的方法 也会通过 callback()获取 当父组件每次面临更新的时候
// 子组件收到通知 useEffect 会在这里自行检查 callback 是否变更 以确定是否更新 count
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
例子逻辑比较简单,但主要还是要消化useCallback
的使用场景。所有依赖本地状态或props
来创建函数,需要使用到缓存函数的地方,都是useCallback
的应用场景。
自定义 Hook
其实自定义 Hook 某种程度上就是我们平时写的函数,偏向于功能性的函数
import React, { useState, useEffect, useCallback } from 'react';
function useWinSize() {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
const onResize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
},[])
useEffect(() => {
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
})
return size
}
export default function Example() {
const size = useWinSize()
return (
<div>页面 Size: {size.width}x{size.height}</div>
)
}
上面的代码我们自定义了一个 hook useWinSize
,用来存储和侦听页面窗口大小,代码还是很好理解的。