今日目标
✔ 掌握 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 清理函数的执行时机是什么?
跟随鼠标的天使 📝
目标
能够完成让图片跟随鼠标移动的效果。
步骤
通过 useState 提供状态。
通过 useEffect 给 document 注册鼠标移动事件
在事件回调里面修改状态为鼠标的坐标。
组件销毁的时候记得清理副作用(解绑事件)。
代码
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,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])
|
演示发请求
准备初始状态 list 和修改状态的方法 setList。
在 useEffect 内部定义自己的请求函数。
函数内部通过 axios 发送请求并把返回的数据通过 setList 设置到 list 中。
调用请求函数。
渲染 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 操作或获取类组件的实例。
使用
useRef 获取 DOM
- 使用 useRef 创建一个有 current 属性的 ref 对象,
{ current: null }
。
1
| const xxxRef = useRef(null)
|
- 通过 DOM 的 ref 属性和上面创建的对象进行关联。
1
| <div ref={xxxRef}></div>
|
- 通过 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) => 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 = () => { 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 useEffect(() => { timer = setInterval(() => { setCount((count) => count - 1) }, 1000) return () => { clearInterval(timer) } }, []) 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() { const timer = useRef(null) const [count, setCount] = useState(10) const handleStart = () => { clearInterval(timer.current) 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 组件。
新建 countContext.js
,通过 createContext 方法创建 Context 对象。
App.js
根组件通过 <Context.Provider/>
组件配合 value 属性提供数据。
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> ) }
|
购物车案例
获取列表数据
📝 需求:本地有,就用本地的,本地没有,从远端获取。
在新的 useEffect 中,获取本地数据。
如果本地有,就把获取到的数据设置到 list 数组。
如果本地没有,发送请求获取远端数据,并把结果设置到 list 数组。
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
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
GoodsItem 中传递 count={goods_count}
给 MyCount 组件。
MyCount 组件接收并绑定给 input 的 value。
App.js
中准备 changeCount(修改数据的方法),并传递给 GoodsItem。
GoodsItem 中进行接收,并继续传递 changeCount={(count) => changeCount(id, count)}
到 MyCount。
给 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
- 在 App.js 中创建 Context 对象,并且导出
1
| export const Context = createContext()
|
- 在 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>
|
- 在
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>
|
- 在 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> ) }
|