Le modèle publish/subscribe (souvent abrégé en pub/sub) vous aide à découpler les fonctionnalités. Un composant publie un événement. Chaque subscriber qui s'est abonné à cet événement exécute son callback. Le publisher ne sait pas qui s'est abonné. Le subscriber n'a pas besoin de savoir qui a publié. Résultat : un code simple et testable.
Que signifie subscribe ?
S'abonner consiste à enregistrer un callback pour un événement nommé. Vous pouvez avoir un subscriber ou plusieurs subscribers pour le même événement. Quand quelqu'un publie cet événement, le callback de chaque subscriber s'exécute avec les données fournies.
// Minimal shape we aim for
// bus.subscribe(eventName, callback)
// bus.publish(eventName, ...args)
Un petit utilitaire Pub/Sub robuste
Voici une implémentation propre qui couvre les besoins réels : subscribe, publish, unsubscribe et subscribeOnce. Elle est petite, rapide et indépendante du framework.
export function createPubSub() {
const subscribersByEvent = Object.create(null);
function subscribe(eventName, callback) {
if (typeof callback !== 'function') {
throw new TypeError('subscriber callback must be a function');
}
if (!Array.isArray(subscribersByEvent[eventName])) {
subscribersByEvent[eventName] = [];
}
subscribersByEvent[eventName].push(callback);
// return unsubscribe handle for convenience
return function unsubscribe() {
const list = subscribersByEvent[eventName];
if (!list) return;
const index = list.indexOf(callback);
if (index !== -1) list.splice(index, 1);
if (list.length === 0) delete subscribersByEvent[eventName];
};
}
function subscribeOnce(eventName, callback) {
const unsubscribe = subscribe(eventName, function wrappedOnce(...args) {
unsubscribe();
callback(...args);
});
return unsubscribe;
}
function publish(eventName, ...args) {
const list = subscribersByEvent[eventName];
if (!list || list.length === 0) return 0;
// copy to prevent mutation during publish from breaking iteration
const currentSubscribers = list.slice();
for (const subscriber of currentSubscribers) {
try {
subscriber(...args);
} catch (error) {
// In production, you might log this error. We keep the loop going
// so one bad subscriber doesn't break others.
}
}
return currentSubscribers.length;
}
function clear(eventName) {
if (eventName) {
delete subscribersByEvent[eventName];
} else {
for (const key in subscribersByEvent) delete subscribersByEvent[key];
}
}
return { subscribe, subscribeOnce, publish, clear };
}
// Example usage
const bus = createPubSub();
const unsubscribeAlert = bus.subscribe('alert', (payload) => {
console.log('Alert subscriber:', payload.message);
});
bus.publish('alert', { message: 'Hello, subscriber!' });
// Stop this subscriber later
unsubscribeAlert();
// Subscribe once
bus.subscribeOnce('ready', () => console.log('Ready fired once'));
bus.publish('ready');
bus.publish('ready'); // second call does nothing
Les subscribers dans l'interface utilisateur : un câblage simple
Un schéma courant consiste à garder les composants UI légers. Le formulaire publie des événements comme
form:success ou form:error. Un système de toast s'abonne à ces événements et affiche
des messages. Le formulaire n'importe pas le toast. Le toast n'importe pas le formulaire.
// form.js
import { bus } from './shared-bus.js';
async function submitForm(data) {
try {
await api.save(data);
bus.publish('form:success', { message: 'Saved!' });
} catch (error) {
bus.publish('form:error', { message: 'Please try again.' });
}
}
// toast.js
import { bus } from './shared-bus.js';
bus.subscribe('form:success', ({ message }) => showToast(message, 'success'));
bus.subscribe('form:error', ({ message }) => showToast(message, 'error'));
Unsubscribe et sécurité mémoire
Gardez toujours une poignée de unsubscribe. Supprimez le subscriber lorsqu'un composant est démonté ou qu'une page change. Cela évite les subscribers orphelins et les fuites mémoire.
const stop = bus.subscribe('tick', () => updateTime());
// later
stop(); // unsubscribe this subscriber
Conseils de pattern et bonnes pratiques
- Nommez les événements de façon cohérente : utilisez des espaces de noms comme
cart:add,user:login. - Retournez unsubscribe : chaque subscribe doit renvoyer une fonction unsubscribe propre.
- Gardez les payloads simples : privilégiez les objets plats. Documentez les champs requis.
- N'abusez pas du pub/sub : les appels de fonction directs sont très bien lorsque le couplage est voulu.
- Gérez les erreurs par subscriber : un subscriber défaillant ne doit pas bloquer les autres.
- Fournissez subscribeOnce : utile pour les moments de cycle de vie ponctuels comme
ready.
Canaux typés (facultatif)
En TypeScript, vous pouvez typer les noms d'événements et les payloads pour sécuriser davantage les appels subscribe et publish.
type Events = {
'alert': { message: string };
'ready': void;
};
function createTypedPubSub>() {
const bus = createPubSub();
return bus as {
subscribe(event: K, cb: (payload: E[K]) => void): () => void;
subscribeOnce(event: K, cb: (payload: E[K]) => void): () => void;
publish(event: K, payload: E[K]): number;
clear(event?: keyof E): void;
};
}
const typedBus = createTypedPubSub();
typedBus.subscribe('alert', (p) => console.log(p.message));
typedBus.publish('alert', { message: 'Typed and safe' });
Conclusion
Le modèle publish/subscribe vous offre une façon propre de relier les parties de votre application sans couplage fort. Avec un utilitaire léger, une API subscribe simple et une gestion attentive des subscribers, votre code reste flexible et maintenable.