React中组件通信02——消息订阅与发布、取消订阅以及卸载组件时取消订阅
1. 前言
1.1 使用props通信
- 上一篇介绍了使用props进行组件之间的通信,但是对于子传父和兄弟之间的通信使用props不是最好的选择,所以介绍一下消息订阅与发布。
- 关于props,可以看下面的文章,如下:
React中组件通信01——props.
1.2 关于useEffect
- 下面写订阅消息的时候会用到,所以这里简单介绍一下:
- useEffect 的使用,可以相当于class组件中的生命周期,可以代替组件挂载完毕(componentDidMount)、组件更新完毕(componentDidUpdate)、组件将要卸载这三个钩子(componentWillUnmount)。
- 1⃣️useEffect 的第二个参数是
空数组
的情况,第一个参数里的函数就相当于是class组件中的组件挂载完毕钩子(componentDidMount)。
这时可以做定时器、订阅消息等。 - 2⃣️useEffect 的第二个参数如果
不传 或者 是非空数组
,此时第一个参数相当于组件更新完毕的钩子(componentDidUpdate)。- 不传:监测组件中state里的每个属性,只要有更新就会调用;
- 非空数组:可以指定监测state里的某一个或某几个属性,只有监测的属性有更新才会调用。
- 3⃣️useEffect 第一个参数可以没有返回值,但是如果有返回函数,则这个返回函数相当于class组件中的组件将要卸载的钩子(componentWillUnmount)。
一般用于取消定时器、取消订阅等。
- 1⃣️useEffect 的第二个参数是
2. 安装 pubsub-js
- 命令如下:
npm install pubsub-js
3. 消息订阅与发布
3.1 简单例子-1
- 小需求设计:
- 代码设计实现
- ChildA——发布消息
核心代码就2行:import PubSub from 'pubsub-js' //PubSub.publish('MY TOPIC', 'hello world!'); PubSub.publish('GamesNews','通知:在我校(ChildA)举办运动会的时间定于10月16日');
- ChildB——订阅消息(借助于
useEffect
钩子),核心代码如下:import PubSub from 'pubsub-js' //订阅运动会消息 const subscriberGamesNew = function(msg, data){ console.log('ChildB 订阅的消息--subscriberGamesNew---'); console.log( msg, data );//msg-订阅的topic data-消息内容 } useEffect(()=>{ //subscribe-订阅的方法 'GamesNews'-订阅的主题 let token = PubSub.subscribe('GamesNews', subscriberGamesNew); console.log('token',token); },[])//这里第二个参数是空数组 [],这种情况相当于class组件中 "组件挂载完毕的钩子"
- ChildA——发布消息
- 效果展示
3.2 简单例子-2(完善、优化)——订阅消息+使用消息
- 小需求如下:
- 代码设计如下:
- 发布方
- 订阅方
- 发布方
- 效果如下:
4. 取消订阅
4.1 取消单个topic
-
语法:
const token1 = PubSub.subscribe('GamesNews', mySubscribers);//订阅 GamesNews //取消订阅 PubSub.unsubscribe(token1); 或 PubSub.unsubscribe('GamesNews');
-
例子如下:
-
效果如下:
-
上面函数可以优化一下,根据主题取消,如下:
const myUnsubscribe =(topic)=>{ PubSub.unsubscribe(topic); //取消订阅的GamesNews console.log('取消订阅运动会消息---成功!!!'); } <button onClick={ ()=>myUnsubscribe('GamesNews')}>取消订阅运动会消息</button>
4.2 取消多个或更多语法
4.3 卸载组件时取消订阅
4.3.1 卸载所有组件
- 首先,给出卸载所有组件的代码,如下:
核心代码就这一丢丢,不说了,自己看import root from '../index'; //卸载组件---卸载所有 const unMountAll =()=>{ //卸载组件 root root.unmount(); } <button onClick={ unMountAll}>卸载所有组件</button>
- 然后,再看效果:
- 最后,简单说明一下:
- 卸载组件,在class组件的声明周期中,其实是调用了组件将要卸载的钩子,函数组件中可以在useEffect中体现,详细看上面1.2的介绍《1.2 关于useEffect》。
- 其实在上面卸载时,如果有代码有开启定时器而又没有取消定时器的话,是有问题的,其实也可以这么说,如果上面只卸载了B组件,但是B组件有有订阅消息,那么如果卸载了B组件,但是订阅没有取消的话,是不合理的,这也可以理解是前端优化的部分。
- 光说看不出效果,下面我门就介绍卸载指定组件B来观察观察。
4.3.2 卸载指定组件——取消订阅
- 1⃣️ 在父组件中,控制B组件展示(即:父组件中,设计一个
卸载/渲染
B组件的按钮)
- 2⃣️ 在B组件 useEffect 中第一参数的返回函数中,有所体现,看卸载B组件时,是否执行返回函数
- 3⃣️ 看效果
- 优化,看效果:
所以,需要在卸载组件时,把订阅取消了,取消订阅的代码所放位置,如下:
5. 附核心完整代码
- 代码结构:
- 核心代码
- App.js + index.js
- Parent.jsx
import React from "react"; import ChildA from "./ChildA"; import ChildB from "./ChildB"; import './index.css' import root from '../index'; function Parent() { const [mountChildBFlag,setMountChildFlag] = React.useState(true); //卸载组件---卸载所有 const unMountAll =()=>{ //卸载组件 root root.unmount(); } return( <div className="parent"> 我是父组件! <div className="childA"> <ChildA notice={ '通知——今天放假!'}/> </div> { /* <div className="childB"> <ChildB notice={'通知——今天放假!'} /> </div> */} { /* 这里根据 mountChildBFlag 控制B组件的状态 */} { mountChildBFlag ? <div className="childB"> <ChildB notice={ '通知——今天放假!'} /> </div> : "" } <br /><br /> <button onClick={ ()=>setMountChildFlag(!mountChildBFlag)}>卸载B组件/渲染B组件</button> <br /><br /> <button onClick={ unMountAll}>卸载所有组件</button> </div> ) } export default Parent;
- ChildA.jsx
import React from "react"; import PubSub from 'pubsub-js' function ChildA(props){ const stuNameRef = React.useRef(); //发布运动会消息 按钮触发 function publishGamesNews(){ // 发布运动会消息 topic-->GamesNews PubSub.publish('GamesNews','通知:在我校(ChildA)举办运动会的时间定于10月16日'); console.log('-----ChildA 发布了 GamesNews 消息----'); } // 发布学生消息 开除的学生 function expelStuSubmit(event){ event.preventDefault();//非受控组件 只取表单数据,但阻止表单提交,实现页面无刷新 const stuName = stuNameRef.current.value; PubSub.publish('stusInfo',{ stuName:stuName,schoolName:'ChildA-School',stuState:'被开除'}); } return( <div> 我是子组件A!!! <br /><br /> 收到来自于父组件的数据:{ props.notice} <br /><br /> <button onClick={ publishGamesNews}>发布运动会消息</button> <br /><br /> <form onSubmit={ expelStuSubmit}> 学生姓名:<input type="text" ref={ stuNameRef} name="stuName"/> <button>开除学生</button> </form> </div> ) } export default ChildA;
- ChildB.jsx
import PubSub from 'pubsub-js' import { useEffect,useState } from 'react'; function ChildB(props){ const [gamesNews,setGamesNews] = useState('等通知……'); const [stusInfo,setStuInfo] = useState( [ { stuName:'学生1',schoolName:'ChildA-School',stuState:'在校'}, { stuName:'学生2',schoolName:'ABC学校',stuState:'离校'}, { stuName:'张三',schoolName:'ChildA-School',stuState:'在校'}, { stuName:'李四',schoolName:'XXX附属中学',stuState:'托管'}, { stuName:'王五',schoolName:'ChildA-School',stuState:'在校'}, ] ); //我的订阅方法 const mySubscribers = function(msg, data){ console.log('ChildB 订阅的消息--mySubscribers---'); // console.log( msg, data );//msg-订阅的topic data-消息内容 //将订阅到的新消息进行更新 if('GamesNews'===msg){ console.log('订阅到运动会的消息是:',data); setGamesNews(data); }else if('stusInfo'===msg){ console.log('订阅到开除的学生是:',data); // const newStuInfo = [...stusInfo,data];//这个不去重,追加数据 //这个地方需要注意stusInfo 和 data的类型 const newStuInfo = stusInfo.map((stu)=>{ return data.stuName===stu.stuName ? data : stu; }); setStuInfo(newStuInfo); } } useEffect(()=>{ //subscribe-订阅的方法 const token1 = PubSub.subscribe('GamesNews', mySubscribers);//订阅 GamesNews const token2 = PubSub.subscribe('stusInfo', mySubscribers);// 订阅 stusInfo console.log('token1---',token1); console.log('token2---',token2); return ()=>{ //这个返回函数,相当于class中的“组件将要卸载的钩子” 在这里可以取消订阅 console.log('ChildB组件...被卸载了'); PubSub.unsubscribe(token1); //取消订阅 } },[])//这里第二个参数是空数组 [],这种情况相当于class组件中 "组件挂载完毕的钩子" //取消订阅 const myUnsubscribe =(topic)=>{ PubSub.unsubscribe(topic); //取消订阅的GamesNews console.log('取消订阅运动会消息---成功!!!'); } return( <div> 我是子组件B!!! <br /><br /> 收到来自于父组件的数据:{ props.notice} <br /><br /><br /> 订阅1——运动会消息:{ gamesNews} <br /> <button onClick={ ()=>myUnsubscribe('GamesNews')}>取消订阅运动会消息</button> <br /><br /><br /> 订阅2——学生消息: <table> <thead> <tr> <th>学生姓名</th> <th>所在学校</th> <th>状态</th> </tr> </thead> <tbody> { stusInfo.map((stu,index)=>{ return <tr key={ index}> <td>{ stu.stuName}</td> <td>{ stu.schoolName}</td> <td>{ stu.stuState}</td> </tr> })} </tbody> </table> </div> ) } export default ChildB;
- component–>index.css
.parent{ background-color: blueviolet; border: 1px solid; height: 900px; width: 600px; text-align: center; } .childA{ background-color: green; height: 170px; margin: 20px; } .childB{ background-color: grey; height: 400px; margin: 20px; }
- App.js + index.js