文章出处: 拉 勾 大前端 高薪训练营
对函数型组件进行增强,让函数型组件可以存储状态,可以拥有处理副作用的能力,让开发者在不使用类组件的情况下,实现相同的功能。
为了复用逻辑增加无实际渲染效果的组件,增加了组件层级,显示十分臃肿,增加了调试的难度以及运行效率的降低
将一组相干的业务逻辑拆分到了多个生命周期函数中,在一个生命周期函数内,存在多个不相干的业务逻辑
Hooks 意为钩子, React Hooks 就是一堆钩子函数, React 通过这些钩子函数对函数型组件进行增强,不同的钩子函数提供了不同的功能
用于为函数组件引入状态
import {
useState} from 'react'
function App() {
const [count, setCount] = useState(0)
return (
<div>
<span>{
count}</span>
<button onClick={
()=>setCount(count+1)}> + 1</button>
</div>
);
}
export default App;
const [count, setCount] = useState(0)
const [person, setPerson] = useState({
name: '张三', age: 20 })
const [count, setCount] = useState(0)
<button onClick={
()=>setCount(count+1)}> + 1</button>
<button onClick={
()=>setPerson({
name: '李四', age: 30})}>setPerson</button>
<button onClick={
()=>setPerson({
...person, name: '李四'})}>setPerson(只改变一个属性,其他属性不变)</button>
// 当初始值是动态值
// 这样写每次渲染都会执行
const propsCount = props.count || 0
// const [count, setCount] = useState(propsCount)
// 应该这样写
const [count, setCount] = useState(() => {
return props.count || 0 // 只有第一次执行的时候才会执行
})
function handleCount () {
setCount((count) => {
const newCount = count + 1
document.title = newCount
return newCount
})
}
<button onClick={
handleCount}> + 1</button>
useReducer 是另一种让函数组件保存状态的方式,可以将 dispatch 传给子组件使用
import {
useReducer } from "react";
export default function App () {
function reducer (state, action) {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
return state;
}
}
const [count, dispatch] = useReducer(reducer, 0)
return (
<div>
<button onClick={
() => dispatch({
type: 'decrement'})}>-1</button>
<span>{
count}</span>
<button onClick={
() => dispatch({
type: 'increment'})}>+1</button>
</div>
)
}
在跨组件层级获取数据时简化获取数据的代码
import {
createContext, useContext } from "react";
const countContext = createContext()
export default function App () {
return (
<countContext.Provider value={
1000}>
<Foo />
</countContext.Provider>
)
}
function Foo () {
const value = useContext(countContext)
return (
<div>I am Foo {
value}</div>
)
}
让函数型组件拥有处理副作用的能力,类似生命周期函数
可以把 useEffect 看做 componentDidMount, componentDidUpdate 和 componentWillUnmount 这三个函数的组合
useEffect(() => {}) => componentDidMount, componentDidUpdate
useEffect(() => {}, []) => componentDidMount
useEffect(() => () => {}) => componentDidUpdate, componentWillUnmount
useEffect(() => () => {}, []) => componentWillUnmount
import {
useEffect, useState } from "react";
import ReactDOM from 'react-dom'
export default function App () {
const [count, setCount] = useState(0)
// 组件挂载完成之后执行,组件数据更新之后执行
// useEffect(() => {
// console.log('123')
// })
// 组件挂载完成之后执行
// useEffect(() => {
// console.log('456')
// }, [])
useEffect(() => {
return () => {
console.log('组件被卸载了')
}
})
return (
<div>
<span>{
count}</span>
<button onClick={
() => setCount(count+1)}>+1</button>
<button onClick={
() => ReactDOM.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
</div>
)
}
(1). 为 window 对象添加滚动事件
(2). 设置定时器让 count 数值每隔一秒增加 1
import {
useEffect, useState } from "react";
import ReactDOM from 'react-dom'
export default function App () {
const [count, setCount] = useState(0)
function onScroll () {
console.log('页面滚动了')
}
useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => {
window.removeEventListener('scroll', onScroll)
}
}, [])
useEffect(() => {
const timerId = setInterval(() => {
setCount(count => {
const newCount = count + 1
document.title = newCount
return newCount
})
}, 1000);
return () => {
clearTimeout(timerId)
}
}, [])
return (
<div>
<span>{
count}</span>
<button onClick={
() => {
ReactDOM.unmountComponentAtNode(document.getElementById('root')) }} >卸载组件</button>
</div>
)
}
useEffect 解决的问题
(1). 按照用途将代码进行分类(将一组相同的业务逻辑归置到了同一个副作用函数中)
(2). 简化重复代码,是组件内部代码更加清晰
只有指定数据发生变化时触发 effect
import {
useEffect, useState } from "react";
export default function App () {
const [count, setCount] = useState(0)
const [person, setPerson] = useState({
name: '张三'})
useEffect(() => {
// person 的变化不会触发 useEffect , 因为第二个数组参数中只监听了 count
document.title = count
console.log(111)
}, [count])
return (
<div>
<span>{
count}</span>
<button onClick={
() => setCount(count + 1)}> + 1</button>
<button onClick={
() => setPerson({
name: '李四'})}>setPerson</button>
</div>
)
}
useEffect(() => {
(async () => {
await axios.get()
})()
}, [])
useMemo 的行为类似 Vue 中的计算属性,可以检测某个值的变化,根据变化只计算新值。
useMemo 会缓存计算结果,如果检测子没有发生变化,及时组建重新渲染,也不会重新计算,此行为可以有助于避免在每个渲染上进行昂贵的计算。
import {
useState, useMemo } from "react";
export default function App () {
const [count, setCount] = useState(0)
const [bool, setBool] = useState(true)
const result = useMemo(() => {
console.log('111') // 只有 count 改变才会重新执行这个回调函数
return count * 2
}, [count])
return (
<div>
<span>{
count} {
result}</span>
<span>{
bool ? '真' : '假'}</span>
<button onClick={
() => setCount(count + 1)}> + 1</button>
<button onClick={
() => setBool(!bool)}>setBool</button>
</div>
)
}
性能优化,如果本组件中的数据没有发生变化,阻止组件更新,类似类组件中的 PureComponent 和 shouldComponentUpdate
import {
memo } from 'react'
const Foo = memo(function Foo () {
return <div>I am Foo</div>
})
性能优化,缓存函数,使用组件重新渲染时得到相同的函数实例。否则每次父组件渲染,函数变量的实例都会变化,导致里层组件被重新渲染
import {
useState, memo, useCallback } from "react";
const Foo = memo(function Foo (props) {
console.log('Foo 重新渲染了')
return <div>
<span>I am Foo</span>
<button onClick={
props.resetCount}>resetCount</button>
</div>
})
export default function App () {
const [count, setCount] = useState(0)
const resetCount = useCallback(() => {
setCount(0)
}, [setCount])
return (
<div>
<span>{
count}</span>
<button onClick={
() => setCount(count + 1)}> + 1</button>
<Foo resetCount={
resetCount} />
</div>
)
}
import {
useRef } from "react";
export default function App () {
const box = useRef()
return (
<div ref={
box}>
<button onClick={
() => console.log(box)}> DIV </button>
</div>
)
}
即使组件重新渲染,保存的数据仍然还在,保存的数据被更改不会触发组件重新渲染。
import {
useRef, useState, useEffect} from "react";
export default function App () {
const [count, setCount] = useState(0)
let timeId = useRef() // 夸组件生命周期
useEffect(() => {
// 使用这个 ref 的 current 属性存储数据
timeId.current = setInterval(() => {
setCount(count => count + 1)
}, 1000);
}, [])
const stopCount = () => {
console.log(timeId.current)
clearInterval(timeId.current)
}
const box = useRef()
return (
<div ref={
box}>
<span>{
count}</span>
<button onClick={
() => stopCount()}> 停止 </button>
</div>
)
}
自定义 Hook 是标准的封装和共享逻辑的方式
自定义 Hook 是一个函数,其名称以 use 开头
自定义 Hook 其实就是逻辑和内置 Hook 的组合
如何使用自定义 Hook:
import {
useState, useEffect} from "react";
import axios from "axios";
function useGetPost () {
const [post, setPost] = useState({
})
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => {
setPost(response.data)
})
}, [])
return [post, setPost]
}
export default function App () {
const [post, setPost] = useGetPost()
return (
<div>
<h1>{
post.title}</h1>
<div>{
post.body}</div>
</div>
)
}
封装公共逻辑:
import {
useState} from "react";
function useUpdateInput (initialState) {
const [value, setValue] = useState(initialState)
return {
value,
onChange: e => setValue(e.target.value)
}
}
export default function App () {
const usernameInput = useUpdateInput('')
const passwordInput = useUpdateInput('')
const submitForm = event => {
event.preventDefault();
console.log(usernameInput.value)
console.log(passwordInput.value)
}
return (
<form onSubmit={
submitForm}>
<input type="text" name="username" {
...usernameInput} />
<input type="password" name="password" {
...passwordInput} />
<input type="submit" />
</form>
)
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {
BrowserRouter as Router } from "react-router-dom";
import App from './App';
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root')
);
App.js
import {
Link, Route } from "react-router-dom";
import Home from './pages/Home'
import List from './pages/List'
export default function App () {
return (
<>
<div>
<Link to="/home/zhangsan">首页</Link>
<Link to="/list">列表页</Link>
</div>
<div>
<Route path="/home/:name" component={
Home} />
<Route path="/list" component={
List} />
</div>
</>
)
}
Home.js
import {
useHistory, useLocation, useRouteMatch, useParams } from "react-router-dom";
export default function Home(props) {
console.log(props)
console.log(useHistory())
console.log(useLocation())
console.log(useRouteMatch())
console.log(useParams())
return <div>
<h1>Home works</h1>
</div>;
}
输出结果:
{history: {…}, location: {…}, match: {…}, staticContext: undefined}
{length: 7, action: “PUSH”, location: {…}, createHref: ƒ, push: ƒ, …}
{pathname: “/home/zhangsan”, search: “”, hash: “”, state: undefined, key: “o6w5y3”}
{path: “/home/:name”, url: “/home/zhangsan”, isExact: true, params: {…}}
{name: “zhangsan”}
List.js
export default function List(props) {
console.log(props)
return <div>
<h1>List works</h1>
</div>;
}
使用数组 state 存储状态,用数组 setters 存储 setState方法,利用闭包管理,将 下标 stateIndex 缓存在闭包中,创建 对应的 setState.
// import { useState } from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function createSetter (index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setter = setters[stateIndex]
stateIndex ++
return [value, setter]
}
function render () {
stateIndex = 0
ReactDOM.render(
<App/>,
document.getElementById('root')
)
}
export default function App () {
const [count, setCount] = useState(0)
const [name, setName] = useState('张三')
return <div>
<span>{
count}</span>
<button onClick={
() => setCount(count + 1)}> + 1</button>
<span>{
name}</span>
<button onClick={
() => setName('李四')}> 李四</button>
</div>
}
// import { useState } from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function createSetter (index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setter = setters[stateIndex]
stateIndex ++
return [value, setter]
}
function render () {
stateIndex = 0
effectIndex = 0
ReactDOM.render(
<App/>,
document.getElementById('root')
)
}
// 上一次的依赖值
let prevDepsAry = []
let effectIndex = 0
/**
* useEffect
* @param {function} callback 回调函数
* @param {Array} depsAry 依赖数组
* @returns {function} 清理函数
*/
function useEffect (callback, depsAry) {
if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect 第一个参数必须是一个函数')
if (typeof depsAry === 'undefined') {
// 没有传递
callback()
} else {
// 判断 depsAry 是不是数组
if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect 第二个参数必须是一个数组')
// 获取上一次的状态值
let prevDeps = prevDepsAry[effectIndex]
// 将当前的依赖值和上一次的依赖值作对比,如果有变化,调用 callback
let hasChanged = prevDeps ? !depsAry.every((dep, index) => dep === prevDeps[index]) : true
// 判断值是否有变化
if (hasChanged) {
callback()
}
// 同步依赖值
prevDepsAry[effectIndex++] = depsAry
}
}
export default function App () {
const [count, setCount] = useState(0)
const [name, setName] = useState('张三')
useEffect(() => {
console.log('Hello')
}, [count])
useEffect(() => {
console.log('World')
}, [name])
// 测试不传监听数据的情况
useEffect(() => {
console.log('xxx')
}, [])
return <div>
<span>{
count}</span>
<button onClick={
() => setCount(count + 1)}> + 1</button>
<span>{
name}</span>
<button onClick={
() => setName('李四')}> 李四</button>
</div>
}
// import { useState } from 'react'
// import { useReducer } from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function createSetter (index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setter = setters[stateIndex]
stateIndex ++
return [value, setter]
}
function render () {
stateIndex = 0
effectIndex = 0
ReactDOM.render(
<App/>,
document.getElementById('root')
)
}
// 上一次的依赖值
let prevDepsAry = []
let effectIndex = 0
/**
* useEffect
* @param {function} callback 回调函数
* @param {Array} depsAry 依赖数组
* @returns {function} 清理函数
*/
function useEffect (callback, depsAry) {
if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect 第一个参数必须是一个函数')
if (typeof depsAry === 'undefined') {
// 没有传递
callback()
} else {
// 判断 depsAry 是不是数组
if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect 第二个参数必须是一个数组')
// 获取上一次的状态值
let prevDeps = prevDepsAry[effectIndex]
// 将当前的依赖值和上一次的依赖值作对比,如果有变化,调用 callback
let hasChanged = prevDeps ? !depsAry.every((dep, index) => dep === prevDeps[index]) : true
// 判断值是否有变化
if (hasChanged) {
callback()
}
// 同步依赖值
prevDepsAry[effectIndex++] = depsAry
}
}
function useReducer (reducer, initialState) {
const [state, setState] = useState(initialState)
function dispatch (action) {
const newState = reducer(state, action)
setState(newState)
}
return [state, dispatch]
}
export default function App () {
function reducer(state, action) {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
return state
}
}
const [cnt, dispatch] = useReducer(reducer, 0)
return <div>
<div>
<button onClick={
() => dispatch({
type: 'increment'})}> + 1</button>
<span>{
cnt}</span>
<button onClick={
() => dispatch({
type: 'decrement'})}> - 1</button>
</div>
</div>
}