Published on

ReactHook 手记

Authors
  • avatar
    Name
    Hansuku
    Twitter

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 通过这种能力补足纯函数组件没有实例、没有状态的情况,当然后面也会有useEffectuseContextuseReduceruseMemo

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里的方法,我们在每次状态被更新的时候都可以改变浏览器的标题,这就是一个副作用。 我们可以把useEffectHook 视作componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合体。这里我们看一下组件卸载时如何用 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

useStateuseEffect可以解决我们大部分的业务场景了,而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中,这个providerreact内的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指令的方法,方法有stateaction两个值,statecount对应,action则是传入的不同方法,方便下面switch...case的时候做不同的处理。useReducer第二个参数我们传了个 0,这里代表着count的默认值。

useContext 和 useReducer 的区别

区别在哪呢?useContext专注于处理组件传值,而useReducer更偏向redux,属于共享状态。两者虽效果不同但是可以互相配合来完成以前在redux中完成的工作。 接下来我们将通过一个小 demo 来体验一下如何用userReduceruserContext替代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

useCallbackuseMemo类似,但它返回的是缓存的函数。

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,用来存储和侦听页面窗口大小,代码还是很好理解的。

*/}