Giter Site home page Giter Site logo

nanoevents's Introduction

Nano Events

Simple and tiny event emitter library for JavaScript.

  • Only 107 bytes (minified and brotlied). It uses Size Limit to control size.
  • The on method returns unbind function. You don’t need to save callback to variable for removeListener.
  • TypeScript and ES modules support.
  • No aliases, just emit and on methods. No Node.js EventEmitter compatibility.
import { createNanoEvents } from 'nanoevents'

const emitter = createNanoEvents()

const unbind = emitter.on('tick', volume => {
  summary += volume
})

emitter.emit('tick', 2)
summary //=> 2

unbind()
emitter.emit('tick', 2)
summary //=> 2
Sponsored by Evil Martians

Table of Contents

Install

npm install nanoevents

TypeScript

Nano Events accepts interface with event name to listener argument types mapping.

interface Events {
  set: (name: string, count: number) => void,
  tick: () => void
}

const emitter = createNanoEvents<Events>()

// Correct calls:
emitter.emit('set', 'prop', 1)
emitter.emit('tick')

// Compilation errors:
emitter.emit('set', 'prop', '1')
emitter.emit('tick', 2)

Mixing to Object

Because Nano Events API has only just 2 methods, you could just create proxy methods in your class or encapsulate them entirely.

class Ticker {
  constructor () {
    this.emitter = createNanoEvents()
    this.internal = setInterval(() => {
      this.emitter.emit('tick')
    }, 100)
  }

  stop () {
    clearInterval(this.internal)
    this.emitter.emit('stop')
  }

  on (event, callback) {
    return this.emitter.on(event, callback)
  }
}

With Typescript:

import { createNanoEvents, Emitter } from "nanoevents"

interface Events {
  start: (startedAt: number) => void
}

class Ticker {
  emitter: Emitter

  constructor () {
    this.emitter = createNanoEvents<Events>()
  }

  on<E extends keyof Events>(event: E, callback: Events[E]) {
    return this.emitter.on(event, callback)
  }
}

Add Listener

Use on method to add listener for specific event:

emitter.on('tick', number => {
  console.log(number)
})

emitter.emit('tick', 1)
// Prints 1
emitter.emit('tick', 5)
// Prints 5

In case of your listener relies on some particular context (if it uses this within itself) you have to bind required context explicitly before passing function in as a callback.

var app = {
  userId: 1,
  getListener () {
    return () => {
      console.log(this.userId)
    }
  }
}
emitter.on('print', app.getListener())

Note: binding with use of the .bind() method won’t work as you might expect and therefore is not recommended.

Remove Listener

Methods on returns unbind function. Call it and this listener will be removed from event.

const unbind = emitter.on('tick', number => {
  console.log('on ' + number)
})

emitter.emit('tick', 1)
// Prints "on 1"

unbind()
emitter.emit('tick', 2)
// Prints nothing

Execute Listeners

Method emit will execute all listeners. First argument is event name, others will be passed to listeners.

emitter.on('tick', (a, b) => {
  console.log(a, b)
})
emitter.emit('tick', 1, 'one')
// Prints 1, 'one'

Events List

You can get used events list by events property.

const unbind = emitter.on('tick', () => { })
emitter.events //=> { tick: [ [Function] ] }

Once

If you need add event listener only for first event dispatch, you can use this snippet:

class Ticker {
  constructor () {
    this.emitter = createNanoEvents()
  }
  
  once (event, callback) {
    const unbind = this.emitter.on(event, (...args) => {
      unbind()
      callback(...args)
    })
    return unbind
  }
}

Remove All Listeners

emitter.on('event1', () => { })
emitter.on('event2', () => { })

emitter.events = { }

nanoevents's People

Contributors

38elements avatar ai avatar azu avatar ben-eb avatar dan-lee avatar dependabot[bot] avatar dharkness avatar gwer avatar ifiokjr avatar igisev avatar jakub791 avatar jrobichaud avatar mnasyrov avatar nulltier avatar posva avatar subzey avatar trysound avatar ukstv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nanoevents's Issues

TypeError: (0 , _nanoevents.createNanoEvents) is not a function

Hi 👋

I'm trying use/test nanonevents in codesandbox (https://codesandbox.io/s/mutable-shape-8urc9?file=/src/index.js) and with create-react-app's jest, both environments don't work with the library out of the box.

As I understand some of the defaults don't work well with *.cjs file extension ("main": "index.cjs",). Maybe it's worth renaming the file to follow a more common extension?

FYI: Renaming index.cjs -> index.cjs.js and changing "main": "index.cjs", -> "main": "index.cjs.js", seems to fix the issue.

Incorrect type declarations

The problem

In index.d.ts Emitter is declared to be a class. That's incorrect.

import { Emitter } from "nanoevents";

const emitter = new Emitter<{
    tick: () => void;
}>();

emitter.on("tick", () => console.log("tick"));
emitter.emit("tick");

At runtime the above code snippet throws an error Import named 'Emitter' not found in module '/home/jakub/server/node_modules/nanoevents/index.js'. because nanoevents dosen't export a value named Emitter. Even thought type definitions imply it should, because a class should to exist at runtime.

Solution

Emitter should be declared as a type alias/interface, not a class so that typescript dosen't allow you to use it where a value is expected.

Speed improvement vs minimal size?

Hey, what's your stance on speed improvement vs package size? Also, what about ?. aka optional chaining?

I was playing with the package, and figured how to make it a bit faster: https://gist.github.com/ukstv/570a633d56b945f7f51875954dabe0c9

Do you think it is worth a PR here?

It makes the package about 50% bigger, but at the same time runtime performance seems to be improved (on M1 Pro):

  • Chrome and Node.js: about 50% improvement: 57k vs 38k ops/s for the current code,
  • Safari: 4x improvement: 60k vs 15k ops/s
  • Firefox: 80% improvement: 18k vs 10k ops/s

Callback context

i.apply(this, args)

Previously, when it was a for loop, this was referring to the class instance itself. Now, within .map function it refers to global. I'm not really sure it's useful.

We could:

  • Bring back the old behavior: .map(function(){ /*...*/ }, this), or
  • Leave as is (same as i.call(null, args))

Importing error in React Native starting from nanoevents 3.0 version

Because .cjs source extension is not very commonly used and React Native do not know it and interprets as part of filename.

error: bundling failed: Error: While trying to resolve module `nanoevents` from file `.../src/api/ws/client.js`, the package `.../node_modules/nanoevents/package.json` was successfully found. However, this package itself specifies a `main` module field that could not be resolved (`.../node_modules/nanoevents/index.cjs`. Indeed, none of these files exist:

  * .../node_modules/nanoevents/index.cjs(.native|.android.js|.native.js|.js|.android.json|.native.json|.json|.android.ts|.native.ts|.ts|.android.tsx|.native.tsx|.tsx)
  * .../node_modules/nanoevents/index.cjs/index(.native|.android.js|.native.js|.js|.android.json|.native.json|.json|.android.ts|.native.ts|.ts|.android.tsx|.native.tsx|.tsx)

Workarounds:

  1. import {createNanoEvents} from 'nanoevents/index'
  2. add .cjs extension to metro.config.js:
const {getDefaultValues} = require('metro-config/src/defaults')
const {resolver: {sourceExts}} = getDefaultValues()

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
  resolver: {
    sourceExts: [...sourceExts, 'cjs'],
  },
}

But it will be nice nanoevents package do not cause the error and works without need an additional configuration.

  1. Use index.cjs.js file name instead
  2. Or add react-native field to package.json with index.js value

Not 107 bytes

Hi! Its actually not 107 bytes! Minimum I measured was 138.

// using minified-size

"originalSize": 419,
"minifiedSize": 219,
"gzippedSize": 167,
"brotliedSize": 138

perhaps doc is broken: this.events unknown

class Ticker {
  constructor() {
    this.emitter = new NanoEvents()
  }
  on() {
    return this.emitter.on.apply(this.events, arguments)
  }
  tick() {
    this.emitter.emit('tick')
  }
}

what is this.events?

This code breaks in my case with error looking like:

main.js:1 Uncaught TypeError: Cannot read property 'streaming' of undefined

where 'streaming' is my event

Consider replacing filter in emit

You could replace the call to Array.prototype.filer in emit with the semantically correct forEach to avoid the array allocation, but that would make the code one character longer.

If you want the code to be shorter and don’t care about the allocation, you could change it to map.

What is the reason behind binding listeners to global object?

I'm pretty sure it was done intentionally. The comment there is also a hint of purposeful binding.

i.apply(this, args) // this === global or window

However I still don't see the thing. On my level of understanding i.apply(null, args) is more predictable because it forces users to bind their listeners to the context they want on their own. Their listeners will throw an exception if they forget to do so.

window.test = 'test'
function testFn() { console.log(this.test); }
testFn.bind(window).apply(null); // outputs 'test', apply doesn't overwrite the context set with bind 

What I'm missing here?

https://github.com/ai/nanoevents/blob/master/index.js#L55

Multiple identical subscriptions

When single function is used as a subscription multiple times (on single event name), then using disposer would remove all copies at once.
This code: .filter(i => i !== cb) eliminates all entries, which is counterintuitive for subscriber's party.
One subscription must be removed at a time.
This is the edge case, but worth mentioning.

Suggestion: .once method()

Hi nano-bro!

I know that nanoevents is positioned as "nano" eventbus but it would be better if .once() method will be implemented.
It's cheaper than writing unbind() for each needed event manually.

I can implement it if you'll agree

"unbind" callback fails after removing all listeners

Hi! unbind callback fails after removing all listeners by the recommended way by the docs.

Tested on [email protected]:

import { createNanoEvents } from 'nanoevents';

const emitter = createNanoEvents();
const unbind = emitter.on('foo', () => undefined);

emitter.events = {};

// Error message: "Cannot read property 'filter' of undefined"
expect(() => unbind()).not.toThrowError();

Add npm installation commands in readme

It's easy for everyone to install it, if there is a line in readme saying - "Use npm install nanoevents to install it." Or else, i have to visit npm again to confirm the package name.

Memory leak

Happens when on and off over time. Simulation:

const emitter = createNanoEvents();
for (let i = 0; i < 1000; i += 1) {
    const off = emitter.on(String(i), console.log);
    // after some time
    off();
}
// emitter.events
// lots of empty arrays that are never deleted

The info in About section is outdated

In the About section it says 72 kb, but it is 99 kb in README

Screen Shot 2022-04-19 at 17 13 51

Since the About section is out of version control, I can't create a PR to fix it. "it ain't much but it's honest work"

It is impossible to bind listener to arbitrary because of the non-strict mode

I've decided to write tests for the change done by #21 and find that the pull request actually changed nothing.

In 'non-strict' mode i.apply(null, args) binds this to global object. I've missed this fact because of the habit to use ES modules everywhere. For them 'strict mode' enabled by default. This was the reason why I mistakenly extrapolated results of my small experiment on all cases.

window.test = 'test'
function testFn() { console.log(this.test); }
testFn.bind(window).apply(null);

This code works correct not because apply do not overwrite the bind. It works because non-strict mode.

You may check my tests here. I added comments there about unexpected behavior.

Fast googling helped me find clear confirmation only here https://learn.javascript.ru/call-apply (only only russian, within highlighted block). MDN page about strict mode contains no precise confirmation of it.

What does it mean?

It means that without strict mode it is impossible to bind the context while using the i.apply(null, args) or i.apply(this, args). The new example on the readme page is just does not work. And revert of my PR won't fix this problem :(

Enabled strict mode will increase the size of the package up to 113 B. I'm still think about other ways of reducing the size but anyway this is a hard blow.

And, of course, I also searching about ways of replacing i.apply(null, args) with direct call of a listener, but I have no strong believe in it is possible.

Won't be smaller and efficient using a `Set`?

I just wrote an event emitter and googled to see how other people were doing. Found this. Very interesting, I wrote something similar but instead of using arrays I figured I could use a Set instead. I was wondering if the code would be smaller, as that seems to be the objective of this library.

Like 321 chars vs 292 chars, 218 vs 199 (minified)

export let createNanoEvents = () => ({
  events: {},
  emit(event, ...args) {
    ;(this.events[event] || new Set()).forEach(i => i(...args))
  },
  on(event, cb) {
    ;(this.events[event] = this.events[event] || new Set()).add(cb)
    return () =>
      this.events[event].delete(cb)
  }
})

I'm also not sure what's your motivation for the defensive programming on the emit method. You are creating the event if it doesn't exist. That looks like code smell and could be confusing. If someone is trying to emit an event for something that doesn't exist, it should just fail?

If you want to keep that behaviour, you may make it smaller by using an array for that case, as its just useless.

export let createNanoEvents = () => ({
  events: {},
  emit(event, ...args) {
    ;(this.events[event] || []).forEach(i => i(...args))
  },
  on(event, cb) {
    ;(this.events[event] = this.events[event] || new Set()).add(cb)
    return () =>
      this.events[event].delete(cb)
  }
})

I would prefer instead

export let createNanoEvents = () => ({
  events: {},
  emit(event, ...args) {
    this.events[event].forEach(i => i(...args))
  },
  on(event, cb) {
    ;(this.events[event] = this.events[event] || new Set()).add(cb)
    return () =>
      this.events[event].delete(cb)
  }
})

I wanted to send a pull request, but I am too lazy to deal with all that typescript :P

Good luck!

Using this in a TypeScript library with `"declaration": true` fails

Description

The error is shown below.

Public property 'events' of exported class has or is using name 'Emitter' from external module "/path/to/node_modules/nanoevents/index" but cannot be named

The fix would be to update the index.d.ts file for the project to look like this. This works because when setting export = createNanoEvents then we also have to wrap everything in a namespace so that it can be accessed by external code.

declare namespace createNanoEvents {
  interface EventsMap {
    [event: string]: any
  }

  interface DefaultEvents extends EventsMap {
    [event: string]: (...args: any) => void
  }

  class Emitter<Events extends EventsMap> {
    /**
     * Event names in keys and arrays with listeners in values.
     *
     * ```js
     * emitter1.events = emitter2.events
     * emitter2.events = { }
     * ```
     */
    events: Partial<{ [E in keyof Events]: Events[E][] }>

    /**
     * Add a listener for a given event.
     *
     * ```js
     * const unbind = ee.on('tick', (tickType, tickDuration) => {
     *   count += 1
     * })
     *
     * disable () {
     *   unbind()
     * }
     * ```
     *
     * @param event The event name.
     * @param cb The listener function.
     * @returns Unbind listener from event.
     */
    on <K extends keyof Events>(this: this, event: K, cb: Events[K]): () => void

    /**
     * Calls each of the listeners registered for a given event.
     *
     * ```js
     * ee.emit('tick', tickType, tickDuration)
     * ```
     *
     * @param event The event name.
     * @param args The arguments for listeners.
     */
    emit <K extends keyof Events>(
      this: this,
      event: K,
      ...args: Parameters<Events[K]>
    ): void
  }
}

/**
 * Create event emitter.
 *
 * ```js
 * import createNanoEvents from 'nanoevents'
 *
 * class Ticker {
 *   constructor() {
 *     this.emitter = createNanoEvents()
 *   }
 *   on(...args) {
 *     return this.emitter.on(...args)
 *   }
 *   tick() {
 *     this.emitter.emit('tick')
 *   }
 * }
 * ```
 */
declare function createNanoEvents<Events extends createNanoEvents.EventsMap = createNanoEvents.DefaultEvents> (
): createNanoEvents.Emitter<Events>

export = createNanoEvents

Can't Compile with Declarations

See: #40

I'm having the same issue but on 7.0.1

 ERROR  Failed to compile with 1 error                                                                       2:21:48 PM

 error  in /node_modules/nanoevents/index.js

Module parse failed: Unexpected token (10:23)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
|   },
|   on(event, cb) {
>     this.events[event]?.push(cb) || (this.events[event] = [cb])
|     return () => {
|       this.events[event] = this.events[event]?.filter(i => cb !== i)

Rolling back to 6.0.0 solves it for me. Changing my .tsconfig.js to have "declaration": false also fixes it but obviously that's not a solve, I need types. I do not have this issue with any other node module in my pkg.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.