每天学点React - 消息订阅与发布模式

250 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24 天,点击查看活动详情

消息订阅与发布模式

在上一篇《每天学点React - 通用功能界面的组件编码流程(二)》中,我们使用到了几个组件,但是组件间到通信都得由其关联都父组件来协助传递,兄弟组件间并没有直接通信,那么这个问题能不能解决呢?

pubsub.js

简介

PubSubJS是一个用JavaScript编写的基于主题的发布/订阅库。其具有同步解耦,因此主题是异步发布的。这有助于保持程序的可预测性,因为在消费者处理主题时,不会阻止主题的发起人。

PubSubJS还支持同步主题发布。这可能会在某些环境(浏览器,而不是所有环境)中加快速度,但也可能会导致一些非常难以推理的程序,其中一个主题会触发同一执行链中另一个主题的发布。

PubSubJS被设计为在单个进程中使用,对于多进程应用程序(如Node.js–具有多个子进程的集群)来说不是一个很好的候选。如果您的Node.js应用程序是一个单进程应用程序,你很好。如果它是(或将是)一个多进程应用程序,那么你最好使用redis Pub/Sub或类似程序

项目整合

地址github.com/mroderick/P…

安装命令(mac版本)_{(mac版本)}

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>
    )
  }
}