本文详细解析JavaScript中三种观察者模式的实现方式。首先介绍基于事件总线(EventBus)的基础发布订阅机制,支持全局或局部使用;其次探讨利用Object.defineProperty劫持get/set属性实现自动监听的方法;最后分析基于Proxy.revocable的代理方案,重点讨论其取消监听及自动释放机制的局限性。
event-bus
event-bus是最基础的事件监听方式,属于通知类型。它可以配置为单例进行全局使用,也可以应用于局部场景。
该方式通过名称进行订阅监听,并通过名称发布新消息,使用过程相对简单。
export default class EventBus {
events: Record<string, Array<Function>> = {}
//订阅
subscribe(name: string, callback: Function) {
if (!this.events[name]) {
this.events[name] = []
}
this.events[name].push(callback)
}
//发布 -- data是往call中传递参数以便于使用的
publish(name: string, data?: any) {
let event = this.events[name]
if (event) {
event.forEach(callback => callback(data))
}
}
//取消订阅,y
unsubscribe(name: string, callback?: Function) {
let event = this.events[name]
if (!callback) {
this.events[name] = []
}else if (event) {
this.events[name] = event.filter(e => e !== callback)
}
}
}测试案例
const event = new EventBus()
const subscribe = () => {
event.subscribe('name', (value: any) => {
console.log(value)
})
console.log('已订阅')
}
const publish = () => {
let content = "我发布消息了,内容是\"哈啊哈哈\""
event.publish('name', content)
}
const unsubscribe = () => {
event.unsubscribe('name')
console.log('取消订阅了')
}自动销毁观察者(event-bus版)
此处根据 event-bus 的变化,参考 UI组件生命周期 的状态,实现 UI组件销毁时自动释放 的监听订阅案例。实际上,这仅增加了一个上下文对象,主要目的是为了方便统一销毁观察者。
///------自动释放组件----
//假设组件释放调用方法为onDestory
const __ComponentDestoryName = 'onDestory'
class EventObj {
key: string
fn: Function[] = []
constructor(key: string, fn: Function) {
this.key = key
this.fn.push(fn)
}
}
class ContextEventObj<T = EventObj> {
context: any
events: T[] = []
constructor(context: any, event: T) {
this.context = context
this.events.push(event)
}
//假设组件释放调用方法为destory
registerDestory(callback: Function) {
let desFunc = this.context[__ComponentDestoryName]
this.context[__ComponentDestoryName] = function() {
callback()
desFunc()
}
}
}
//根据上下文context自动释放版本(
class EventBusByAutoRelease {
events: ContextEventObj[] = []
//订阅,context为所属上下文,当上下文对象销毁时,响应监听随机销毁
subscribe(name: string, callback: Function, context: any = null) {
let contextObj = this.events.find(e => e.context === context)
if (contextObj) {
let event = contextObj.events.find(e => e.key === name)
event?.fn.push(callback)
}else {
let event = new EventObj(name, callback)
contextObj = new ContextEventObj(context, event)
contextObj.registerDestory(() => {
this.events = this.events.filter(e => e.context !== context)
})
this.events.push(contextObj)
}
}
//发布 -- data是往call中传递参数以便于使用的
publish(name: string, data?: any) {
this.events.forEach(item =>
item.events.forEach(e =>
e.key === name &&
e.fn.forEach(fn => fn(data))
)
)
}
//取消订阅
unsubscribe(context?: any, name?: string) {
if (context) {
let ctx = this.events.find(e => e.context === context)
if (!ctx) return
if (name) {
ctx.events = ctx.events.filter(e => e.key !== name)
}else {
this.events = this.events.filter(e => e.context !== context)
}
}else {
this.events.length = 0
}
}
}observer
该观察者模式通过 Object.defineProperty 方法,重新定义指定对象的 set、get 方法,从而实现自动监听的效果。具体实现参考了其他平台的部分案例(实际实现可以更加简洁)。
ps:开发者也可以尝试编写更好用的版本,本部分主要应用 Object.defineProperty。
const symbol_observe = '__obs_map'
const canObserve = (obj: any) => obj !== null && typeof obj === 'object'
class ObserveObj {
target: any
key: string
fn: Function[] = []
val: any
constructor(target: any, key: string, fn: Function, val: any) {
this.target = target
this.key = key
this.fn.push(fn)
this.val = val
this.defineProperty()
}
defineProperty() {
let that = this
Object.defineProperty(that.target, that.key, {
get() {
return that.val
},
set(newVal) {
that.notify(newVal)
},
enumerable: true, // 可枚举
})
}
notify(val: any) {
this.val = val
this.fn.forEach(e => e(val))
}
removeObserver() {
delete this.target[this.key]
this.target = null
}
}
class TargetObs {
target: any
observes: Array<Record<'key' | 'fn', string | Function>> = []
constructor(target: any) {
this.target = target
}
}
export default class Observer {
targets: TargetObs[] = [] //用于保存target和callback信息,以便于后续清理
observe(target: any, key: string, callback: Function) {
if (!canObserve(target)) return
let targetObs = this.targets.find(e => e.target === target)
if (!targetObs) {
targetObs = new TargetObs(target)
targetObs.observes.push({
key,
fn: callback
})
this.targets.push(targetObs)
}
if (!target[symbol_observe]) {
target[symbol_observe] = new Map<string, ObserveObj>()
}
let observeMap: Map<string, ObserveObj> = target[symbol_observe]
let observeObj = observeMap.get(key)
if (observeObj) {
observeObj.fn.push(callback)
}else {
observeObj = new ObserveObj(target, key, callback, target[key])
observeMap.set(key, observeObj)
}
}
//清理某个target部分监听
unobserve(target: any, key: string, callback?: Function) {
if (!canObserve(target)) return
let observeMap: Map<string, ObserveObj> = target[symbol_observe]
if (!observeMap) return
let obj = observeMap.get(key)
if (!obj) return
let targetObs = this.targets.find(e => e.target = target)
if (callback) {
if (targetObs) {
targetObs.observes = targetObs.observes.filter(e => e.fn !== callback)
}
obj.fn = obj.fn.filter(e => e !== callback)
}else {
let fns
if (targetObs) {
fns = targetObs.observes.filter(e => e.key === key)
}
fns?.forEach(item => {
obj!.fn = obj!.fn.filter(e => e !== item.fn)
})
}
if (obj.fn.length < 1) {
obj.removeObserver()
observeMap.delete(key)
}
}
//清理本观察者添加的指定监听
unobserveTarget(target: any) {
if (!canObserve(target)) return
let observeMap: Map<string, ObserveObj> = target[symbol_observe]
if (!observeMap) return
let targetObs = this.targets.find(e => e.target = target)
for (let key in observeMap) {
let fns
if (targetObs) {
fns = targetObs.observes.filter(e => e.key === key)
}
let obj = observeMap.get(key)
if (!obj) continue
fns?.forEach(item => {
obj!.fn = obj!.fn.filter(e => e !== item.fn)
})
if (obj.fn.length < 1) {
obj.removeObserver()
observeMap.delete(key)
}
}
}
//清理本观察者添加的指定监听
unobserveAll(target?: any) {
if (target) {
this.unobserveTarget(target)
}else {
this.targets.forEach(e => {
this.unobserveTarget(e.target)
})
this.targets.length = 0
}
}
}测试案例
const event = new Observer()
const subscribe = () => {
event.observe(dataModel, 'name', (value: any) => {
console.log(value)
})
console.log('已订阅')
}
const publish = () => {
let content = "我更改内容了,内容是\"哈啊哈哈\""
dataModel.name = content
}
const unsubscribe = () => {
event.unobserve(dataModel, 'name')
console.log('取消观察了')
}observer-proxy
本观察案例通过 Proxy 代理实现,使用了其中的 Proxy.revocable,参考 Proxy 基础文档,支持取消监听功能。
唯一的缺陷是更改方法需要使用新的 Proxy 对象来替代之前的对象,否则无法实现监听功能。实际使用体验不够友好,此处已尽量优化使其更加可用。
ps:实际上,Proxy 在其他特殊场景中才能发挥其巨大优势,此处仅展示一种实现方案,若有更好的方案欢迎讨论。
class ObserveObj {
observer: string
key: string
fn: Function[] = []
constructor(key: string, observer: any, fn: Function) {
this.key = key
this.observer = observer
this.fn.push(fn)
}
}
export default class ObserverProxy<T extends Object> {
target: any //用于回退原来对象 ori = proxy.target
o: T
rk: Function //用于取消代理
//存放回调的数组,一个对象的监听一般不会过多(例如成百上千),因为他存在性能可以考虑调整
observers: ObserveObj[] = []
//如果根据observer自动释放,可以重写observer的销毁属性,在哪里自动释放即可,由于很多组件的销毁方法不一样就不实现了
constructor(target: T) {
this.target = target
let that = this
const {proxy, revoke} = Proxy.revocable(target, {
//注意里面的 this 都会指向 target
get: function (target, propKey, receiver) {
return Reflect.get(target, propKey, receiver);
},
//这里拦截set方法即可
set: function (target: any, propKey: string, value: any, receiver: any) {
let observers = that.observers.filter(e => e.key === propKey)
observers.forEach(e => e.fn.forEach(fn => fn(value)))
//调动原来的set方法赋值
return Reflect.set(target, propKey, value, receiver);
},
})
this.o = proxy
this.rk = revoke
}
revoke() {
this.rk()
this.removeObserve()
let target = this.target
this.target = null
return target
}
//callback回调,context观察者(参与观察的对象,即回调所在的类),responseByFirst初次是否响应回调
addObserve(key: string, callback: Function, observer: any, responseByFirst = false) {
let observerObj = this.observers.find(
e => e.observer === observer && e.key === key
)
if (!observerObj) {
observerObj = new ObserveObj(key, observer, callback)
this.observers.push(observerObj)
}else {
observerObj.fn.push(callback)
}
responseByFirst && callback(this.target[key])
}
removeObserve(observer?: any, key?: string) {
if (observer) {
if (key) {
this.observers = this.observers.filter(e => e.observer !== observer || e.key !== key)
}else {
this.observers = this.observers.filter(e => e.observer !== observer)
}
}else {
this.observers.length = 0
}
}
}测试案例
const dataModel = new ObserverProxy(new DataModel())
const subscribe = () => {
dataModel.addObserve('name', (value: any) => {
console.log(value)
}, this)
console.log('已订阅')
}
const publish = () => {
let content = "我更改内容了,内容是\"哈啊哈哈\""
//由于要用代理修改,使用.ob代替原对象即可
dataModel.o.name = content
}
const unsubscribe = () => {
//撤销监听后,返回原对象,注意原对象没有监听
let res = dataModel.revoke()
console.log('取消观察了')
}最后
最后两个方案原本试图实现自动释放。由于部分平台存在引用计数机制(不仅限于垃圾回收),引用关系可能导致内存泄露问题。
由于 JavaScript 对象没有析构函数,即使引入 WeakMap 也无济于事,因为 WeakMap 不可遍历。因此,目前实现自动释放的监听对我来说仍有困难(当然这难不住技术大佬)。这也是后续需要解决的难题,欢迎大家提供策略共同探讨,以提升整体水平。
以上就是JavaScript三种观察者实现案例分享的详细内容,更多关于JavaScript观察者实现的资料请关注本站其它相关文章!