10分钟,手把手带你用typescript实现一个发布订阅模式,完成测试用例。

1,922 阅读4分钟
  • 先来看我们要实现的功能
    • 订阅,eventHub.$on(方法名,function(接收数据的参数){用该组件的数据接收传递过来的数据})
    • 发布,eventHub.$emit(方法名,传递的数据)
    • 取消发布,eventHub.$off(方法名,函数名称)取消订阅,要给函数一个名字,不能为匿名函数
  • 使用console.assert 完成测试用例
let eventHub = new Vue()

// 订阅
eventHub.$on('eventName', (data) => {
    console.log('😍' + data)
})

// 发布
eventHub.$emit('eventName', 'cats') // '😍 cats'

取消发布,在发布之前调用,不能取消匿名函数。

let eventHub = new Vue()
let fn = (data) => {
    console.log('😍' + data)
}
// 订阅
eventHub.$on('eventName', fn)

// 取消发布
eventHub.$off('eventName', fn)

// 发布
eventHub.$emit('eventName', 'cats') // 没有结果

先来实现第一版eventHub

  • 文件结构

  •    eventHub/
        src/
            index.ts
        test/
            index.ts  放测试代码
    
    
  • 将函数存在一个对象中,命名为cache,格式为, js { 'xxx': [fn1, fn2], 'xxx2': [fn1, fn2], }

    • 写数组的原因:
      • 一个事件名,可能会传入多个函数,调用多次
  • on

    • 如果传进新的函数名,初始化
    • 将函数存入cache中,之后可以从off调用函数
  • off

    • 根据传递的函数名,从cache中调用函数
class EventHub {
    cache = {}
    // ts 语法 === js的 this.cache={}
    // 对象的属性
    on(eventName, fn) {
        // 如果没有,初始化
        if (this.cache[eventName] === undefined){
            this.cache[eventName] = []
        }
        // 把fn 推进 this.cache[eventName] 数组
        let array = this.cache[eventName]
        array.push(fn)
    }
    emit(eventName) { 
        let array = this.cache[eventName]
        if (array === undefined) {
            array = []
        }
        // 把this.cache中函数依次执行
        array.forEach(fn => {
            fn()
        })
    }
}

export default EventHub

测试代码ts

  • console.assert
    • 如果断言为false,则将错误消息写入控制台。如果断言为真,则没有任何反应。
  • 运行ts-node test.ts, 没有中断, 测试通过
import EventHub from '../src/index'

const eventHub = new EventHub()

console.assert(eventHub instanceof Object === true, 'eventHub 是个对象')

// on emit
let called = false
eventHub.on('xxx', () => {
    called = true
    console.log('called:' + called)
})

eventHub.emit('xxx')

第二版 优化on、emit

on(eventName, fn) {
    // this.cache[eventName] 如果不存在,为[];否则为他自己
    // if (this.cache[eventName] === undefined) {
        // this.cache[eventName] = []
    // }
    // 将上面的代码优化为 下面一句(下同)
    this.cache[eventName] = this.cache[eventName] || []
    // let array = this.cache[eventName]
    // array.push(fn)
    this.cache[eventName].push(fn)
}
emit(eventName) { 
    // let array = this.cache[eventName]
    // if (array === undefined) {
    //     array = []
    // }
    // 此处与on优化方式相同 --> this.cache[eventName] || []
    
    // this.cache[eventName].forEach(fn => 
    //     fn()
    // )  
    // 上方的代码优化为下面一句
    (this.cache[eventName] || []).forEach(fn => fn())
}

第三版 emit实现传递参数

// ? 可以不传递参数 ...拓展运算符 可以传递多个参数
emit(eventName, ...args?) { 
        (this.cache[eventName] || []).forEach(fn => fn(...args))
}

测试 传递参数代码

// 测试 on emit
eventHub.on('xxx', (y) => {
    console.log('called:' + called)
    console.assert( y === 'hello')
})

eventHub.emit('xxx', 'hello')

第四版 添加off功能

    off(eventName, fn) {
        // 把fn从this.cache[eventName] 删掉
        this.cache[eventName] = this.cache[eventName] || []
        // 找一个元素是数组的第几项 array.indexOf(searchElement)
        // 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
        // 但是兼容性不好,不兼容IE11,这里自己实现一个函数
        let index
        for (let i = 0; i < this.cache[eventName].length; i++) {
            if (this.cache[eventName][i] === fn) {
                index = i
                break
            }
        }
        if(index === undefined) {
            return
        } else {
            this.cache[eventName].splice(index, 1)
        }
    }

第五版 优化off

    off(eventName, fn) {
        // this.cache[eventName] = this.cache[eventName] || [] 优化为下面一句
        let index = indexOf(this.cache[eventName], fn)
        // if(index === undefined) {
        //     return
        // } else {
        //     this.cache[eventName].splice(index, 1)
        // } 优化为下面一句
        if (index === undefined) return
        this.cache[eventName].splice(index, 1)
    }
}

export default EventHub

function indexOf(array, item) {
    if (array === undefined) return -1
    let index = -1
    for (let i = 0; i < array.length; i++) {
        if (array[i] === item) {
            index = i
            break
        }
    }
}

最终版 完整使用TypeScript重构

TypeScript 可以简单理解为 Type + JavaScript,现在我们来给所有参数加上类型定义

class EventHub {
    // 声明为一个cache 对象
    // -    key: value
    // -    key是string : value 数组
    // -                        数组中是函数 参数(未知) 和 返回值(为空)
    private cache: { [key: string]: Array<(data: unknown) => void>} = {}
    // unknow 第一次传递的为什么类型,以后只能传递此种类型,不能修改
    // void 返回值为空
    on(eventName: string, fn: (data: unknown) => void) {
        this.cache[eventName] = this.cache[eventName] || []
        this.cache[eventName].push(fn)
    }
    // data? 表示可以为空,不传递
    emit(eventName: string, data?: unknown) {
        (this.cache[eventName] || []).forEach(fn => fn(data))
    }
    off(eventName: string, fn: (data: unknown) => void) {
        let index = indexOf(this.cache[eventName], fn)
        if (index === undefined) return
        this.cache[eventName].splice(index, 1)
    }
}

export default EventHub

function indexOf(array: Array<unknown>, item: unknown) {
    if (array === undefined) return -1
    let index = -1
    for (let i = 0; i < array.length; i++) {
        if (array[i] === item) {
            index = i
            break
        }
    }
}

测试代码使用TypeScript

import EventHub from '../src/index'

// 定义一种类型 TestCase 传递string参数,返回值为空
type TestCase = (message: string) => void

// 第一个测试用例,起名为test1 在最后调用
// 断言 eventHub是个对象
const test1: TestCase = message => {
    const eventHub = new EventHub()
    console.assert(eventHub instanceof Object === true, 'eventHub 是个对象')
    // 确定成功之后打出message
    console.log(message)
}

const test2: TestCase = message => {
    const eventHub = new EventHub()
    // on emit
    let called = false
    eventHub.on('xxx', y => {
        called = true
        console.assert(y === 'hello')
    })
    eventHub.emit('xxx', 'hello')
    console.assert(called)
    console.log(message)
}

const test3: TestCase = message => {
    const eventHub = new EventHub()
    let called = false
    const fn1 = () => {
        called = true
    }
    eventHub.on('yyy', fn1)
    eventHub.off('yyy', fn1)
    eventHub.emit('yyy')
    console.assert(called === false)
    console.log(message)
}

test1('eventHub 可以创建对象')
test2('.on 之后,.emit会触发on的函数')
test3('.off有用')

这就是使用TypeScipt编写的发布订阅模式,感谢各位的阅读 ~

欢迎大家留下问题一起探讨 ~

希望可以在面试、工作中助你一臂之力!