Take you with a practical hook module

The hook pattern is often used in our daily code. For example, we define a series of events and then notify the handler of these events to handle them one by one when needed.

Essentially, hook pattern is similar to responsibility chain pattern. hook establishes a one-to-many relationship between specific events and event handlers. When events need to be handled, they are executed one by one along the event handler chain. But different designs may have different processing behavior. In this article, the handler in front of the chain has the right to decide whether it needs to be handed over to the next handler for processing or to return the final result directly.

The principle of opening and closing is followed here: opening to expansion and closing to modification.

This means that in hook mode, once there is a new event handling requirement, we just need to register the event handling function to process the event according to the established behavior, and we don't need to change the hook framework.

Let's first look at how hook frameworks are created and what functional interfaces they have:

function createHook() {
  const _hooks = {};
  
  function register(key, fn) {...}
  
  function isRegistered(key) {...}
  
  function syncExec(key, ...args) {...}
  
  function asyncExec(key, ...args) {...}
  
  function unregisterAll(key) {...}
  
  return {
    register,
    isRegistered,
    syncExec,
    asyncExec,
    unregisterAll
  };
}

This is an overview of the hook framework, which we use const hook = createHook(); create an instance of the hook object to use. Next, we will analyze the implementation and use of each function one by one.

register(key, fn)

The function registers event handling -- key is the event name string and fn is the event handler. It is important to note that fn needs to be a higher order function in the following format:

const fn = function (next) {
  return function (...args) {
    return next({...});
    // return {...}
  }
};

Next injected here is a function: calling next means calling the next processing function to process, and returning the processing result directly if it is not needed, so the processing function in the front position has the option.

Back to the register function, the implementation is as follows:

function register(key, fn) {
  if (!_hooks[key]) {
    _hooks[key] = [];
  }
  _hooks[key].push(fn);

  return function unregister() {
    const fns = _hooks[key];
    const idx = fns.indexOf(fn);
    fns.splice(idx, 1);
    if (fns.length === 0) {
      delete _hooks[key];
    }
  };
}

The registration logic is simple, _hooks is an object that records the relationship between the event name and the event handler, because there are many and they are executed in registration order, so each key attribute is an array.

It's worth noting here that the register function returns an unregister function, which is used to cancel event processing. The purpose of this is that the user does not need to record the reference of fn, but when subsequently needs to cancel, retain unregister and use it when appropriate.

isRegistered(key)

isRegistered is used to determine whether an event has a corresponding handler. The content is simple:

function isRegistered(key) {
  return (_hooks[key] || []).length > 0;
}

syncExec(key, ...args)

syncExec is a chain of processing functions used to synchronize the execution of an event and return the processing results. The following examples are used:

const hook = createHook();

// First Processing Function
hook.register('process-test', function (next) {
  return function (num) {
    return next(num + 1);
  };
});
// The second processing function
hook.register('process-test', function (next) {
  return function (num) {
    return next(num + 2);
  };
});

const rst = hook.syncExec('process-test', 1);
// The result of rst is 4

As shown in the example, each processing function passes its own processing result to the next processing function through next, or return s the final result directly.

Let's look at the implementation of syncExec:

function syncExec(key, ...args) {
  const fns = (_hooks[key] || []).slice();

  let idx = 0;
  const next = function (...args) {
    if (idx >= fns.length) {
      return (args.length > 1 ? args : args[0]);
    } else {
      const fn = fns[idx++].call(this, next.bind(this));
      return fn.apply(this, args);
    }
  };

  return next.apply(this, args);
}

First, it is worth noting that slice() is used to copy an array of processing functions corresponding to a key in order to prevent the original array from changing during execution. Then a next function is constructed and injected into the event handler (remember that the event handler is a higher-order function?) And record the index idx of the next event handler to be executed through the closure. Then through the first next call, the entire processing chain starts to work.

A special logic here is the formatting of the returned results. If next passes multiple parameters, the return result is an array containing all the parameters; if next passes a single parameter, the return result is that parameter.

asyncExec(key, ...args)

Since there is synchronous execution, we also need asynchronous execution. Examples of asyncExec's use are as follows:

const hook = createHook();

hook.register('process-test', function (next) {
  return function (obj) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(next({ count: obj.count + 1 }));
      }, 100);
    });
  };
});
hook.register('process-test', function (next) {
  return function (obj) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(next({ count: obj.count + 2 }));
      }, 100);
    });
  };
});

hook.asyncExec('process-test', { count: 66 }).then(rst => {
  console.log(rst);
  // rst === 69
});

When the event handler needs to be executed asynchronously, we can call asyncExec to handle it, and even if the return of the event handler is synchronous, such as the return of the first handler above ({count: obj. count + 1}); it is also possible.

Let's look at how asyncExec is implemented:

function asyncExec(key, ...args) {
  const fns = (_hooks[key] || []).slice();
  let idx = 0;

  const next = function (...args) {
    if (idx >= fns.length) {
      return Promise.resolve(args.length > 1 ? args : args[0]);
    } else {
      try {
        const fn = fns[idx++].call(this, next.bind(this));
        const rst = fn.apply(this, args);
        return Promise.resolve(rst);
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };

  return next.apply(this, args);
}

The core idea here is that if the parameter of resolve is Promise, then the status of the original Promise is determined by the inner Promise, and unfamiliar children's shoes can review Promiseha. Another thing to note here is that exceptions thrown when an event handler is executed are handled. Then the processing of returned results is the same logic as syncExec.

unregisterAll(key)

The last unregisterAll is very simple, canceling all handlers on an event:

function unregisterAll(key) {
  delete _hooks[key];
}

summary

Above is the whole analysis of hook module, the source code is in Here . For more detailed use cases, please refer to unit testing.

I think some children's shoes may ask, "Why not write Hook as a Class, and then the business system inherits hook to use, so that the Hook function can not be inherited into business objects?"

The main consideration here is to follow another design principle: use more combinations and use less inheritance. Sometimes the combination approach is more flexible, so if we want to inherit hook s into our business objects, the simple point is to do the following:

const app = {...};
const hook = createHook();

// mixin
Object.assign(app, hook);

If you have any questions or suggestions about this article, you are welcome to Here Put forward.

Welcome to star and pay attention to my JS blog: Whisper than Javascript

Tags: Javascript Attribute less

Posted on Wed, 11 Sep 2019 21:04:14 -0700 by jcinsov