├── .eslintignore ├── .gitignore ├── jest.config.js ├── .eslintrc.js ├── .babelrc.js ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ └── unit-test.js.yml ├── src └── index.ts ├── package.json ├── tests └── index.spec.ts └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | *.log 4 | dist 5 | coverage 6 | 7 | .idea 8 | .vscode 9 | .project 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | moduleFileExtensions: ['ts', 'js'], 4 | coverageProvider: 'v8', 5 | coverageReporters: ['text', 'json'], 6 | globals: { 7 | 'ts-jest': { 8 | babelConfig: true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | 'jest' 7 | ], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'standard', 12 | 'plugin:jest/all' 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ], 13 | // plugins: [ 14 | // '@babel/plugin-proposal-optional-chaining', 15 | // ], 16 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "dist", 5 | "sourceMap": false, 6 | "target": "esnext", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "allowJs": false, 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "experimentalDecorators": true, 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true, 15 | "removeComments": false, 16 | "types": ["jest", "node"], 17 | "declarationDir": "dist/types", 18 | "declaration": true 19 | }, 20 | "include": [ 21 | "src" 22 | ], 23 | "exclude": [] 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present, tangdaohai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Unit Test 5 | 6 | on: 7 | push: 8 | branches: [ vue3 ] 9 | pull_request: 10 | branches: [ vue3 ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: yarn 28 | - run: yarn test 29 | - name: Upload coverage to Codecov 30 | uses: codecov/codecov-action@v1 31 | with: 32 | # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 33 | file: ./coverage/coverage-final.json # optional 34 | flags: unittests # optional 35 | name: coverage # optional 36 | fail_ci_if_error: false # optional (default = false) 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInternalInstance, getCurrentInstance, onUnmounted } from 'vue' 2 | import Emitter, { EventNameType, EventCallback, ListenStopHandle } from 'happy-event-bus' 3 | 4 | const eventMap = new WeakMap>() 5 | const emitter = new Emitter() 6 | const markOnUnmounted = new WeakSet() 7 | 8 | const markListenHandle = (stopHandle: ListenStopHandle) => { 9 | // get vue instance 10 | let vm: ComponentInternalInstance | null = getCurrentInstance() 11 | if (vm === null) { 12 | // error. please call '$on/$once' on setup or Lifecycle Hooks 13 | return 14 | } 15 | const list = eventMap.get(vm) || eventMap.set(vm, []).get(vm) 16 | list?.push(stopHandle) 17 | 18 | // onUnmounted and mark it 19 | if (!markOnUnmounted.has(vm)) { 20 | markOnUnmounted.add(vm) 21 | onUnmounted(() => { 22 | const stopHandleList = vm && eventMap.get(vm) 23 | stopHandleList?.forEach(val => val()) 24 | // gc 25 | vm = null 26 | }, vm) 27 | } 28 | } 29 | 30 | export const $on = (type: EventNameType, callback: EventCallback): ListenStopHandle => { 31 | const stopHandle = emitter.on(type, callback) 32 | markListenHandle(stopHandle) 33 | return stopHandle 34 | } 35 | export const $once = (type: EventNameType, callback: EventCallback): ListenStopHandle => { 36 | const stopHandle = emitter.once(type, callback) 37 | markListenHandle(stopHandle) 38 | return stopHandle 39 | } 40 | export const $off = emitter.off 41 | export const $emit = emitter.emit 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-happy-bus", 3 | "version": "2.0.1-vue3", 4 | "description": "Event Bus for Vue3, automatically cancel listening events when unmounted.", 5 | "source": "src/index.ts", 6 | "amdName": "VueHappyBus", 7 | "main": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "esmodule": "dist/index.modern.js", 10 | "unpkg": "dist/index.umd.js", 11 | "types": "dist/types", 12 | "scripts": { 13 | "test": "jest --coverage", 14 | "build": "microbundle --strict" 15 | }, 16 | "files": [ 17 | "src", 18 | "dist" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/tangdaohai/vue-happy-bus.git" 23 | }, 24 | "keywords": [ 25 | "vue bus", 26 | "vue3 bus", 27 | "bus", 28 | "event-bus", 29 | "event-hub" 30 | ], 31 | "author": "tangdaohai@outlook.com", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/tangdaohai/vue-happy-bus/issues" 35 | }, 36 | "homepage": "https://github.com/tangdaohai/vue-happy-bus", 37 | "dependencies": { 38 | "vue": "^3.0.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.12.3", 42 | "@babel/preset-env": "^7.12.1", 43 | "@babel/preset-typescript": "^7.12.1", 44 | "@types/jest": "^26.0.15", 45 | "@typescript-eslint/eslint-plugin": "^4.7.0", 46 | "@typescript-eslint/parser": "^4.7.0", 47 | "@vue/test-utils": "^2.0.0-beta.9", 48 | "eslint": "^7.13.0", 49 | "eslint-config-standard": "^16.0.1", 50 | "eslint-plugin-import": "^2.22.1", 51 | "eslint-plugin-jest": "^24.1.3", 52 | "eslint-plugin-node": "^11.1.0", 53 | "eslint-plugin-promise": "^4.2.1", 54 | "eslint-plugin-standard": "^4.1.0", 55 | "happy-event-bus": "^1.0.0", 56 | "jest": "^26.6.3", 57 | "microbundle": "^0.12.4", 58 | "ts-jest": "^26.4.4", 59 | "typescript": "^4.0.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, nextTick } from 'vue' 2 | import { mount } from '@vue/test-utils' 3 | import { $on, $once, $emit, $off } from '../src/index' 4 | 5 | describe('vue-happy-bus:', () => { 6 | it('$on/$once and $emit:', async () => { 7 | expect.hasAssertions() 8 | const onceFn = jest.fn() 9 | const component = defineComponent({ 10 | template: '
{{foo}}
', 11 | setup () { 12 | const foo = ref('foo') 13 | 14 | $on('foo', (...args: Array) => (foo.value = args.join(''))) 15 | $once('bar', onceFn) 16 | 17 | return { 18 | foo 19 | } 20 | } 21 | }) 22 | 23 | const wrapper = mount(component) 24 | expect(wrapper.find('div').text()).toBe('foo') 25 | 26 | // emit, change foo value 27 | $emit('foo', 'foo1', 'foo2') 28 | await nextTick() 29 | expect(wrapper.find('div').text()).toBe('foo1foo2') 30 | 31 | // emit bar 32 | $emit('bar') 33 | expect(onceFn).toHaveBeenCalledTimes(1) 34 | // emit bar again 35 | $emit('bar') 36 | expect(onceFn).toHaveBeenCalledTimes(1) 37 | 38 | // $on return a function, run it will off himself 39 | // wrapper.vm.stopHandle() 40 | // $emit('foo', 'foo') 41 | // // foo value is bar, because foo event canceled 42 | // expect(wrapper.vm.foo).toBe('bar') 43 | }) 44 | 45 | it('$off:', () => { 46 | expect.hasAssertions() 47 | const fooFn = jest.fn() 48 | const barFn = jest.fn() 49 | const foo2Fn = jest.fn() 50 | const bar2Fn = jest.fn() 51 | const bazFn = jest.fn() 52 | 53 | const component = defineComponent({ 54 | template: '
', 55 | setup () { 56 | $on('foo', fooFn) 57 | const stopOnce = $once('bar', barFn) 58 | 59 | $on('foo', foo2Fn) 60 | 61 | $on('bar', bar2Fn) 62 | 63 | $once('baz', bazFn) 64 | 65 | return { 66 | stopOnce 67 | } 68 | } 69 | }) 70 | const wrapper = mount(component) 71 | // off fooFn 72 | $off('foo', fooFn) 73 | $emit('foo') 74 | expect(fooFn).toHaveBeenCalledTimes(0) 75 | expect(foo2Fn).toHaveBeenCalledTimes(1) 76 | 77 | // off all foo 78 | $off('foo') 79 | $emit('foo') 80 | expect(fooFn).toHaveBeenCalledTimes(0) 81 | expect(foo2Fn).toHaveBeenCalledTimes(1) 82 | 83 | // off bar once 84 | wrapper.vm.stopOnce() 85 | $emit('bar') 86 | expect(barFn).toHaveBeenCalledTimes(0) 87 | expect(bar2Fn).toHaveBeenCalledTimes(1) 88 | 89 | // bar2Fn and bazFn not cancelled before off all events 90 | expect(bazFn).toHaveBeenCalledTimes(0) 91 | $off() 92 | $emit('bar') 93 | $emit('baz') 94 | expect(bar2Fn).toHaveBeenCalledTimes(1) 95 | expect(bazFn).toHaveBeenCalledTimes(0) 96 | }) 97 | 98 | it('event type symbol/string/number:', () => { 99 | expect.hasAssertions() 100 | const symbolFoo = Symbol('foo') 101 | const symbolFn = jest.fn() 102 | const numberFn = jest.fn() 103 | const component = defineComponent({ 104 | template: '
', 105 | setup () { 106 | $on(symbolFoo, symbolFn) 107 | $on(9999, numberFn) 108 | } 109 | }) 110 | mount(component) 111 | 112 | $emit(symbolFoo) 113 | expect(symbolFn).toHaveBeenCalledTimes(1) 114 | 115 | $emit(9999) 116 | expect(numberFn).toHaveBeenCalledTimes(1) 117 | }) 118 | 119 | it('quickly cancel listen:', () => { 120 | expect.hasAssertions() 121 | const fooFn = jest.fn() 122 | const barFn = jest.fn() 123 | const fooStop = $on('foo', fooFn) 124 | const barStop = $once('bar', barFn) 125 | 126 | $emit('foo') 127 | expect(fooFn).toHaveBeenCalledTimes(1) 128 | // cancel foo-fooFn 129 | fooStop() 130 | $emit('foo') 131 | expect(fooFn).toHaveBeenCalledTimes(1) 132 | 133 | // stop bar 134 | barStop() 135 | $emit('bar') 136 | expect(barFn).toHaveBeenCalledTimes(0) 137 | }) 138 | 139 | it('vue3 Composition API auto cancel listen:', () => { 140 | expect.hasAssertions() 141 | const fn = jest.fn() 142 | const component = defineComponent({ 143 | template: '
', 144 | setup () { 145 | $on('foo', fn) 146 | } 147 | }) 148 | const wrapper = mount(component) 149 | expect(fn).toHaveBeenCalledTimes(0) 150 | 151 | $emit('foo') 152 | expect(fn).toHaveBeenCalledTimes(1) 153 | 154 | wrapper.unmount() 155 | $emit('foo') 156 | expect(fn).toHaveBeenCalledTimes(1) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vue-happy-bus 2 | === 3 | [![Github Actions Test](https://github.com/tangdaohai/vue-happy-bus/workflows/Unit%20Test/badge.svg)](https://github.com/tangdaohai/vue-happy-bus/actions?query=workflow%3A%22Unit+Test%22) 4 | Downloads 5 | Version 6 | License 7 | ![欢迎PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 8 | 9 | ### 注意 10 | 11 | * 此版本基于 `vue3` 使用,如果您是 `vue2` 用户请查看[这个版本](https://github.com/tangdaohai/vue-happy-bus/tree/master) 12 | * 当前版本基于 vue3 和 [happy-event-bus](https://github.com/tangdaohai/happy-event-bus) 运行,如果您是 React/Angular/原生JS 用户可以直接使用 [happy-event-bus](https://github.com/tangdaohai/happy-event-bus) 13 | 14 | ### vue-happy-bus 是干嘛的 15 | 16 | > 在 vue3 版本中删除了 `$on/$once/$off` API ([see](https://v3.vuejs.org/guide/migration/events-api.html#_3-x-update)),不过不用担心,可以使用此仓库作为替代方案,继续使用 event bus 的方式来实现跨组件的通信功能,并且不用手动去 $off 事件回调。 17 | 18 | `vue-happy-bus`是一款基于vue3实现的`订阅/发布`插件。 19 | 20 | 在 vue 中,我们可以使用 event bus 来实现 `跨组件间通信`。但一个弊端就是,这种方式并不会自动销毁,所以为了避免回调函数重复执行,还要在 `onUnmounted` 中去移除回调函数。 21 | 22 | 这样带来的冗余代码就是: 23 | 24 | 1. $on 的回调函数必须是具名函数。不能简单的 `$on('event name', () => {})` 使用匿名函数作为回调,因为这样无法销毁事件监听,所以一般采用 `具名函数` 作为回调 25 | 2. 在`onUnmounted`生命周期中去销毁事件的监听。 26 | 27 | 我只是想在某个路由中监听下 header 中一个按钮的点击事件而已,竟然要这么麻烦??? 28 | 29 | 所以此轮子被造出来了 🤘。 30 | 31 | 它主要解决在`夸组件间通信时`,避免重复绑定事件、无法自动销毁的而导致`回调函数被执行多次`的问题。 32 | 33 | **总得来说他是能让你`偷懒`少写代码的工具。** 34 | 35 | ### 安装 36 | 37 | 1. npm/yarn 38 | 39 | ```shell 40 | npm i vue-happy-bus@next 41 | # or 42 | yarn add vue-happy-bus@next 43 | ``` 44 | 45 | 2. CDN 46 | 47 | ```html 48 | 49 | 50 | 54 | 55 | ``` 56 | 57 | ### 如何使用 58 | 59 | 自动注销监听事件的方式: 60 | 61 | ```typescript 62 | // foo.vue 63 | import { $on } from 'vue-happy-bus' 64 | export default { 65 | setup () { 66 | // 在 foo.vue unmounted 后,会自动销毁 foo.vue 中的事件回调 67 | $on('event name', (...args) => { 68 | // do something 69 | }) 70 | } 71 | } 72 | 73 | // bar.vue 74 | import { $emit } from 'vue-happy-bus' 75 | export default { 76 | setup () { 77 | // 触发 foo.vue 中的 event name 事件 78 | $emit('event name', 'bar1', 'bar2') 79 | } 80 | } 81 | ``` 82 | 83 | `$on/$once` 会返回一个取消监听函数,用来停止触发回调: 84 | 85 | ```typescript 86 | import { $on } from 'vue-happy-bus' 87 | export default { 88 | setup () { 89 | const stop = $on('foo', (...args) => { 90 | // 停止监听 foo 事件 91 | stop() 92 | }) 93 | } 94 | } 95 | ``` 96 | 97 | 98 | 99 | ### API 100 | 101 | ##### $on(eventName, callback) 102 | 103 | 监听一个事件,可以由 `$emit` 触发,回调函数会接收所有传入事件触发函数的额外参数。 104 | 105 | * 参数 106 | 107 | * `{string | number | symbol}` eventName 108 | * `{Function}` callback 109 | 110 | * 返回 111 | 112 | * `{Function}` ListenStopHandle 113 | 114 | * 示例 115 | 116 | ```typescript 117 | // string 118 | $on('foo', (msg) => { 119 | console.log(msg) 120 | }) 121 | $emit('foo', 'hi') // => hi 122 | 123 | // symbol 124 | export const symbolFoo = Symbol('foo') 125 | $on(symbolFoo, (msg) => { 126 | console.log(msg) 127 | }) 128 | $emit(symbolFoo, 'hi') // => hi 129 | 130 | // number 131 | export const FOO = 1 132 | $on(FOO, (msg) => { 133 | console.log(msg) 134 | }) 135 | $emit(FOO, 'hi') // => hi 136 | 137 | // return 138 | const stop = $on('foo', () => {}) 139 | // 主动取消监听 140 | stop() 141 | ``` 142 | 143 | ##### $once(eventName, callback) 144 | 145 | 监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。 146 | 147 | * 参数 148 | 149 | * `{string | number | symbol}` 事件名称 150 | * `{Function}` callback 151 | 152 | * 返回 153 | 154 | * `{Function}` ListenStopHandle 155 | 156 | * 示例 157 | 158 | ```typescript 159 | // 使用方式与 $on 一致 160 | $once('foo', (msg) => { 161 | console.log(msg) 162 | }) 163 | $emit('foo', 'hi') // => hi 164 | // emit again 165 | $emit('foo') // foo 回调不会执行,已经在 event bus 移除了 166 | ``` 167 | 168 | ##### $off(eventName, callback) 169 | 170 | > 如果只是移除一个回调函数的监听,建议使用 `$on` 的返回值来取消。 171 | 172 | * 说明 173 | 174 | 移除自定义事件监听。 175 | 176 | - 如果没有提供参数,则移除所有的事件监听; 177 | - 如果只提供了事件,则移除该事件所有的监听; 178 | - 如果同时提供了事件与回调,则只移除这个回调的监听。 179 | 180 | * 参数 181 | 182 | * `{string | number | symbol | undefined}` 事件名称 183 | * `{Function}` callback 184 | 185 | ##### $emit(eventName, [...args]) 186 | 187 | 触发指定的事件。附加参数都会传给事件监听器的回调。 188 | 189 | * 参数 190 | 191 | * `{string | number | symbol}` eventName 192 | * `[...args: Array]` 193 | 194 | * 示例 195 | 196 | ```typescript 197 | $on('foo', (...args) => console.log(args)) 198 | 199 | $emit('foo', 'hi') // => ['hi'] 200 | $emit('foo', 'hi', 'vue3') // => ['hi', 'vue3'] 201 | ``` 202 | 203 | ### 升级 204 | 205 | 确保已完成了 vue2 升级到 vue3 的工作。 206 | 207 | 1. 更新或者重新安装 `vue-happy-bus@next` 208 | 209 | 2. 因为导出方式的改变,需要手动修改引入方式。如果涉及多处修改,可使用下面的方式进行兼容: 210 | 211 | * 保存下面的代码为 `src/bus.ts` 212 | 213 | ```typescript 214 | import { $on, $once, $off, $emit } from 'vue-happy-bus' 215 | export const Bus = { $on, $once, $off, $emit } 216 | const BusFactory = () => Bus 217 | BusFactory.$emit = $emit 218 | BusFactory.$once = $once 219 | export default BusFactory 220 | ``` 221 | 222 | * 借助编辑器的全局搜索替换功能,替换以下代码 223 | 224 | ```js 225 | import BusFactory, { Bus } from 'vue-happy-bus' 226 | // 将 vue-happy-bus 替换为 @/bus (@ 为 src 目录) 227 | import BusFactory, { Bus } from '@/bus' 228 | ``` 229 | 230 | ### PR 231 | 期待并欢迎您的PR。 232 | 但请您一定要遵守 [standard ](https://github.com/standard/standard)代码风格规范。 233 | 234 | ### License 235 | 236 | [MIT](http://opensource.org/licenses/MIT) 237 | 238 | Copyright (c) 2017-present, tangdaohai 239 | --------------------------------------------------------------------------------