持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24 天,点击查看活动详情
消息订阅与发布模式
在上一篇《每天学点React - 通用功能界面的组件编码流程(二)》中,我们使用到了几个组件,但是组件间到通信都得由其关联都父组件来协助传递,兄弟组件间并没有直接通信,那么这个问题能不能解决呢?
pubsub.js
简介
PubSubJS是一个用JavaScript编写的基于主题的发布/订阅库。其具有同步解耦,因此主题是异步发布的。这有助于保持程序的可预测性,因为在消费者处理主题时,不会阻止主题的发起人。
PubSubJS还支持同步主题发布。这可能会在某些环境(浏览器,而不是所有环境)中加快速度,但也可能会导致一些非常难以推理的程序,其中一个主题会触发同一执行链中另一个主题的发布。
PubSubJS被设计为在单个进程中使用,对于多进程应用程序(如Node.js–具有多个子进程的集群)来说不是一个很好的候选。如果您的Node.js应用程序是一个单进程应用程序,你很好。如果它是(或将是)一个多进程应用程序,那么你最好使用redis Pub/Sub或类似程序
项目整合
安装命令
sudo npm add pubsub-js
引入方式
// ES6
import PubSub from 'pubsub-js'
// CommonJS
const PubSub = require('pubsub-js')
基础用法
发布消息
// PubSub.publish(发布的主题, 发布的消息);
PubSub.publish('MY TOPIC', 'hello world!');
订阅消息
// PubSub.subscribe(订阅的主题, 接收到订阅后执行的函数);
var token = PubSub.subscribe('MY TOPIC', mySubscriber);
var mySubscriber = function(msg, data) {
console.log(msg, data);
}
取消订阅
PubSub.unsubscribe(token);
TodoList改造
App
首先,我们把App中的state等相关状态与函数移除到对应的子组件中。
import { Component } from 'react'
import Header from './components/Header'
import Body from './components/Body'
import Footer from './components/Footer'
class App extends Component {
render() {
return (
<div id="root">
<div className="todo-container">
<div className="todo-wrap">
<Header/>
<Body/>
<Footer/>
</div>
</div>
</div>
)
}
}
export default App
Header
在该组件中,我们不需要订阅消息,只需要将每次新增的todo发送给Body组件即可,那么我们可以定义一个Header主题,将获取到的文本框中的值转化成一个todo对象,发布出去。()
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import { nanoid } from 'nanoid'
import './index.css'
export default class Header extends Component {
// 按钮弹起事件
handleKeyUp = (event) => {
// 得到按键Code和目标值
const { keyCode, target } = event
if (target.value.trim() === '') {
alert('输入内容不能为空!')
return
}
// 判断是否回车
if (keyCode === 13) {
// 发布消息
const todo = {
id: nanoid(),
name: target.value,
done: false
}
target.value = ''
// 发送 object 类型消息
PubSub.publish('Header', todo)
}
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入任务名称,按回车键确认" />
</div>
)
}
}
Body
我们将state定义在该组件中,由其来与Header组件和Footer组件进行交互,同时传递todo给子组件Item。()
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import Item from '../Item'
import './index.css'
export default class Body extends Component {
state = {
todos: [
{ id: 1, name: 'eat', done: true },
{ id: 2, name: 'sleep', done: false },
{ id: 3, name: 'code', done: false },
{ id: 4, name: 'juejin', done: true },
]
}
// 更改todo状态
updateTodo = (id, done) => {
const { todos } = this.state
const newTodos = todos.map(todo => {
if (todo.id === id) {
return { ...todo, done }
} else {
return todo
}
})
this.setState({
todos: newTodos
})
PubSub.publish('Body', newTodos)
}
// 删除todo
deleteTodo = (id) => {
const { todos } = this.state
const newTodos = todos.filter(todo => todo.id !== id)
this.setState({
todos: newTodos
})
PubSub.publish('Body', newTodos)
}
componentDidMount() {
// 在组件挂载完毕后订阅主题
this.headerToken = PubSub.subscribe('Header', (msg, todo) => {
const { todos } = this.state
// 接收object类型,直接添加到数组头部
this.setState({ todos: [todo, ...todos] })
})
this.footerToken = PubSub.subscribe('Footer', (msg, footer) => {
const { check:done, click } = footer
const {todos} = this.state
if(typeof(done) === 'boolean'){
// 接收boolean类型,修改数组元素对应对属性
const newTodos = todos.map(todo => {
return {...todo, done}
})
this.setState({todos: newTodos})
PubSub.publish('Body', newTodos)
}else if(typeof(click) === 'string'){
// 接收string类型类型
const newTodos = todos.filter(todo => !todo.done)
this.setState({todos: newTodos})
PubSub.publish('Body', newTodos)
}
})
}
componentWillUnmount() {
// 在组件销毁时取消订阅
PubSub.unsubscribe(this.headerToken)
PubSub.unsubscribe(this.footerToken)
}
render() {
const { todos } = this.state
return (
<ul className="todo-main">
{
todos.map(todo => {
return <Item {...todo} key={todo.id} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo} />
})
}
</ul>
)
}
}
Item 该组件不做改动
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
state = ({ mouse: false })
// 鼠标移动状态控制
handlerMouse = (flag) => {
return () => {
this.setState({ mouse: flag })
}
}
// 复选框回调
handleCheck = (id) => {
return (event) => {
this.props.updateTodo(id, event.target.checked)
}
}
// 删除按钮回调
handleClick = (id) => {
// confirm不能直接写,需要使用window来调用
if(window.confirm('确认删除该任务吗?')){
this.props.deleteTodo(id)
}
}
render() {
const { id, name, done } = this.props
const { mouse } = this.state
return (
<li style={{ backgroundColor: mouse ? '#ddd' : 'white' }} onMouseEnter={this.handlerMouse(true)} onMouseLeave={this.handlerMouse(false)} >
<label>
<input onChange={this.handleCheck(id)} type="checkbox" checked={done} />
<span>{name}</span>
</label>
<button className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }} onClick={() => this.handleClick(id)}>删除</button>
</li>
)
}
}
Footer
该组件需要将原先修改对操作改成通知,具体实现如下:
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import './index.css'
export default class Footer extends Component {
state = {doneCount: 2, total: 4}
// 全选按钮回调
handleCheck = (event) => {
// 发送 boolean 类型消息
PubSub.publish('Footer', {check: event.target.checked})
}
// 全部删除按钮回调
handlerClick = () => {
// 发送 string 类型消息
PubSub.publish('Footer', {click: 'delete'})
}
componentDidMount() {
// 在组件挂载完毕后订阅 Header 主题的消息
this.bodyToken = PubSub.subscribe('Body', (msg, todos) => {
const doneCount = todos.reduce((pre, todo) => { return todo.done ? pre + 1 : pre }, 0)
const total = todos.length
this.setState({doneCount: doneCount, total: total})
})
}
componentWillUnmount() {
// 在组件销毁时取消订阅
PubSub.unsubscribe(this.bodyToken)
}
render() {
const {doneCount, total} = this.state
// 计算已完成数量
return (
<div className="todo-footer">
<label>
<input type="checkbox" onChange={this.handleCheck} checked={doneCount === total && doneCount !== 0} />
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button className="btn btn-danger" onClick={this.handlerClick}>清除已完成任务</button>
</div>
)
}
}