Hello, everyone. We are the front end team of Unmanman. This time, we want to share with you how to solve the pain points of strong interaction and deep communication caused by data drive

Project and needs are ever-changing, especially for the strong interaction and some tools manufacture project, each component by event manager to manage their own business, just come by event emitters to trigger to monitor events in each component, also can call it a simple subscription model, similar to the central event in the vue, However, central events can only be subscribed and published through the $EMIT and $ON mode

Consider what situations do not apply to data-driven patterns, or are they difficult to maintain with a lot of data-driven, components interacting at multiple levels, or interacting across levels

Every graph is a component, and some tool projects are just like drawing on a canvas. The interaction is very scattered, the data span is too large, and the data does not have much correlation and presentation

How to deal with this through a data-driven model?

Each component needs to store a large amount of data at the top layer when interacting with each other. Each data format is different. Once the amount of data is large, it will be particularly complex and difficult to maintain

Just because spa applications wire each component into a large container, when the component is destroyed, the event manager should also be able to not only subscribe to publish each component, but also subscribe to destroy it

Here we reinforce the subscription publishing pattern by incubating each component’s own subscription state in the event bus, while simultaneously removing the subscriber of the currently destroyed component when its own component is destroyed

throughzmaWhat problem did we solve?

  1. Make the data more private and prevent the top energy source from storing large amounts of data and then relaying it
  2. Resolve the difficulty of communicating across hierarchies
  3. Distribute by simple subscription
  4. Each component maintains its own data, making it more maintainable
  5. When the group is destroyed, the total subscriber layer is destroyed at the same time, which improves the performance
  6. Compared to Vue central Event, it provides a large number of apis that can be applied to a variety of scenarios

Zma document use address feel applicable to their own project to a star oh! Thank you!

The code analysis

function zma() {const Manager = {// total subscriber store point events: {}, // total subscriber store point onceEvents: [], // Total subscriber name freezeEvents: [], // Subscription method on(type, fn) {}, // publish method fire(type. Rest) {}, // unbind(type, fn) {}, // subscribe once(type, fn) {}, // freeze the subscriber freezeEvent(type) {}, // unfreeze subscription clearFreezeEvent(type) {}, // clear all subscriptionsclear() {}, // generates a private subscriber for the current componentgetProxy() {
      returnNew ManagerProxy(this)},} // Return the subscriberreturn manager
}
Copy the code

Bus method and rough outline implementation

  • By performingZam functionReturns an object (total subscriber)
  • attributeeventsIs a total subscription store for all subscribers, including those who subscribe only once
  • attributeonceEventsIt is also a subscriber storage point, which, unlike Events, only holds subscribers who subscribe once
  • attributefreeeEventsIt’s a frozen list of subscribers who have been frozen
  • methodsonAll incoming subscribers are received, and in the incoming total subscriber, the list of subscribers and what to do are saved
  • methodsfireThrough the list of subscribers, publish the events that the subscribers have explained
  • methodsonceA subscriber who receives a subscription once, toonceEventsTo the list of subscribers, and then all subscription information to the master subscriber for record keeping
  • methodsunbindUnsubscribe certain subscribers based on incoming subscriber information
  • methodsfreezeEventTemporarily freeze the publication of subscribers based on the list of incoming subscribers
  • methodsclearFreezeEventResolve the publication of a frozen subscriber based on the incoming subscriber list
  • methodsclearClear all subscribers on the total subscriber
  • methodsgetProxyReturns a new instance object that incubates the respective internal subscribers of the current component, passing in the manager total subscriber as a parameter, and sharing all methods of the total subscriber

The entire body of the method is defined on the object of the total subscriber, and each component’s private subscriber implementation is hatched through getProxy

The call to getProxy incubates the subscribers that correspond to the internal management of each component of the group

function ManagerProxy(extendSolt) {
  this.extendSolt = extendSolt; // The upper layer of this points to the Manager object
  this.msgs = [];
}

ManagerProxy.prototype.on = function(type, fn) {
  const result = this.extendSolt.on(type, fn);
  if (result) {
    this.msgs.push([type, fn]); }}; ManagerProxy.prototype.fire =function(type) {
  this.extendSolt.fire(type);
};

ManagerProxy.prototype.once = function(type, fn) {
  const result = this.extendSolt.once(type, fn);
  if (result) {
    this.msgs.push([type, fn]); }}; ManagerProxy.prototype.unbind =function(type) {
  this.extendSolt.unbind(type);
};
ManagerProxy.prototype.freezeEvent = function(type) {
  this.extendSolt.freezeEvent(type);
};
ManagerProxy.prototype.clearFreezeEvent = function(type) {
  this.extendSolt.clearFreezeEvent(type);
};
ManagerProxy.prototype.dispose = function() {
  const msgs = this.msgs;
  let i = msgs.length;
  while (i--) {
    this.extendSolt.unbind(msgs[i][0], msgs[i][1]);
  }
  this.msgs = null;
  this.extendSolt = null;
};

Copy the code

Main process of private subscriber:

The ManagerProxy constructor new gives the current component a subscriber implementation, and all methods are attached to the prototype chain. By mounting to the prototype chain, all new implementations of the current component subscriber refer to the same reference, saving performance and memory

When this. ExtendSolt passes through the New ManagerProxy, it passes in the total subscriber object to each implementer in order to private the functional reuse of subscribers with the total subscriber and all components

This.msgs subscribs to all subscribers in the current component, including one subscriber subscriber

  • onCalls the methods of the total subscriber and, once the subscription is successful, logs the subscription to the current component subscriber
  • fireCalls the total subscriber’s fire method, both shared
  • onceThe total subscriber once method is called, and once the subscription is successful, the subscription is logged to the current component subscriber
  • unbind freezeEvent clearFreezeEventCall the same method to the total subscriber, they share
  • disposeLoop when all subscribers in the component to be destroyed, get the subscription name and the subscription event, call the unbind method, and perform a destruction of the current component subscription

The general structure is already clear, both from the general structure of the total subscriber, and from the original design of the private subscriber, where both the total subscriber and the private subscriber implement the same functional API, and the private subscriber has a self-destruct mechanism

Look inside from the surface

1. Subscription method

 manager = {
  events : {},
  on (type, fn) {
    if (isString(type) || isArray(type) && isFn(fn)) {
      if (isArray(type)) {// If the subscription names are passed in as an array, then the on method is called recursively until it is possible to subscribe togetherfor (let key of type) { this.on(key, fn); }}else{// Check the total subscription storage point to see if there is a subscription space for this subscription namelet eventHandler = this.events[type]; // If not, set the corresponding subscription space to an empty arrayif(! eventHandler) { eventHandler = this.events[type] = []; } // Obtain the length of the subscription space corresponding to the subscription name if it existsleti = eventHandler.length; // Make a loop to the subscription space. If the method refers to the same reference, only one copy is savedreturn false
        while (i--) {
          if (eventHandler[i] == fn) {
            return false; } // Pass the subscription method into the subscription space corresponding to the subscription name,return ture
        eventHandler.push(fn);
        return true; }}return false; }}Copy the code

With events objects wrapped around each subscriber, all subscription events are placed in an array so that all subscribers with the same name can be maintained

2. Subscribe once

  once(type, fn) {// Perform a parameter checkif (isString(typeConst result = this.on() &&isfunc (fn)) {const result = this.on();type, fn) // Once successful, store records to a subscription storage point and returntrue
      if (result) {
        this.onceEvents.push([type, fn])
        return true} // Otherwise returnfalse
      return false}}Copy the code

As you can see from the one-subscription method, although a subscription must be stored in the Events object, a subscription must also be logged to onceEvents

3. Freeze subscribers

freezeEvent(type) {// Make a validation for the parameterif (isString(type) || isArray(type)) {// If there are multiple event names, call freezeEvent recursivelyif (isArray(type)) {
      for (let key of type) { this.freezeEvent(key); }}else{// pass the subscription name this.freezeEvents. Push ()type); }}},Copy the code

Freeze subscriber, freeze from subscriber name, freeze the frozen subscriber name in freezeEvents, once frozen, frozen subscriber will not be published

4. Lift the freeze

clearFreezeEvent(type) {
  if (isString(type) || isArray(type)) {
    if (isArray(type)) {// Recursively unfreeze if it is an arrayfor (let key of type) { this.clearFreezeEvent(key); }}else{/ / by subscribing to name lookup const index = this. FreezeEvents. IndexOf (type); // If it is found, remove the original frozen name from the array and unfreeze itif(index >= 0) { this.freezeEvents.splice(index, 1); }}}},Copy the code

In some scenarios it is good to maintain subscribers by freezing, and not resubscription and unsubscribe is a priority, both in terms of performance and functionality!

Publish and subscribe

fire(type. rest) {if (isString(type) || isArray(type)) {
        if (isArray(type)) {// Recursively publish if it is an arrayfor (let key of type) {
            this.fire(key, ...rest);
          }
        } else{// Publish name can get the corresponding publish space through the total subscription storage pointlet emitEvent = this.events[type]; // If there is no publish space, or a subscription name is frozen, exit directlyif(! emitEvent || this.freezeEvents.indexOf(type) > = 0) {return false; } // Otherwise loop through the corresponding subscription publishing space, triggering the subscriber's subscription methodlet i = 0;
          while(i < emitEvent.length) { emitEvent[i](... rest); i++; } // In the same subscription, compare the total subscription to onceEvents and the same subscription to onceEvents, and delete the subscriber's subscription to prevent it from triggering again on the next publicationfor (let m = 0; m < this.onceEvents.length; m++) {
            const onceEvents = this.onceEvents;
            const curOnceEvents = onceEvents[m];
            const curEventType = this.events[type];
            if (curOnceEvents[0] === type) {
              for (let j = 0; j < curEventType.length; j++) {
                if (curOnceEvents[1] === curEventType[j]) {
                  curEventType.splice(j, 1);
                  onceEvents.splice(m, 1);
                  break; }}}} // After comparison, if the events for a subscriber does not have any subscribed events, the current subscriber will be removedif(! this.events[type].length) {
            delete this.events[type]; }}}},Copy the code

In the release, it is because the support of the same name to subscribe to, so to do release cycle, the name of each subscription subscription event, in a cycle, at the same time for a subscription in onceEvents points and total subscriptions are compared, and a if they exist, to remove the subscriber, because support subscriptions, with the same are probably the same subscription subscription only once, There may be some subscriptions with the same name that want to be subscribed more than others, but the event references are compared so that you can distinguish the subscriptions with the same name

6. Unsubscribe

unbind(type, fn) {
  if (isString(type) || isFunc(fn)) {
    if(! fn) { delete this.events[type];
    } elseConst eventHandler = this.events[const eventHandler = this.events[type]; // Get the length of the subscription spaceleteventHandlerL = eventHandler.length; // If there is a subscription spaceif(eventHandler && eventHandlerL) {// In some cases, there are two possibilities: the private subscriber needs to be destroyed completely at the component level, or the subscriber name needs to be destroyedif (fn) {
           while (eventHandlerL--) {
              if (eventHandler[eventHandlerL] == fn) {
                eventHandler.splice(eventHandlerL, 1);
                break; }}}else {
            delete this.events[type} // If a subscriber does not have any subscribed events, remove the current subscriberif(! eventHandlerL) { delete this.events[type]; }}}}},Copy the code

Unsubscribe to only can subscribe to cancel your subscription, it will put all the subscription subscription name space were all removed, it will also cause if is of the same name to subscribe to, not for the same subscription to a subscription to remove, so try to use different name to distinguish the difference between, if for the same event cancellation which want to subscribe to, In the second argument, the reference that was passed in to the binding, you take the event that was passed in, and you take the event that was passed in, and it’s the same reference when you bind and unbind, and you destroy it accordingly

7. Component destruction

ManagerProxy.prototype.dispose = function() {
  const msgs = this.msgs;
  let i = msgs.length;
  while (i--) {
    this.extendSolt.unbind(msgs[i][0], msgs[i][1]);
  }
  this.msgs = null;
  this.extendSolt = null;
};
Copy the code

MSGS, and then unbind to the total subscriber. This is associated with the second FN parameter in the unbind method. When the internal mechanism is called, Make a comparison search in the storage point of the total subscription to destroy subscribers in the currently executing component. Note that when used, the Dispose method must be called when the component is destroyed