JavaScript中三种观察者实现案例分享

 更新时间:2023年08月08日 08:39:40   作者:剪刀石头布啊  
前面突然看到 Object.defineProperty,就顺道想到 Proxy,然后就想到了观察者案例,这边还没有用 javascript编写一个观察者的案例呢,顺道加入了一个 event-bus 监听事件案例,凑一起看一看不同的实现方式,需要的朋友可以参考下

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('取消观察了')
}

最后

最后两个本来想实现自动释放,由于一些平台存在引用计数(不只是垃圾回收),引用会存在内存泄露问题,

由于 js 对象没有析构函数,这里即使加上 WeakMap 也不行,因为它不可遍历,所以目前对于我来说实现自动释放的监听尚有困难(当然难不住大佬),当然这也是后续要解决的难题,大家要是有策略可以探讨,大家一起提升😂

以上就是JavaScript三种观察者实现案例分享的详细内容,更多关于JavaScript观察者实现的资料请关注脚本之家其它相关文章!

相关文章

  •  javascript数组中的lastIndexOf方法

     javascript数组中的lastIndexOf方法

    这篇文章主要介绍了 javascript数组中的lastIndexOf方法,该方法可返回一个指定的字符串值最后出现的位置,在一个字符串中的指定位置从后向前搜索,下文详细内容需要的小伙伴可以参考一下
    2022-03-03
  • 微信小程序实现点赞业务

    微信小程序实现点赞业务

    这篇文章主要为大家详细介绍了微信小程序实现点赞业务,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-02-02
  • 网页从弹窗页面单选框传值至父页面代码分享

    网页从弹窗页面单选框传值至父页面代码分享

    最近有项目需求,需要在加入新机构的时候,需要选择它的上级机构,下面把代码整理,分享给大家,需要的朋友可以参考下
    2015-09-09
  • JavaScript中EventLoop介绍

    JavaScript中EventLoop介绍

    本篇文章给大家详细介绍了JavaScript中EventLoop的相关知识,有这方面需要的朋友参考学习下。
    2018-01-01
  • 浅谈ES6新增的数组方法和对象

    浅谈ES6新增的数组方法和对象

    下面小编就为大家带来一篇浅谈ES6新增的数组方法和对象。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • 微信小程序开发之左右分栏效果的实例代码

    微信小程序开发之左右分栏效果的实例代码

    本文以一个简单的小例子,简述在微信小程序开发中左右分栏功能的实现方式,主要涉及scroll-view ,列表数据绑定,及简单样式等内容,感兴趣的朋友跟随小编一起看看吧
    2019-05-05
  • element-ui 时间选择器限制范围的实现(随动)

    element-ui 时间选择器限制范围的实现(随动)

    这篇文章主要介绍了element-ui 时间选择器限制范围(随动),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • 10个经典的网页鼠标特效代码

    10个经典的网页鼠标特效代码

    小编为广大读者们整理了10个经典的网页鼠标特效代码,并对代码进行了编译和解释,需要的朋友收藏下吧。
    2018-01-01
  • 一文详解JS中的事件循环机制

    一文详解JS中的事件循环机制

    JavaScript是单线程的编程语言,只能同一时间内做一件事。但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行。这是因为JS有事件循环机制,本文就为大家详细讲解一下这一机制,需要的可以参考一下
    2022-03-03
  • JavaScript中如何判断一个值的类型

    JavaScript中如何判断一个值的类型

    在js中有一个运算符可以帮助我们判断一个值的类型,它就是typeof运算符。下面通过本文给大家分享JavaScript中如何判断一个值的类型,需要的朋友参考下吧
    2017-09-09

最新评论