危险

为之则易,不为则难

0%

06_Hooks 进阶

今日目标

✔ 掌握 useEffect 清理副作用。

✔ 掌握 useRef 操作 DOM。

✔ 掌握 useContext 组件通讯。

useEffect 清理副作用

目标

掌握 useEffect 清理副作用的写法。

内容

  • useEffect 可以返回一个函数,这个函数称为清理函数,在此函数内用来执行清理相关的操作(例如事件解绑、清除定时器等)。

  • 清理函数的执行时机:下一次副作用回调函数调用时以及组件卸载时

  • 建议:一个 useEffect 只用来处理一个功能,有多个功能时,可以使用多个 useEffect。

执行时机演示

App.js

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useState } from 'react'
import Test from './Test'

export default function App() {
const [flag, setFlag] = useState(true)
return (
<div>
{flag && <Test />}
<button onClick={() => setFlag(!flag)}>销毁/创建</button>
</div>
)
}

Test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useEffect, useState } from 'react'

export default function Test() {
const [count, setCount] = useState(0)

useEffect(() => {
console.log('effect')
return () => {
// 一般用来清理上一次的副作用
console.log('clear effect')
}
})
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
{count}
<button onClick={handleClick}>click</button>
</div>
)
}

清理定时器演示

优化昨天的倒计时案例:组件销毁时清除定时器,Test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useEffect } from 'react'

export default function Test() {
const [count, setCount] = useState(10)
useEffect(() => {
const timer = setInterval(() => {
console.log(1)
setCount((count) => count - 1)
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
return (
<div>
<h3>{count}</h3>
</div>
)
}

小结

useEffect 清理函数的执行时机是什么?

跟随鼠标的天使 📝

目标

能够完成让图片跟随鼠标移动的效果。

步骤

  1. 通过 useState 提供状态。

  2. 通过 useEffect 给 document 注册鼠标移动事件

  3. 在事件回调里面修改状态为鼠标的坐标。

  4. 组件销毁的时候记得清理副作用(解绑事件)。

代码

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useState, useEffect } from 'react'
import avatar from './images/avatar.png'

export default function App() {
const [pos, setPos] = useState({
x: 0,
y: 0,
})
useEffect(() => {
const move = (e) => {
setPos({
x: e.pageX,
y: e.pageY,
})
}
document.addEventListener('mousemove', move)
return () => {
document.removeEventListener('mousemove', move)
}
}, [])
return (
<div>
<img src={avatar} alt='头像' style={{ position: 'absolute', top: pos.y, left: pos.x }} />
</div>
)
}

自定义 Hook

目标

能够使用自定义的 Hook 实现状态逻辑的复用。

内容

  • 目的:复用状态逻辑。

  • 自定义 Hook 是一个函数,规定函数名称必须以 use 开头,格式是 useXxx,React 内部会据此来区分是否是一个 Hook。

  • 自定义 Hook 只能在函数组件或其他自定义 Hook 中使用,否则,会报错!

案例

封装一个获取鼠标位置的 Hook,hooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useState, useEffect } from 'react'
export const useMouse = () => {
const [pos, setPos] = useState({
x: 0,
y: 0,
})
useEffect(() => {
const move = (e) => {
setPos({
x: e.pageX,
y: e.pageY,
})
}
document.addEventListener('mousemove', move)
return () => {
document.removeEventListener('mousemove', move)
}
}, [])
return pos
}

App.js

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
import avatar from './images/avatar.png'
import { useMouse } from './hooks'
export default function App() {
const pos = useMouse()
return (
<div>
<img src={avatar} alt='头像' style={{ position: 'absolute', top: pos.y, left: pos.x }} />
</div>
)
}

封装记录滚动位置的 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const useScroll = () => {
const [scroll, setScroll] = useState({
scrollLeft: 0,
scrollTop: 0,
})
useEffect(() => {
const scroll = (e) => {
setScroll({
scrollLeft: window.pageXOffset,
scrollTop: window.pageYOffset,
})
}
window.addEventListener('scroll', scroll)
return () => {
window.removeEventListener('scroll', scroll)
}
}, [])
return scroll
}

小结

自定义 Hook 的作用/目的是什么?

useEffect 发送请求

目标

能够在函数组件中通过 useEffect 发送 AJAX 请求。

内容

  • useEffect 是专门用来处理副作用的,所以发送请求这个副作用可以在 useEffect 回调内进行处理。

  • 注意:useEffect 的回调只能是一个同步函数,即不能使用 async 进行修饰。

  • 原因:如果 useEffect 的回调是异步的,此时返回值会被 Promise 化,这样的话就无法保证清理函数被立即调用。

  • 若需要使用 async/await 语法,可以在 useEffect 回调内部再次创建 async 函数并调用。

错误演示

1
2
3
4
5
// 发请求是没问题,但涉及清理副作用的操作就出事了
useEffect(async () => {
const res = await xxx()
return () => {}
}, [])

正确使用

1
2
3
4
5
6
7
8
useEffect(() => {
async function fetchMyAPI() {
let url = 'http://something/' + productId
const response = await myFetch(url)
}

fetchMyAPI()
}, [productId])

演示发请求

  1. 准备初始状态 list 和修改状态的方法 setList。

  2. 在 useEffect 内部定义自己的请求函数。

  3. 函数内部通过 axios 发送请求并把返回的数据通过 setList 设置到 list 中。

  4. 调用请求函数。

  5. 渲染 list。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState, useEffect } from 'react'
import axios from 'axios'

export default function App() {
const [list, setList] = useState([])
useEffect(() => {
const getData = async () => {
const res = await axios.get('http://geek.itheima.net/v1_0/user/channels')
setList(res.data.data.channels)
}
getData()
}, [])
return (
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}

小结

useEffect 的回调函数不能是异步的,那么如何使用 async/await 语法来简化代码。

useRef 操作 DOM

目标

能够使用 useRef 操作 DOM。

内容

使用场景:DOM 操作或获取类组件的实例。

使用

  • 参数:在获取 DOM 时,一般都设置为 null。

  • 返回值:返回一个带有 current 属性的对象,通过该对象就可以得到 DOM 对象或类组件实例。

useRef 获取 DOM

  1. 使用 useRef 创建一个有 current 属性的 ref 对象,{ current: null }
1
const xxxRef = useRef(null)
  1. 通过 DOM 的 ref 属性和上面创建的对象进行关联。
1
<div ref={xxxRef}></div>
  1. 通过 xxxRef.current 就可以访问到对应的 DOM 啦。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useRef } from 'react'

const App = () => {
const inputRef = useRef(null)
const add = () => {
console.log(inputRef.current.value)
}
return (
<section>
<input placeholder='请输入内容' ref={inputRef} />
<button onClick={add}>添加</button>
</section>
)
}

export default App

🤔 useRef 每次都会返回相同的引用,而 createRef 每次渲染都会返回一个新的引用。

useRef 获取类组件

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useRef } from 'react'
import Test from './Test'

const App = () => {
const testClassCmp = useRef(null)
const add = () => {
testClassCmp.current.handleClick()
}
return (
<section>
<Test ref={testClassCmp} />
<button onClick={add}>添加</button>
</section>
)
}

export default App

Test.js

1
2
3
4
5
6
7
8
9
10
import React, { Component } from 'react'

export default class Test extends Component {
handleClick() {
console.log(1)
}
render() {
return <div>类组件</div>
}
}

useRef 共享数据

目标

掌握 useRef 共享数据的写法,完成点击按钮开始倒计时的功能。

基础写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState } from 'react'

export default function App() {
const [count, setCount] = useState(10)
const handleStart = () => {
setInterval(() => {
// setCount(count - 1)
setCount((count) => count - 1)
}, 1000)
}
return (
<div>
<h3>{count}</h3>
<button onClick={handleStart}>开始倒计时</button>
</div>
)
}

问题:多次点击按钮时会开启多个定时器控制 count,乱了!

局部变量

🧐 了解局部变量的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState } from 'react'

export default function Test() {
const [count, setCount] = useState(10)
let timer = null
const handleClick = () => {
// 每次点击按钮整个 Test 函数都会执行 1 此(当然也包括这儿)
// 导致每次都会产生 1 个闭包变量 timer
// !其实每次清除的是自己的闭包变量 timer,而不是上一次的
clearInterval(timer)
timer = setInterval(() => {
setCount((count) => count - 1)
}, 1000)
}
return (
<div>
<h3>{count}</h3>
<button onClick={handleClick}>开启</button>
</div>
)
}

思考:为什么下面代码的 timer 能保证是同一个,和上面写法的不同是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useEffect } from 'react'

export default function Test() {
const [count, setCount] = useState(10)
let timer = null // !#1
useEffect(() => {
// 这里代码只会执行 1 次,也就只产生了 1 次闭包变量 timer
// 保证了 #2 处的 timer 变量就是第 1 次被引用的 #1 处的闭包变量 timer
// eslint-disable-next-line react-hooks/exhaustive-deps
timer = setInterval(() => {
setCount((count) => count - 1)
}, 1000)
return () => {
clearInterval(timer) // !#2
}
}, [])
return (
<div>
<h3>{count}</h3>
</div>
)
}

全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState } from 'react'
let timer

export default function App() {
const [count, setCount] = useState(10)
const handleStart = () => {
clearInterval(timer)
timer = setInterval(() => {
setCount((count) => count - 1)
}, 1000)
}
return (
<div>
<h3>{count}</h3>
<button onClick={handleStart}>开始倒计时</button>
</div>
)
}

全局变量的问题:多个组件实例之间会共用一个全局变量,以至于会相互影响,可以通过以下代码验证。

App.js

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
import Hello from './Hello'

export default function App() {
return (
<div>
<Hello />
<Hello />
</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState } from 'react'
let timer

export default function App() {
const [count, setCount] = useState(10)
const handleStart = () => {
clearInterval(timer)
timer = setInterval(() => {
setCount((count) => count - 1)
}, 1000)
}
return (
<div>
<h3>{count}</h3>
<button onClick={handleStart}>开始倒计时</button>
</div>
)
}

解决方案

useRef:可以实现多次渲染之间进行数据共享,保证了更新期间共用同一个 ref 对象(可以先理解为是一个全局变量)的同时,多个组件实例之间又不会相互影响(因为它是在组件内部的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState, useRef } from 'react'
export default function App() {
// #1
const timer = useRef(null)
const [count, setCount] = useState(10)
const handleStart = () => {
// #2
clearInterval(timer.current)
// #3
timer.current = setInterval(() => {
setCount((count) => count - 1)
}, 1000)
}
return (
<div>
<h3>{count}</h3>
<button onClick={handleStart}>开始倒计时</button>
</div>
)
}

createContext

目标

回顾 Context 跨级组件通讯的使用。

内容

  • 使用场景:跨组件共享数据。

  • Context 作用:实现跨组件传递数据,而不必在每一个组件传递 props,简化组件之间数据传递的过程。

  • 使用步骤。

    a,祖先组件通过 <Context.Provider value/> 组件配合 value 属性提供数据。

    b,后代组件通过 <Context.Consumer/> 组件配合函数获取提供的数据。

    c,如果祖先组件没有使用 <Context.Provider/> 组件进行包裹,那么 Consumer 获取到的是 React.createContext(defaultValue) 的 defaultValue 值。

步骤

需求:App 根组件经过 Parent 组件把数据传递到 Child 组件。

  1. 新建 countContext.js,通过 createContext 方法创建 Context 对象。

  2. App.js 根组件通过 <Context.Provider/> 组件配合 value 属性提供数据。

  3. Child.js 孙组件通过 <Context.Consumer/> 消费数据。

代码

countContext.js

1
2
import { createContext } from 'react'
export const Context = createContext()

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import { Context } from './countContext'
import Parent from './Parent'

export default function App() {
return (
<Context.Provider value={{ count: 0 }}>
App
<hr />
<Parent />
</Context.Provider>
)
}

Parent.js

1
2
3
4
5
6
7
8
9
10
import Child from './Child'
export default function Parent() {
return (
<div>
Parent
<hr />
<Child />
</div>
)
}

Child.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { context } from './countContext'

export default function Child() {
return (
<Context.Consumer>
{(value) => {
return (
<div>
Child
<h3>{value.count}</h3>
</div>
)
}}
</Context.Consumer>
)
}

小结

useRef 的使用步骤是什么?

useContext 使用

目标

能够通过 useContext 实现跨级组件通讯。

内容

  • 作用:在函数组件中,获取 <Context.Provider/> 提供的数据。

  • 参数:Context 对象,即通过 React.createContext 函数创建的对象。

  • 返回:<Context.Provider/> 提供的 value 数据。

1
2
3
4
5
6
7
8
9
10
11
12
import { useContext } from 'react'
import { Context } from './countContext'

export default function Child() {
const value = useContext(Context)
return (
<div>
Child
<h3>{value.count}</h3>
</div>
)
}

购物车案例

获取列表数据

📝 需求:本地有,就用本地的,本地没有,从远端获取。

  1. 在新的 useEffect 中,获取本地数据。

  2. 如果本地有,就把获取到的数据设置到 list 数组。

  3. 如果本地没有,发送请求获取远端数据,并把结果设置到 list 数组。

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 初始的 state 也就没有必要这样写了
/* const [list, setList] = useState(() => {
return JSON.parse(localStorage.getItem('list')) || arr
}) */
// 建议
const [list, setList] = useState([])

useEffect(() => {
// 判断本地是否有数据
const arr = JSON.parse(localStorage.getItem('list')) || []
if (arr.length) {
return setList(arr)
}
// 本地没有数据,发送请求,获取数据
const getList = async () => {
const res = await axios.get('https://www.escook.cn/api/cart')
setList(res.data.list)
}
getList()
}, [])

MyCount 组件的封装

components/MyCount/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import './index.scss'
export default function MyCount() {
return (
<div className='my-counter'>
<button type='button' className='btn btn-light'>
-
</button>
<input type='number' className='form-control inp' value='1' />
<button type='button' className='btn btn-light'>
+
</button>
</div>
)
}

components/MyCount/index.scss

1
2
3
4
5
6
7
8
.my-counter {
display: flex;
.inp {
width: 45px;
text-align: center;
margin: 0 10px;
}
}

components/GoodItem/index.js

1
2
3
4
5
6
7
8
import MyCount from '../MyCount'
;<div className='right'>
<div className='top'>{goods_name}</div>
<div className='bottom'>
<span className='price'>¥ {goods_price}</span>
<MyCount />
</div>
</div>

数量控制 props

  • 设置初始值
  1. GoodsItem 中传递 count={goods_count} 给 MyCount 组件。

  2. MyCount 组件接收并绑定给 input 的 value。

  • 点击按钮修改数据
  1. App.js 中准备 changeCount(修改数据的方法),并传递给 GoodsItem。

  2. GoodsItem 中进行接收,并继续传递 changeCount={(count) => changeCount(id, count)} 到 MyCount。

  3. 给 MyCount 中的加减按钮绑定点击事件,调用传递过来的 changeCount 并传递期望的 count。

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default function App() {
const changeCount = (id, count) => {
setList(
list.map((item) => {
if (item.id === id) {
return {
...item,
goods_count: count,
}
} else {
return item
}
})
)
}

return (
<div className='app'>
<MyHeader>购物车</MyHeader>
{list.map((item) => (
<GoodsItem key={item.id} {...item} changeState={changeState} changeCount={changeCount}></GoodsItem>
))}
</div>
)
}

components/GoodsItem/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function GoodsItem({ goods_count, goods_img, goods_name, goods_price, goods_state, id, changeState, changeCount }) {
return (
<div className='my-goods-item'>
<div className='right'>
<div className='top'>{goods_name}</div>
<div className='bottom'>
<span className='price'>¥ {goods_price}</span>
<MyCount count={goods_count} changeCount={(count) => changeCount(id, count)} />
</div>
</div>
</div>
)
}

components/MyCount/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function MyCount({ count, changeCount }) {
const plus = () => {
changeCount(count + 1)
}
const minus = () => {
if (count <= 1) return
changeCount(count - 1)
}
return (
<div className='my-counter'>
<button type='button' className='btn btn-light' onClick={minus}>
-
</button>
<input type='number' className='form-control inp' value={count} />
<button type='button' className='btn btn-light' onClick={plus}>
+
</button>
</div>
)
}

数量控制 useContext

  1. 在 App.js 中创建 Context 对象,并且导出
1
export const Context = createContext()
  1. 在 App.js 中,通过 Provider 提供方法
1
2
3
4
5
6
7
8
9
10
11
<Context.Provider value={{ changeCount }}>
<div className='app'>
<MyHeader>购物车</MyHeader>

{list.map((item) => (
<GoodsItem key={item.id} {...item} changeState={changeState}></GoodsItem>
))}

<MyFooter list={list} changeAll={changeAll}></MyFooter>
</div>
</Context.Provider>
  1. components/GoodsItem/index.js 中把 id 传递过去
1
2
3
4
5
6
7
<div className='right'>
<div className='top'>{goods_name}</div>
<div className='bottom'>
<span className='price'>¥ {goods_price}</span>
<MyCount count={goods_count} id={id} />
</div>
</div>
  1. 在 myCount 组件中,使用 useContext 获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useContext } from 'react'
import { Context } from '../../App'
import './index.scss'

export default function MyCount({ count, id }) {
const { changeCount } = useContext(Context)
const plus = () => {
changeCount(id, count + 1)
}
const minus = () => {
if (count <= 1) return
changeCount(id, count - 1)
}
return (
<div className='my-counter'>
<button type='button' className='btn btn-light' onClick={minus}>
-
</button>
<input type='number' className='form-control inp' value={count} />
<button type='button' className='btn btn-light' onClick={plus}>
+
</button>
</div>
)
}