函数组件的特点:

  • 没有组件实例
  • 没有生命周期
  • 没有state和setState,只能接收props

class组件的问题:

  • 大型组件很难拆分和重构,不易于测试
  • 相同业务逻辑分散到各个方法中,逻辑混乱
  • 复用逻辑复杂,如Mixins、HOC、Render Prop

React提倡函数式编程,函数更灵活,更易拆分,更易测试,但函数组件太简单没有实例没有生命周期没有state,需要增强能力 - 所以引入了Hooks。

# 用useState实现state和setState功能

useState Hook就是让函数组件实现state和setState功能

默认函数组件没有state

函数组件是一个纯函数,执行完即销毁,无法存储state,需要state hook,把state功能“钩”到纯函数中

useState使用总结:

  1. useState(0)传入初始值,返回数组[state, setState]
  2. 通过state获取值
  3. 通过setState修改值

hooks命名规范

  1. 所有的Hooks都使用use开头,如useXXX
  2. 自定义Hook也要以use开头
  3. 非Hooks的地方,尽量不要用use开头

useState使用示例:

import React, { useState } from 'react'

function ClickCounter() {
    // 数组的解构
    // useState 就是一个 Hook “钩”,最基本的一个 Hook
    const [count, setCount] = useState(0) // 传入一个初始值

    const [name, setName] = useState('f老师')

    // const arr = useState(0)
    // const count = arr[0]
    // const setCount = arr[1]

    function clickHandler() {
        setCount(count + 1)
        setName(name + '2020')
    }

    return <div>
        <p>你点击了 {count}{name}</p>
        <button onClick={clickHandler}>点击</button>
    </div>
}

export default ClickCounter

# 用useEffect模拟组件生命周期

函数组件默认没有生命周期

函数组件是一个纯函数,执行完即销毁,自己无法实现生命周期

使用effect hook把生命周期“钩”到纯函数中

// 这样使用可以模拟class组件的 DidMount 和 DidUpdate两个生命周期,每次有state更新都会重新执行
useEffect(() => {
    console.log('在此发送一个 ajax 请求')
})
// 只模拟 class 组件的 DidMount
useEffect(() => {
    console.log('加载完了')
}, []) // 第二个参数是 [] (不依赖于任何 state)
// 只模拟 class 组件的 DidUpdate
useEffect(() => {
    console.log('更新了')
}, [count, name]) // 第二个参数就是依赖的 state,当state发生变化时才会执行
// 模拟 class 组件的 DidMount
useEffect(() => {
    let timerId = window.setInterval(() => {
        console.log(Date.now())
    }, 1000)

    // 返回一个函数
    // 模拟 WillUnMount
    return () => {
        window.clearInterval(timerId)
    }
}, [])

useEffect的使用总结:

  1. 模拟componentDidMount - useEffect依赖 []
  2. 模拟componentDidUpdate - useEffect无依赖,或者依赖[a, b] (a, b代表状态)
  3. 模拟compoentWillUnMount - useEffect中返回一个函数

useEffect让纯函数有了副作用

默认情况下,执行纯函数输入参数返回结果无副作用,所谓副作用,就是对函数之外造成影响,如设置全局定时任务。,而组件需要副作用,所以需要useEffect“钩”到纯函数中。

# 用useEffect模拟WillUnMount时的注意事项

用useEffect模拟compoentWillUnMount并不完全和compoentWillUnMount功能相等。

import React, { useState, useEffect } from 'react'

function FriendStatus({ friendId }) {
    const [status, setStatus] = useState(false)

    // DidMount 和 DidUpdate
    useEffect(() => {
        console.log(`开始监听 ${friendId} 在线状态`)

        // 【特别注意】
        // 此处并不完全等同于 WillUnMount
        // props 发生变化,即更新,也会执行结束监听
        // 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
        return () => {
            console.log(`结束监听 ${friendId} 在线状态`)
        }
    })

    return <div>
        好友 {friendId} 在线状态:{status.toString()}
    </div>
}

export default FriendStatus

useEffect中返回函数fn,如果useEffect依赖[],组件销毁时执行fn,这个时候useEffect等价于compoentWillUnMount。

useEffect无依赖或依赖某些状态如[a, b],组件更新时执行fn,即下一次执行useEffect之前会先执行上一次的fn,无论更新或卸载。

# useRef和useContext

useRef使用示例

import React, { useRef, useEffect } from 'react'

function UseRef() {
    const btnRef = useRef(null) // 初始值

    // 使用方式二:存储值
    // const numRef = useRef(0)
    // numRef.current

    useEffect(() => {
        // 使用方式一:获取DOM节点
        console.log(btnRef.current) // DOM 节点
    }, [])

    return <div>
        <button ref={btnRef}>click</button>
    </div>
}

export default UseRef

useContext使用示例:

import React, { useContext } from 'react'

// 主题颜色
const themes = {
    light: {
        foreground: '#000',
        background: '#eee'
    },
    dark: {
        foreground: '#fff',
        background: '#222'
    }
}

// 创建 Context
const ThemeContext = React.createContext(themes.light) // 初始值

function ThemeButton() {
    const theme = useContext(ThemeContext)

    return <button style={{ background: theme.background, color: theme.foreground }}>
        hello world
    </button>
}

function Toolbar() {
    return <div>
        <ThemeButton></ThemeButton>
    </div>
}

function App() {
    return <ThemeContext.Provider value={themes.dark}>
        <Toolbar></Toolbar>
    </ThemeContext.Provider>
}

export default App

# useReducer能代替redux吗

useReducer示例:

import React, { useReducer } from 'react'

const initialState = { count: 0 }

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 }
        case 'decrement':
            return { count: state.count - 1 }
        default:
            return state
    }
}

function App() {
    const [state, dispatch] = useReducer(reducer, initialState)

    return <div>
        count: {state.count}
        <button onClick={() => dispatch({ type: 'increment' })}>increment</button>
        <button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
    </div>
}

export default App

useReducer和redux的区别:

useReducer是useState的代替方案,用于state的复杂变化

useReducer是单组件状态管理,组件通讯需要props

redux是全局的状态管理,多组件共享数据

# 使用useMemo做性能优化

useMemo示例:

import React, { useState, memo, useMemo } from 'react'

// 子组件
// function Child({ userInfo }) {
//     console.log('Child render...', userInfo)

//     return <div>
//         <p>This is Child {userInfo.name} {userInfo.age}</p>
//     </div>
// }
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({ userInfo }) => {
    console.log('Child render...', userInfo)

    return <div>
        <p>This is Child {userInfo.name} {userInfo.age}</p>
    </div>
})

// 父组件
function App() {
    console.log('Parent render...')

    const [count, setCount] = useState(0)
    const [name, setName] = useState('f老师')

    // const userInfo = { name, age: 20 }
    // 用 useMemo 缓存数据,有依赖
    const userInfo = useMemo(() => {
        return { name, age: 21 }
    }, [name])

    return <div>
        <p>
            count is {count}
            <button onClick={() => setCount(count + 1)}>click</button>
        </p>
        <Child userInfo={userInfo}></Child>
    </div>
}

export default App

React默认会更新所有子组件,class组件使用SCU和PureComponent做优化,Hooks中使用useMemo,优化的原理是相同的。

# 使用useCallback做性能优化

useCallback示例

import React, { useState, memo, useMemo, useCallback } from 'react'

// 子组件,memo 相当于 PureComponent
const Child = memo(({ userInfo, onChange }) => {
    console.log('Child render...', userInfo)

    return <div>
        <p>This is Child {userInfo.name} {userInfo.age}</p>
        <input onChange={onChange}></input>
    </div>
})

// 父组件
function App() {
    console.log('Parent render...')

    const [count, setCount] = useState(0)
    const [name, setName] = useState('f老师')

    // 用 useMemo 缓存数据
    const userInfo = useMemo(() => {
        return { name, age: 21 }
    }, [name])

    // function onChange(e) {
    //     console.log(e.target.value)
    // }
    // 用 useCallback 缓存函数
    const onChange = useCallback(e => {
        console.log(e.target.value)
    }, [])

    return <div>
        <p>
            count is {count}
            <button onClick={() => setCount(count + 1)}>click</button>
        </p>
        <Child userInfo={userInfo} onChange={onChange}></Child>
    </div>
}

export default App

useMemo缓存数据

useCallback缓存函数

两者是React Hooks的常见优化策略

# 什么是自定义Hook

自定义Hook提现了怎么封装通用功能,自定义Hook带来了无限的扩展性,解耦代码

import { useState, useEffect } from 'react'
import axios from 'axios'

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
    const [loading, setLoading] = useState(false)
    const [data, setData] = useState()
    const [error, setError] = useState()

    useEffect(() => {
        // 利用 axios 发送网络请求
        setLoading(true)
        axios.get(url) // 发送一个 get 请求
            .then(res => setData(res))
            .catch(err => setError(err))
            .finally(() => setLoading(false))
    }, [url])

    return [loading, data, error]
}

export default useAxios

// 第三方 Hook
// https://nikgraf.github.io/react-hooks/
// https://github.com/umijs/hooks

export default useMousePosition

import React from 'react'
import useAxios from '../customHooks/useAxios'

function App() {
    const url = 'http://localhost:3000/'
    // 数组解构
    const [loading, data, error] = useAxios(url)

    if (loading) return <div>loading...</div>

    return error
        ? <div>{JSON.stringify(error)}</div>
        : <div>{JSON.stringify(data)}</div>
}

export default App

自定义Hook本质是一个函数,以use开头,内部可以使用useState、useEffect等获取其他Hooks,自定义返回结果,格式不限。

# 使用Hooks的两条重要规则

Hooks的使用规范

  1. 只能用于React函数组件和自定义Hook中,其他地方不可以,如class组件
  2. 只能用于顶层代码,不能在循环、判断中使用Hooks,如下面的这样使用Hooks都是不正确的。
let flag
// 不能在if中使用
if (flag) {
  useEffect(()=>{})
}

// 不能在循环中使用
for (let i = 0; i < 10; i++) const [name, setName] = useState()

// 不能在一些可能被打断逻辑的后面使用
if (flag) return
useEffect(()=>{})
  1. eslint插件eslint-plugin-react-hooks可以帮我们检测代码
// eslint配置文件
{
  "plugins": [
    "react-hooks"
  ],
  "rules": {
    "react-hooks/rules-of-hooks": "error", // 检查hook规则
    "react-hooks/exhaustive-deps": "warn" // 检查effect依赖
  }
}

# 为何Hooks要依赖于调用顺序?

Hooks的顺序必须保持一致,无论是render还是re-render,Hooks调用顺序必须一致,如果Hooks出现在循环、判断里,无法保证顺序一致。

import React, { useState, useEffect } from 'react'

function Teach({ couseName }) {
    // 函数组件,纯函数,执行完即销毁
    // 所以,无论组件初始化(render)还是组件更新(re-render)
    // 都会重新执行一次这个函数,获取最新的组件
    // 这一点和 class 组件不一样,class会保持一个实例

    // render: 初始化 state 的值 '张三'
    // re-render: 读取 state 的值 '张三'
    const [studentName, setStudentName] = useState('张三')

    // if (couseName) {
    //     const [studentName, setStudentName] = useState('张三')
    // }

    // render: 初始化 state 的值 'f'
    // re-render: 读取 state 的值 'f'
    const [teacherName, setTeacherName] = useState('f')

    // if (couseName) {
    //     useEffect(() => {
    //         // 模拟学生签到
    //         localStorage.setItem('name', studentName)
    //     })
    // }

    // render: 添加 effect 函数
    // re-render: 替换 effect 函数(内部的函数也会重新定义)
    useEffect(() => {
        // 模拟学生签到
        localStorage.setItem('name', studentName)
    })

    // render: 添加 effect 函数
    // re-render: 替换 effect 函数(内部的函数也会重新定义)
    useEffect(() => {
        // 模拟开始上课
        console.log(`${teacherName} 开始上课,学生 ${studentName}`)
    })

    return <div>
        课程:{couseName},
        讲师:{teacherName},
        学生:{studentName}
    </div>
}

export default Teach

# class组件逻辑复用有哪些问题

Mixins的问题:

  1. 变量作用域来源不清
  2. 属性重名
  3. Mixins引入过多会导致顺序冲突

高阶组件HOC:

  1. 组件层级嵌套过多,不易渲染,不易调试
  2. HOC会劫持props,必须严格规范,容易出现疏漏

Render Prop

  1. 学习成本比较高,不易理解
  2. 只能传递纯函数,默认情况下纯函数功能有限

# Hooks组件逻辑复用有哪些好处

  1. 完全符合Hooks规则,没有其他要求,易于理解
  2. 变量作用域明确
  3. 不会产生组件嵌套
import { useState, useEffect } from 'react'

function useMousePosition() {
    const [x, setX] = useState(0)
    const [y, setY] = useState(0)

    useEffect(() => {
        function mouseMoveHandler(event) {
            setX(event.clientX)
            setY(event.clientY)
        }

        // 绑定事件
        document.body.addEventListener('mousemove', mouseMoveHandler)

        // 解绑事件
        return () => document.body.removeEventListener('mousemove', mouseMoveHandler)
    }, [])

    return [x, y]
}

export default useMousePosition

import React from 'react'
import useMousePosition from '../customHooks/useMousePosition'

function App() {
    const [x, y] = useMousePosition()
    return <div style={{ height: '500px', backgroundColor: '#ccc' }}>
        <p>鼠标位置 {x} {y}</p>
    </div>
}

export default App

# Hooks使用中的几个注意事项

  • useState初始化值只有第一次有效
import React, { useState } from 'react'

// 子组件
function Child({ userInfo }) {
    // render: 初始化 state
    // re-render: 只恢复初始化的 state 值,不会再重新设置新的值
    //            只能用 setName 修改
    const [ name, setName ] = useState(userInfo.name)

    return <div>
        <p>Child, props name: {userInfo.name}</p>
        <p>Child, state name: {name}</p>
    </div>
}


function App() {
    const [name, setName] = useState('f')
    const userInfo = { name }

    return <div>
        <div>
            Parent &nbsp;
            <button onClick={() => setName('fei')}>setName</button>
        </div>
        <Child userInfo={userInfo}/>
    </div>
}

export default App
  • 当useEffect第二个参数有值时,useEffect内部不能修改state
import React, { useState, useRef, useEffect } from 'react'

function UseEffectChangeState() {
    const [count, setCount] = useState(0)

    // 模拟 DidMount
    const countRef = useRef(0)
    useEffect(() => {
        console.log('useEffect...', count)

        // 定时任务
        const timer = setInterval(() => {
            console.log('setInterval...', countRef.current)
            // setCount(count + 1)
            setCount(++countRef.current)
        }, 1000)

        // 清除定时任务
        return () => clearTimeout(timer)
    }, []) // 依赖为 []

    // 依赖为 [] 时: re-render 不会重新执行 effect 函数
    // 没有依赖:re-render 会重新执行 effect 函数

    return <div>count: {count}</div>
}

export default UseEffectChangeState
  • 当依赖是引用类型时,useEffect会出现死循环
function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setCount(count + 1)
  }, [{}])

  return <span>{count}</span>
}

export default App

useEffect的第二个参数是一个依赖,如果依赖变了这个函数要重新执行,Hook利用Object.is来判断依赖项是否更新

# Hooks的几个小题

  1. 为什么会有React Hooks,它解决了哪些问题?

完善函数组件的能力,函数更适合React组件,组件逻辑复用,Hooks表现好。class组件不易拆解不易测试,逻辑混乱。

  1. React Hooks如何模拟组件生命周期?

模拟componentDidMount - useEffect依赖[]

模拟componetUpdate - useEffect无依赖,或者依赖[a, b]

模拟componentWillUnMount - useEffect中返回一个函数

useEffect中返回函数fn:

useEffect依赖[],组件销毁时执行fn,等价于WillUnMount

useEffect无依赖或依赖[a, b],组件更新时执行fn,即,下一次执行useEffect之前就会执行fn,无论更新还是卸载。

  1. Hooks相比HOC和Render Prop有哪些优点?

完全符合Hooks原有规则,没有其他要求,易于理解。变量作用域明确,不会产生组件嵌套。