A New Event Subscription API
We’ve recently been focusing on cleaning up Atom’s API, improving its consistency and usability before we commit to supporting it without breaking changes after Atom reaches version 1.0. As part of this effort, we’re changing our approach to event subscription in Atom.
The previous design grew organically at a time when jQuery played a prominent role in Atom. As a result, we designed our event API to blend in with jQuery’s
.off methods, which are based on passing around event names as strings. Over time, we’ve found this approach to be less than ideal in a number of respects.
The Old Approach
String-based events are awkward to document and difficult to inspect at runtime. When subscribing, it’s possible to pass an event name that isn’t even emitted by the target object without receiving an error or warning. Unsubscribing is also confusing due to features we borrowed from jQuery that ended up being anti-patterns, such as namespaces and the ability to cancel subscriptions registered by other packages via
# Subscribingsubscription = editoron 'changed.my-namespace'= # ...# Unsubscribingsubscriptionoff# or...editoroff 'changed'handler# or...editoroff 'changed.my-namespace'# or...editoroff 'changed' # ruin everything by unsubscribing everyone
The New Approach
In our new approach to event subscription, we decided to focus on simplicity and consistency above all else. There should be one way to subscribe and one way to unsubscribe, and mixins shouldn’t be required to interact with or implement event-based APIs. To support these goals, we’ve introduced explicit methods for subscribing to events:
# Subscribingsubscription = editoronDidChange # ...# Unsubscribingsubscriptiondispose
Event names always start with
onWill, then follow with a verb and an optional noun. For example,
::onDidChangePath. While the auxilliary “will” or “did” adds a bit to the length, it makes it easy to name all the methods according to a consistent scheme, regardless of when callbacks are invoked relative to the named event.
As you saw above, whenever you subscribe to an event via a new subscription method, it returns a disposable instance. If the lifetime of the observing object is shorter than that of the observed object, it’s important that you cancel the subscription by calling
.dispose() on this disposable when the subscription is no longer needed. Failure to do this will cause a memory leak.
:@subscription = observedonDidChange => # ...: ->@subscriptiondispose # Don't forget to cancel your subscription!
If you’re making multiple subscriptions, our new helper library event-kit has a simple
CompositeDisposable class that you can use to aggregate multiple disposables.
# include event-kit as a dependency if you want to use thisrequire 'event-kit':@subscriptions =@subscriptionsadd observed1onDidChange => # ...@subscriptionsadd observed1onDidActivate => # ...@subscriptionsadd observed2onWillDance => # ...: ->@subscriptionsdispose # Dispose of all subscriptions at once
Managing subscriptions was previously handled by including the
Subscriber mixin from the emissary library, which gave the including object
unsubscribe methods. We’ve now opted to use an explicit disposable instance instead, because it’s not always possible or desirable to introduce a mixin in a situation where subscription tracking is needed. By making subscription management explicit, we hope to also make it easier to understand and emulate.
We plan to completely replace emissary with event-kit. Emissary is encumbered by supporting too many features we no longer need and is actually hurting performance because of it. By switching to a new, radically simplified library, we can easily leave the old event support in place during the deprecation period.
Implementing Event-Oriented APIs
Event-kit also provides a simple toolkit for implementing event-oriented APIs. Using the library is completely optional, but it’s a convenient way to build out APIs that blend in with the rest of Atom. Here’s the pattern we now use internally to implement events:
require 'event-kit': ->@emitter =:@emitteron 'did-change'callback: -># do changes...@emitteremit 'did-change'changeEvent # 1 optional argument allowed: ->@emitterdispose # remove subscribers on destruction
The emitter automatically returns a
Disposable instance which can be used to unsubscribe. You can also create your own disposable instance with an action to perform on disposal, which can be useful if you want to track when observers unsubscribe.
require 'event-kit': ->@emitter =:subscription = @emitteron 'did-change'callback@changeSubscriptionAdded=>@changeSubscriptionRemovedsubscriptiondispose: -> # ...: -> # ...# ...
The old string-based approach to events will continue to work for now, but as of Atom 0.126.0, you’ll see deprecation warnings in the spec runner guiding you to use the new approach. Once we complete some other planned changes to the API, we’ll start showing deprecation warnings to users and make a big push to switch all packages to the 1.0 APIs before removing the deprecated methods entirely. Thanks for rolling with us as we iterate on the package authoring experience.