什么是发布订阅模式?
发布订阅模式定义对象间的一种一对多的依赖关系,当目标对象
Subject的状态发生改变时,所有依赖于它的对象Observer都将得到通知。JS中大多使用事件模型来模拟这个动作。
举个🍐子
这周末去看了心仪很久的一款车-蔚来ES6,去了上海金茂大厦的蔚来空间试驾了下,销售人员很热情,让我填写了一些表格就可以去试驾了,推背感很强烈,自动泊车爱了爱了❤️,试驾当场我就准备大定,可是蔚来客服说由于南京疫情导致芯片短缺导致库存不足,回到家几天以后一个蔚来的销售打电话跟我说有库存了......
这是一个平时生活中很常见的例子,却是一个很典型的发布订阅模式。
JS简单实现
上节的🌰子我们说到是发布订阅模式,如果你通过阅读上节点文字很直截了当的看出了谁是目标对象,谁是订阅者,是谁发布者,我相信你已经明白了什么是发布订阅。这节我们用简单的JS代码来实现这个例子。
- 目标对象Subject是蔚来ES6的库存,因为他的库存变化会通知订阅他的消费者。
- 依赖对象Observer是准备购买蔚来汽车的消费者,当库存发生变化时会通知依赖对象。
1.创建蔚来销售处对象
我们在这里需要创建一个蔚来销售处对象,因为我们订阅购买信息需要在销售处登记,库存的变化也是要蔚来才知道。
let Sale=function(){
//存放订阅的列表(用户登记的信息)
this.list=[];
}
2.增加订阅的方法
Sale.prototype.add=function(listener){
//将用户登记的信息存储起来
this.list.push(listener)
}
3.增加发布的方法
因为用户库存变化并不是很好模拟,所以我们增加发布的方法,方便手动发布信息
Sale.prototype.trigger=function(){
this.list.forEach(listener=>{
listener()
})
}
4.使用方法
//完整代码
let Sale=function(){
//存放订阅的列表(用户登记的信息)
this.list=[];
}
Sale.prototype.add=function(listener){
//将用户登记的信息存储起来
this.list.push(listener)
}
Sale.prototype.trigger=function(){
this.list.forEach(listener=>{
listener()
})
}
//使用方法
let NioSale=new Sale();
//订阅消息
NioSale.add(()=>{console.log("我是意向购买者1,有消息请第一时间通知我")})
NioSale.add(()=>{console.log("我是意向购买者2,有消息请第一时间通知我")})
NioSale.add(()=>{console.log("我是意向购买者3,有消息请第一时间通知我")})
setTimeout(()=>{
NioSale.trigger();
},10000)
是的,简单的发布订阅模式去除注释只有10行。
5.思考题
其实一个真正的发布订阅还需要有懒注册功能、取消订阅、指定发布订阅等功能,希望你可以自己实现。
实现useBreakPoint hook
本文的重点其实是实现useBreakpoint这个hook,那么这个hook的作用是什么呢?antd useBreakpoint是Grid组件内部使用的hook。主要是判断屏幕,可以给予正确的代表指定字符串的标识,但是我们这次要实现的略微和他的不同,我们可以精确匹配对应的屏幕标识。
1.matchMedia API
在传统监听浏览器窗口变化中,我们大多使用resize事件,但是由于resize触发频率过高,容易造成资源的严重浪费,我们现在可以使用matchMedia API,该方法返回一个新的MediaQueryList,这个方法有一个addListener方法当匹配的媒体查询变化时会产生一个回调,其中含有一个matches属性可以判断是否匹配成功。
let mql=window.matchMedia('(max-width:800px)');
//当窗口大小超过800px 会打印‘我大于800px’ 否则打印‘我小于800px’
mql.addListener(({matches})=>{console.log(`我${matches?"小于":"大于"}800px`)})
2.确定媒体查询大小
const responsiveMap={
xs:'(max-width: 575px)',
sm:'(min-width: 576px) and (max-width:767px)',
md:'(min-width: 768px) and (max-width:991px)',
lg:'(min-width: 992px) and (max-width:1119px)',
xl:'(min-width: 1200px) and (max-width:1599px)',
xxl:'(min-width: 1600px)'
};
- 0px < xs <= 575px
- 576px <= sm <= 767px
- 768px <= md <= 991px
- 992px <= lg <= 1199px
- 1200px <= xl <= 1599px
- xxl >= 1600px
3.发布订阅模式三部曲
1.创造media对象
//目标对象 会产生变化
let screens={};
//缓存列表
let subscribers=[];
const responsiveObserve={
}
2.增加订阅的方法
const responsiveObserve={
subscribe(func){
subscribers.push(func);
}
}
3.增加发布的方法
const responsiveObserve={
dispatch(pointMap){
screens=pointMap;
subscribers.forEach(func=>func(screens));
}
}
4.自定义hook引用此对象
import React ,{ useState,useEffect } from 'react';
//responsiveObserve为上文中对象
import ResponsiveObserve from './responsiveObserve';
function useBreakpoint(){
const [screens,setScreens]=useState({});
useEffect(()=>{
const token=ResponsiveObserve.subscribe(supportScreens=>{
setScreens(supportScreens);
})
return ()=>ResponsiveObserve.unsubscribe(token);
},[]);
return screens;
}
export default useBreakpoint;
由上可知该hook在useEffect中订阅了该media对象,而且返回了一个token便于在组件写在的时候清除订阅的方法。
5.增加取消订阅的方法
- 首先需要在订阅的时候将指定的id与方法形成一一映射关系,并返回id。
- 在取消订阅的时候通过map结构delete方法直接删除map中的键值对。
let subUid=-1;
let subscribers=new Map();
const responsiveObserve={
subscribe(func){
subUid+=1;
subscribers.set(subUid,func);
func(screens);
return subUid;
},
unsubscribe(token){
subscribers.delete(token);
},
}
6.创建不同的media对象(核心)
创建六个不同的媒体查询对象,然后就可以监听得出合适的匹配对象,由于我们这里是精确匹配,所以我们只需要匹配相对应的matches为true即可,即同时只有一个满足条件。
Object.keys(responsiveMap).forEach((screen:string)=>{
const matchMediaQuery=responsiveMap[screen as Breakpoint];
const listener=({matches}:{matches:boolean})=>{
if(matches){
this.dispatch({
...NoMatchScreens,
[screen]:matches
})
}
}
const mql=window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery]={
mql,
listener
};
listener(mql);
})
7.懒处理
设想一下,如果我们不进行订阅该对象,是不是就不需要进行创建media对象。所以这里我们进行了懒处理的方式,即只在有订阅方法产生时才创建media对象,避免性能损耗。
const responsiveObserve={
subscribe(func:SubscribeFunc):number{
//当有订阅产生 才进行注册
if(!subscribers.size) this.register();
...
},
unsubscribe(token:number){
//如果没有订阅 取消注册
if(!subscribers.size) this.unregister();
},
unregister() {
//移除监听对象s
handler?.mql.removeListener(handler?.listener);
//主要是清除订阅
subscribers.clear();
},
register(){
//...核心方法
}
}
发布到npm上
-
代码已经发布到git上并且已经发包了
//使用方法
npm install @parrotjs/rc-hook-usebreakpoint -S
import React from "react";
import useBreakpoint from "@parrotjs/rc-hook-usebreakpoint";
const Demo = () => {
const screens=useBreakpoint();
console.log("screens",screens);
//当窗口匹配时会变化 {lg: false,md: true,sm: false,xl: false,xs: false,xxl: false}
return (
<></>
)
}
export default Demo;