函数组件的特点:
- 没有组件实例
- 没有生命周期
- 没有state和setState,只能接收props
class组件的问题:
- 大型组件很难拆分和重构,不易于测试
- 相同业务逻辑分散到各个方法中,逻辑混乱
- 复用逻辑复杂,如Mixins、HOC、Render Prop
React提倡函数式编程,函数更灵活,更易拆分,更易测试,但函数组件太简单没有实例没有生命周期没有state,需要增强能力 - 所以引入了Hooks。
# 用useState实现state和setState功能
useState Hook就是让函数组件实现state和setState功能
默认函数组件没有state
函数组件是一个纯函数,执行完即销毁,无法存储state,需要state hook,把state功能“钩”到纯函数中
useState使用总结:
- useState(0)传入初始值,返回数组[state, setState]
- 通过state获取值
- 通过setState修改值
hooks命名规范
- 所有的Hooks都使用use开头,如useXXX
- 自定义Hook也要以use开头
- 非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的使用总结:
- 模拟componentDidMount - useEffect依赖 []
- 模拟componentDidUpdate - useEffect无依赖,或者依赖[a, b] (a, b代表状态)
- 模拟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的使用规范
- 只能用于React函数组件和自定义Hook中,其他地方不可以,如class组件
- 只能用于顶层代码,不能在循环、判断中使用Hooks,如下面的这样使用Hooks都是不正确的。
let flag
// 不能在if中使用
if (flag) {
useEffect(()=>{})
}
// 不能在循环中使用
for (let i = 0; i < 10; i++) const [name, setName] = useState()
// 不能在一些可能被打断逻辑的后面使用
if (flag) return
useEffect(()=>{})
- 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的问题:
- 变量作用域来源不清
- 属性重名
- Mixins引入过多会导致顺序冲突
高阶组件HOC:
- 组件层级嵌套过多,不易渲染,不易调试
- HOC会劫持props,必须严格规范,容易出现疏漏
Render Prop
- 学习成本比较高,不易理解
- 只能传递纯函数,默认情况下纯函数功能有限
# Hooks组件逻辑复用有哪些好处
- 完全符合Hooks规则,没有其他要求,易于理解
- 变量作用域明确
- 不会产生组件嵌套
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
<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的几个小题
- 为什么会有React Hooks,它解决了哪些问题?
完善函数组件的能力,函数更适合React组件,组件逻辑复用,Hooks表现好。class组件不易拆解不易测试,逻辑混乱。
- React Hooks如何模拟组件生命周期?
模拟componentDidMount - useEffect依赖[]
模拟componetUpdate - useEffect无依赖,或者依赖[a, b]
模拟componentWillUnMount - useEffect中返回一个函数
useEffect中返回函数fn:
useEffect依赖[],组件销毁时执行fn,等价于WillUnMount
useEffect无依赖或依赖[a, b],组件更新时执行fn,即,下一次执行useEffect之前就会执行fn,无论更新还是卸载。
- Hooks相比HOC和Render Prop有哪些优点?
完全符合Hooks原有规则,没有其他要求,易于理解。变量作用域明确,不会产生组件嵌套。