它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Effect Hook 可以让你在函数组件中执行副作用操作
import React, { useState, useEffect } from "react";
function Counter () {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Counter Effect -- 1 --')
document.title = 'Counter ' + count
return () => {
console.log('Counter Effect -- 1 -- clear')
}
})
return (
<>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}> Add </button>
</>
)
}
该示例声明了一个函数组件,其useEffect
函数会在组件每次渲染(包括首次)的时候执行,首次执行一次,控制台打印输出如下:
点击【Add】–》 函数的count --state
变化 --》函数组件刷新 --》 useEffect
清理函数 --》新的useEffect
函数
什么是副作用操作?
数据网络请求、DOM操作、定时器等
有时候,在 React 更新 DOM 之后运行一些额外的代码,就可以使用一个useEffect
函数去操作
比如:在组件渲染之后,需要获取后端数据,可以在该函数内部操作;创建一个定时器、倒计时弹框展示等等
而有些副作用操作,需要在组件卸载之后清理(清除定时器、变量,避免内存泄露),有些就不用(数据请求),useEffect
函数返回一个函数,可以在其中执行清理操作
所以,一个
useEffect
就相当于componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合
useEffect
函数的第一个参数是函数,第二个参数是数组
数组中的记录的是useEffect
执行依赖的变量,这些变量可以是state
、props
或者是上下文等其他数据源,当依赖的变量发生变化后,useEffect
才会执行,这可以作为一种性能优化
下面示例主要用于订阅聊天室人员的在线/下线状态
从最基本的开始吧,定义人员类:
class Friend {
constructor (id, name) {
this.id = id
this.name = name
}
}
class Member extends Friend {
constructor (id, name, isOnline) {
super(id, name)
this.isOnline = isOnline
}
}
定义聊天室类,添加相关方法
class ChatRoomClass {
constructor () {
this.members = []
this.handlers = {}
}
/**
* 加入聊天室
* @param {Member} member
*/
join (member) {
this.members.push(member)
}
/**
* 改变在线状态
* @param {number|string} id
*/
changeLineStatus (id) {
const member = this.members.find(m => m.id === id)
if (member) {
member.isOnline = !member.isOnline
this.trigger(id)
}
}
/**
* 退出聊天室
* @param {Member} member
*/
out (member) {
const idx = this.members.findIndex(i => i === member)
idx > -1 && this.members.splice(idx, 1)
}
// 触发订阅了该 id 人员在线状态变化的函数
trigger (id) {
const handlerArr = this.handlers[id]
if (!Array.isArray(handlerArr)) return void 0
const member = this.members.find(m => m.id === id)
handlerArr.forEach(fn => fn(member))
}
subscribeToFriendStatus (id, fn) {
const member = this.members.find(m => m.id === id)
fn(member)
if (Array.isArray(this.handlers[id])) {
this.handlers[id].push(fn)
} else {
this.handlers[id] = [fn]
}
}
unsubscribeFromFriendStatus (id, fn) {
const handlerArr = this.handlers[id]
if (Array.isArray(handlerArr)) {
const idx = handlerArr.findIndex(h => fn === h)
idx > -1 && handlerArr.splice(idx, 1)
}
}
}
在FriendStatus
组件的useEffect
函数中添加在线状态的订阅,在清理函数中取消订阅
function FriendStatus (props) {
const [online, setOnline] = useState(false)
useEffect(() => {
console.log('FriendStatus useEffect -- excute')
function handleStatusChange (status) {
setOnline(status.isOnline)
}
ChatRoom.subscribeToFriendStatus(props.friend.id, handleStatusChange)
return () => {
console.log('FriendStatus useEffect -- clean up excute')
ChatRoom.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
}
}, [props.friend.id])
return (
<>
<span align="left">
<strong>{props.friend.name}</strong>
<span
style={{
backgroundColor: online ? '#0F0' : '#F00',
display: 'inline-block',
width: '12px',
height: '12px',
borderRadius: '50%'
}} />
</span>
</>
)
}
该组件的使用方式如下:
export default class Home extends React.Component {
constructor (props) {
super(props)
this.state = {
members: ChatRoom.members,
}
this.inputNameRef = React.createRef()
this.handleJoinChat = this.handleJoinChat.bind(this)
this.changeLineStatus = this.changeLineStatus.bind(this)
}
changeLineStatus (id) {
ChatRoom.changeLineStatus(id)
this.setState({
members: [...ChatRoom.members]
})
}
handleJoinChat () {
const inputVal = this.inputNameRef.current.value
if (!inputVal) return void 0
const member = new Member(
randomID(),
inputVal,
true,
)
ChatRoom.join(member)
this.setState({
members: [...ChatRoom.members]
}, () => {
this.inputNameRef.current.value = ''
})
}
render() {
const members = this.state.members
const adminWindow = (
<ul>
{members.map(m => (
<li key={m.id}>
<FriendStatus friend={m} />
<button onClick={() => this.changeLineStatus(m.id)}>Change</button>
</li>
))}
</ul>
)
const otherUserMemberSplit = members.map((item, idx, arr) => arr.filter(m => m.id !== item.id))
const otherUserWindows = (
<div style={{
width: '100vw',
display: 'flex'
}}>
{members.map((om, idx) => (
<div style={{
flex: '1 1 auto',
borderRight: '1px solid #000'
}} key={om.id}>
<h4 style={{
textDecoration: om.isOnline ? 'none' : 'line-through'
}}>【{om.name}】窗口</h4>
<ul>
{otherUserMemberSplit[idx].map(im => <li key={im.id}>
<FriendStatus friend={im} />
</li>)}
</ul>
</div>
))}
</div>
)
return (
<>
<div>
<input type="text" ref={this.inputNameRef} />
<input type="button" onClick={this.handleJoinChat} value="加入" />
</div>
<hr />
<h4>管理员窗口</h4>
{adminWindow}
<hr />
<h4>其他用户窗口</h4>
{otherUserWindows}
</>
)
}
}
代码解释:
Home
组件中定义了整个页面的渲染情况,包含“管理员窗口”(主要用于切换某个用户的在线状态,订阅了该用户在线状态的组件更新),还有"其他人员窗口"(展示除自身以外的其他人员的在线状态 – 订阅)比如:在同一个聊天室中,一个用户相当于订阅了其他所有用户的在线状态,当某一个用户的在线状态变更后,其他用户就能够及时看到该用户的状态变化
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题
将Counter和FriendStatus组合起来,互不影响:
function FriendStatusWithCounter (props) {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Counter Effect -- 1 --')
document.title = 'Counter ' + count
return () => {
console.log('Counter Effect -- 1 -- clear')
}
}, [count])
const [online, setOnline] = useState(false)
useEffect(() => {
console.log('FriendStatusWithCounter useEffect -- excute')
function handleStatusChange (status) {
setOnline(status.isOnline)
}
ChatRoom.subscribeToFriendStatus(props.friend.id, handleStatusChange)
return () => {
console.log('FriendStatusWithCounter useEffect -- clean up excute')
ChatRoom.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
}
}, [props.friend.id])
return (
<div style={{
display: 'inline-block',
width: 'auto',
padding: '10px',
border: '1px solid #AAA',
marginBottom: '10px',
}}>
<div align="left">
<strong>{props.friend.name}</strong>
<span
style={{
backgroundColor: online ? '#0F0' : '#F00',
display: 'inline-block',
width: '12px',
height: '12px',
borderRadius: '50%'
}} />
</div>
{!props.admin && <div>
<strong>Count: {count}</strong>
<button onClick={() => setCount(count + 1)}> Add </button>
</div>}
</div>
)
}
完整代码如下:
import React, { useState, useEffect } from "react";
import './index.css'
function randomID () {
return Math.random().toString(36).slice(-6)
}
class Friend {
constructor (id, name) {
this.id = id
this.name = name
}
}
class Member extends Friend {
constructor (id, name, isOnline) {
super(id, name)
this.isOnline = isOnline
}
}
class ChatRoomClass {
constructor () {
this.members = []
this.handlers = {}
}
/**
* 加入聊天室
* @param {Member} member
*/
join (member) {
this.members.push(member)
}
/**
* 改变在线状态
* @param {number|string} id
*/
changeLineStatus (id) {
const member = this.members.find(m => m.id === id)
if (member) {
member.isOnline = !member.isOnline
this.trigger(id)
}
}
/**
* 退出聊天室
* @param {Member} member
*/
out (member) {
const idx = this.members.findIndex(i => i === member)
idx > -1 && this.members.splice(idx, 1)
}
// 触发订阅了该 id 人员在线状态变化的函数
trigger (id) {
const handlerArr = this.handlers[id]
if (!Array.isArray(handlerArr)) return void 0
const member = this.members.find(m => m.id === id)
handlerArr.forEach(fn => fn(member))
}
subscribeToFriendStatus (id, fn) {
const member = this.members.find(m => m.id === id)
fn(member)
if (Array.isArray(this.handlers[id])) {
this.handlers[id].push(fn)
} else {
this.handlers[id] = [fn]
}
}
unsubscribeFromFriendStatus (id, fn) {
const handlerArr = this.handlers[id]
if (Array.isArray(handlerArr)) {
const idx = handlerArr.findIndex(h => fn === h)
idx > -1 && handlerArr.splice(idx, 1)
}
}
}
const ChatRoom = new ChatRoomClass()
function FriendStatusWithCounter (props) {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Counter Effect -- 1 --')
document.title = 'Counter ' + count
return () => {
console.log('Counter Effect -- 1 -- clear')
}
}, [count])
const [online, setOnline] = useState(false)
useEffect(() => {
console.log('FriendStatusWithCounter useEffect -- excute')
function handleStatusChange (status) {
setOnline(status.isOnline)
}
ChatRoom.subscribeToFriendStatus(props.friend.id, handleStatusChange)
return () => {
console.log('FriendStatusWithCounter useEffect -- clean up excute')
ChatRoom.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
}
}, [props.friend.id])
return (
<div style={{
display: 'inline-block',
width: 'auto',
padding: '10px',
border: '1px solid #AAA',
marginBottom: '10px',
}}>
<div align="left">
<strong>{props.friend.name}</strong>
<span
style={{
backgroundColor: online ? '#0F0' : '#F00',
display: 'inline-block',
width: '12px',
height: '12px',
borderRadius: '50%'
}} />
</div>
{!props.admin && <div>
<strong>Count: {count}</strong>
<button onClick={() => setCount(count + 1)}> Add </button>
</div>}
</div>
)
}
export default class Home extends React.Component {
constructor (props) {
super(props)
this.state = {
members: ChatRoom.members,
}
this.inputNameRef = React.createRef()
this.handleJoinChat = this.handleJoinChat.bind(this)
this.changeLineStatus = this.changeLineStatus.bind(this)
}
changeLineStatus (id) {
ChatRoom.changeLineStatus(id)
this.setState({
members: [...ChatRoom.members]
})
}
handleJoinChat () {
const inputVal = this.inputNameRef.current.value
if (!inputVal) return void 0
const member = new Member(
randomID(),
inputVal,
true,
)
ChatRoom.join(member)
this.setState({
members: [...ChatRoom.members]
}, () => {
this.inputNameRef.current.value = ''
})
}
render() {
const members = this.state.members
const adminWindow = (
<ul>
{members.map(m => (
<li key={m.id}>
<FriendStatusWithCounter admin friend={m} />
<button onClick={() => this.changeLineStatus(m.id)}>Change</button>
</li>
))}
</ul>
)
const otherUserMemberSplit = members.map((item, idx, arr) => arr.filter(m => m.id !== item.id))
const otherUserWindows = (
<div style={{
width: '100vw',
display: 'flex'
}}>
{members.map((om, idx) => (
<div style={{
flex: '1 1 auto',
borderRight: '1px solid #000'
}} key={om.id}>
<h4 style={{
textDecoration: om.isOnline ? 'none' : 'line-through'
}}>【{om.name}】窗口</h4>
<ul>
{otherUserMemberSplit[idx].map(im => <li key={im.id}>
<FriendStatusWithCounter friend={im} />
</li>)}
</ul>
</div>
))}
</div>
)
return (
<>
<div>
<input type="text" ref={this.inputNameRef} />
<input type="button" onClick={this.handleJoinChat} value="加入" />
</div>
<hr />
<h4>管理员窗口</h4>
{adminWindow}
<hr />
<h4>其他用户窗口</h4>
{otherUserWindows}
</>
)
}
}
useState
、useEffect
Hooks用于在函数组件中勾入相当于类组件生命周期的功能,使函数组件具有了状态useEffect
函数用于处理组件中的副作用,副作用包括数据获取、DOM操作、定时器等,当然不仅限于此。它相当于 componentDidMount
, componentDidUpdate
和 componentWillUnmount
这三个函数的组合useEffect
实现分离关注点,抽离出细致的功能,封装成函数在适当地时机执行