react在16.8版本就用了hook,hook是在以前的无状态纯函数组件增加了内部自己的状态,而且我们完全可以用hookopen in new window方式替换原有的class组件,本文是一篇重读react官网hook的笔记。

在开始本文之间,主要会从以下方面理解hook

  • 为什么会有hook,hook到底有什么好

  • hook组件比class组件更简单,更容易维护

  • 核心hookAPI的使用

hook到底有多好

在官网总结里,使用过往class组件其实主要有以下几个弊端

  • class组件之间状态难以复用

  • class内部复杂的钩子componentDidMount,componentDidUpdate,componentWillUnmount耦合了太多的逻辑,在class组件耦合了这样类似的生命周期钩子有太多的逻辑状态,使得组件的复用有很大挑战。

  • class组件不易压缩,而且在热重载上有效果不好

针对以上hook做了一些渐进增强,向后兼容,hook不是唯一选择,不影响原有class组件的生命周期

  • 让组件状态可以更好的复用

  • 不再有那么的生命周期钩子函数,而是更简单的hook,组件细粒度可以更小的纯函数。

hook是什么

hook本质就是纯函数,通常我们使用hook就是useXXX,我们看下hook的第一个基础hookuseState

useState

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

useState的初始值就是传入的形参initState,并且返回是一个数组[state,dispatch]

我们看一下官网简单的例子

import React, { useState } from "react";
function Title() {
    const [count, setCount] = useState(0);
    const add = () => {
        console.log(count, 'before count')
        setCount(count + 1)
    }
    return (<>
        <p>{count}</p>
        <button onClick={add}>计数</button>
    </>)
}

export default Title

useState初始值是0,然后我们用数组解构方式,第一个值是state,第二值是setState,注意我们setCount是异步,这与class组件的setState是一样的

useEffect

为函数组件提供了副作用能力,怎么理解副作用,就是我们平时获取数据,修改dom之类的操作,一个函数内部产生不确定性操作这就是副作用。

官方解释useEffect是原有componentDidMount,componentDidUpdate,componentWillUnmount的组合

具体我们看下

function Title() {
    const [count, setCount] = useState(0);
    const add = () => {
        console.log(count, 'before count1')
        setCount(count + 1);
        console.log(count, 'before count2')
    }
    useEffect(() => {
        console.log(count, 'after count title')
        document.title = count;
    })
     const renderText = () => {
        console.log(count, 'render')
        return count
    }
    return (<>
        <p>{renderText()}</p>
        <button onClick={add}>添加</button>
    </>)
}

useEffect这个hook是在更新dom后,执行的,也就react会在dom更新后再执行useEffect中的内部的副作用函数,每次更新state,就会重新渲染组件,useEffect会重新调用。

useEffect内部就是能获取到修改后最新的值。

为啥useEffect能拿到最新值,这是因为这其实用了一个js中的闭包,在useEffect内部有访问函数外部的变量,在Title这个函数组件内部作用域内,useEffect内部是可以访问函数组件的变量的。

消除useEffect副作用

我们知道useEffect有副作用,那么如何消除呢,官网介绍到,只需要返回一个函数即可,在官网提到,如果是外部数据源,是有必要消除副作用的,这样可以防止内存泄漏。

  ...
useEffect(() => {
      console.log(count, 'after count title')
      document.title = count;
      return () => {
          console.log('清除副作用')
      }
  })

useEffect(callback, []),如果是这样,就只会调用一次,如果会传入一个值useEffect(callback, [count]),那么count变化了,那么useEffect就会重新调用

我们尝试在useEffect使用一个定时器计数,如下

useEffect(() => {
        console.log(count, 'after count title')
        document.title = count;
        const timer = setInterval(() => {
            setCount(count => count + 1);
        }, 1000)
        return () => {
            console.log('清除副作用')
        }
    })

你会发现定时器计数设值是不准确,这个问题怎么解呢,实际上只需要在useEffect中清除即可

 useEffect(() => {
    console.log(count, 'after count title')
    document.title = count;
    prevCount.current = count;
    const timer = setInterval(() => {
        setCount(count => count + 1);
    }, 1000)
    return () => {
        clearInterval(timer)
        console.log('清除副作用')
    }
})

在hook上使用有规定

  • 不能使用在条件,循环,子函数使用

  • 只存在于函数组件自定义hook中调用

  • hook可以多次重复使用

通俗来讲,就是只能在函数组件最顶层使用

自定义hook

render props高阶组件一样共享组件状态逻辑

用一段为伪代码来感受下

// useFeatchList
import {useState, useEffect } from 'react'
import fetchListProxy from '@/service/proxy'
export function useFeatchList(id) {
    const [data, setData] = useState([]);
    useEffect(async () => {
        // 请求获取数据
        const {list} = await fetchListProxy(id);
        setData(list)
    }, []);
    return {
      data
    }
}

在另一个页面

  import react from 'react';
  import { useFeatchList} from './useFeatchList'
  function List (props) {
    const {data} = useFeatchList(props.id);
    return (<>
      <ul>
        {
          data.map(v => <li>{v.text}</li>)
        }
      </ul>
    </>)
  }
  function App () {
    return <List id="123" />
  }
  export default App

useFeatchList就是一个自定义hook,我们把请求获取数据成了一个hook,与实际组件解藕了出去,这样业务逻辑更加清晰了

我们再来看下官方的一个自定义自己的redux,reduxreact状态管理工具类似vuex一样,但是个人觉得vuex相比较redux理解起来也容易得多。

我们今天并不打算去了使用redux这个库,设计使用还是有些复杂。但是我们可以实现一个类似redux的功能,我们封装一个自定义hook,useReducer

useReducer

import React, { useState, useEffect, createRef } from "react";
import { useReducer } from '../../hook/useReducer';

function addReducer(state, action) {
    switch (action.type) {
        case 'add':
            return [...state, {
                ...action.payload
            }];
        default:
            return state;
    }
}
 function Title() {
    const [data, dispatch] = useReducer(addReducer, []);
    const [val, setValue] = useState('Web技术学苑');
    const inputRef = createRef();
    const dispatchAdd = () => {
        dispatch({
            type: 'add',
            payload: {
                name: inputRef.current.value
            }
        })
    }
    const renderData = () => {
        return data.map((v, index) => <p key={index}>{v.name}-{index}</p>)
    }
    const handleChangeVal = (e) => {
        setValue(e.target.value)
    }
    return (<>
        <input placeholder="请输入内容..." ref={inputRef} value={val} onChange={handleChangeVal} />
        <button onClick={dispatchAdd}>dispatchAdd</button>

        {renderData()}
    </>)
 }

我们在hook/useReducer中定义了一个useReducer,useReducer(callback, initState)的第一个参数就是一个纯函数,useReducer返回的就是一个数组[state, disptch],实际上这个dispatch做了啥呢,其实就是调用了callback返回的结果,重新设值了。

// hook/useReducer.js
import { useState } from 'react'
// 自定义一个reducer
export function useReducer(reducer, initState) {
    const [state, setState] = useState(initState);
    function dispatch(action) {
        const nextState = reducer(state, action);
        setState(nextState);
    }
    return [state, dispatch]
}

所以页面上就是ok了

所以一个自定义useReucer就已经可以了,其实关于自定义hook返回的并不一定是一个[state, dispatch],这点因你你自己业务而定,但是记住一点hook就是一个纯函数

官方react已经实现了useReducer,所以如果引用官方,那就很简单了,就只需要注释引入自定义引入hook即可

import React, { useState, useEffect, createRef, useReducer } from "react";
// import { useReducer } from '../../hook/useReducer';
function addReducer(state, action) {
    switch (action.type) {
        case 'add':
            return [...state, {
                ...action.payload
            }];
        default:
            return state;
    }
}
 function Title() {
    const [data, dispatch] = useReducer(addReducer, []);
    const [val, setValue] = useState('Web技术学苑');
    const inputRef = createRef();
    const dispatchAdd = () => {
        dispatch({
            type: 'add',
            payload: {
                name: inputRef.current.value
            }
        })
    }
    const renderData = () => {
        return data.map((v, index) => <p key={index}>{v.name}-{index}</p>)
    }
    const handleChangeVal = (e) => {
        setValue(e.target.value)
    }
    return (<>
        <input placeholder="请输入内容..." ref={inputRef} value={val} onChange={handleChangeVal} />
        <button onClick={dispatchAdd}>dispatchAdd</button>

        {renderData()}
    </>)
 }

useCallback

缓存内部依赖的回调函数,避免不必要渲染

import React, { useState, useEffect, createRef, useReducer, useCallback } from "react";
function Title() {
    const [count, setCount] = useState(0);
    const [data, dispatch] = useReducer(addReducer, []);
    const [val, setValue] = useState('Web技术学苑');
    const inputRef = createRef();
    const add = () => {
        console.log(count, 'before count1')
        setCount(count + 1);
        console.log(count, 'before count2')
    }
   ...
    const handleChangeVal = useCallback((e) => {
        setValue(e.target.value)
    }, [])
    return (<>
        <p>{renderText()}</p>
        <button onClick={add}>添加</button>
        <input placeholder="请输入内容..." ref={inputRef} value={val} onChange={handleChangeVal} />
        <button onClick={dispatchAdd}>dispatchAdd</button>

        {renderData()}
    </>)
}

useRef

这个hook与createRef的作用类似,可以拿到具体dom,以上createRef可以换成useRef

useMemo

在渲染期间完成的,作为优化组件渲染的一种手段,useMemo返回的是一个组件,可以依赖于某个值,而做不必要的渲染

...
function Title() {
    const [count, setCount] = useState(0);
    const prevCount = useRef(0);
    const [data, dispatch] = useReducer(addReducer, []);
    const [val, setValue] = useState('Web技术学苑');
    const inputRef = useRef();
    const add = () => {
        console.log(count, 'before count1')
        setCount(count + 1);
        console.log(count, 'before count2')
    }
    const dispatchAdd = () => {
        dispatch({
            type: 'add',
            payload: {
                name: inputRef.current.value
            }
        })
    }

    const renderText = () => {
        console.log(count, 'render')
        return count
    }
    const renderData = () => {
        console.log(data)
        return data.map((v, index) => <p key={index}>{v.name}-{index}</p>)
    }
    // const handleChangeVal = (e) => {
    //     setValue(e.target.value)
    // }
    const handleChangeVal = useCallback((e) => {
        setValue(e.target.value)
    }, [])

  const input = useMemo(() => <input placeholder="请输入内容..." ref={inputRef} value={val} onChange={handleChangeVal} />, [val])
    
    return (<>
        <p>prev:{prevCount.current}</p>
        <p>now: {renderText()}</p>
        <button onClick={add}>添加</button>
        {input}
        <button onClick={dispatchAdd}>dispatchAdd</button>

        {renderData()}
    </>)
}

memo

缓存组件,相当于class组件的PureComponent,会以props做浅比较优化

import React, { useState, useEffect, useRef, useReducer, useCallback, memo, useMemo } from "react";
// import { useReducer } from '../../hook/useReducer'
...
function Title() {
    const [data, dispatch] = useReducer(addReducer, []);
    ...
    const Pom = (props) => props.data.map((v, index) => <p key={index}>{v.name}-{index}</p>)
    const List = memo(() => <Pom data={data}></Pom>)
    const handleChangeVal = useCallback((e) => {
        setValue(e.target.value)
    }, [])

    const input = useMemo(() => <input placeholder="请输入内容..." ref={inputRef} value={val} onChange={handleChangeVal} />, [val])
    // console.log(input, 'input')
    return (<>
        {/* <input placeholder="请输入内容..." ref={inputRef} value={val} onChange={handleChangeVal} /> */}
        {input}
        <button onClick={dispatchAdd}>dispatchAdd</button>
        <List></List>
    </>)
}

export default Title

在官网提供了一篇hook的FAQopen in new window非常值得一看

总结

  • hook替换写以前的class组件方式,hook让纯函数组件有了自己的状态

  • useEffect就是以前class组件componentDidMount,componentDidupdate,componentWillUnmount的集合,每次更新state,dom渲染后会调用useEffect,内部获取的state是最新更新后的值

  • 常用hookApi的使用,useState,useEffect,useReducer,useCallbackuseMemo

  • 消除useEffect副作用,在useEffect中返回一个函数即可

  • memo是纯函数组件的优化类似class组件的PureComponent

  • 本文示例code exampleopen in new window

扫二维码,关注公众号
专注前端技术,分享web技术
加作者微信
扫二维码 备注 【加群】
教你聊天恋爱,助力脱单
微信扫小程序二维码,助你寻他/她
专注前端技术,分享Web技术
微信扫小程序二维码,一起学习,一起进步
前端面试大全
海量前端面试经典题,助力前端面试