js design mode [detailed explanation] - decorator mode

Posted by BigToach on Sat, 19 Feb 2022 09:33:44 +0100

catalogue

Definition of decorator mode

Configure development environment

Demonstration example -- decorator @ autobind # implements this to point to the original object

Demonstration example -- decorator @ debounce # realizes function anti shake

Demonstration example -- decorator @ predict implements warning prompt

Definition of decorator mode

Decorator Pattern: a design pattern that adds new functionality to an existing object without changing its structure

Purpose: add new functions to existing objects

If you want to use a lot of decorators in the project, it is recommended to have a look core-decorators This library encapsulates many common decorators

Configure development environment

At present, the syntax of Decorator is only a proposal. If you want to use Decorator mode now, you need to install babel + webpack and implement it in combination with plug-ins.

  • npm installation dependency
npm install babel-core babel-loader babel-plugin-transform-decorators babel-plugin-transform-decorators-legacy babel-preset-env
  • to configure. babelrc file
{
  "presets": ["env"],
  "plugins": ["transform-decorators-legacy"]
}
  • On webpack config. Add Babel loader in JS
  module: {
    rules: [
      { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
    ],
  }

If the IDE you are using is Visual Studio Code, you may also need to add the following tsconfig. In the root directory of the project JSON file to organize an error report of ts check.

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "allowJs": true,
    "lib": [
      "es6"
    ],
  }
}

Demonstration example -- decorator @ autobind # implements this to point to the original object

Implement an autobind function to decorate the getPerson method and realize that this always points to the instance of Person.

function autobind(target, key, descriptor) {
  var fn = descriptor.value;
  var configurable = descriptor.configurable;
  var enumerable = descriptor.enumerable;

  // Return to descriptor
  return {
    configurable: configurable,
    enumerable: enumerable,
    get: function get() {
      // Bind the method to this
      var boundFn = fn.bind(this);
      // Use object Defineproperty redefines the method
      Object.defineProperty(this, key, {
        configurable: true,
        writable: true,
        enumerable: false,
        value: boundFn
      })

      return boundFn;
    }
  }
}

The binding of this is realized through bind, and object is used in get Defineproperty rewrites this method and defines value as the function boundFn bound through bind, so as to realize that this always points to the instance. Let's decorate the getPerson method and call it.

class Person {
  @autobind
  getPerson() {
    return this;
  }
}

let person = new Person();
let { getPerson } = person;

console.log(getPerson() === person); // true

Demonstration example -- decorator @ debounce # realizes function anti shake

Function debounce has many applications in front-end projects. For example, operating DOM in events such as resize or scroll, or realizing real-time ajax search for user input, will be triggered by high frequency. The former will have an intuitive impact on browser performance, and the latter will put great pressure on the server, We expect this kind of high-frequency continuous trigger event to respond after the trigger is completed, which is the application of functional anti shake.

class Editor {
  constructor() {
    this.content = '';
  }

  updateContent(content) {
    console.log(content);
    this.content = content;
    // There are some performance consuming operations
  }
}

const editor1 = new Editor();
editor1.updateContent(1);
setTimeout(() => { editor1.updateContent(2); }, 400);


const editor2= new Editor();
editor2.updateContent(3);
setTimeout(() => { editor2.updateContent(4); }, 600);

// Print result: 1 3 2 4

In the above code, we defined the Editor class, in which the updateContent method will execute when the user inputs and may have some performance consuming DOM operations. Here, we print the passed in parameters inside the method to verify the calling process. You can see that the results of four calls are 1, 3, 2 and 4 respectively.

Next, we implement a debounce function, which passes in a timeout parameter of numeric type.

function debounce(timeout) {
  const instanceMap = new Map(); // Create a Map data structure and use the instantiated object as the key

  return function (target, key, descriptor) {

    return Object.assign({}, descriptor, {
      value: function value() {

        // Clear delay
        clearTimeout(instanceMap.get(this));
        // Delay setting
        instanceMap.set(this, setTimeout(() => {
          // Call this method
          descriptor.value.apply(this, arguments);
          // Set delayer to null
          instanceMap.set(this, null);
        }, timeout));
      }
    })
  }
}

In the above method, we use the Map data structure provided by ES6 to realize the mapping of instantiated objects and delayers. Inside the function, first clear the delayer, and then set the delay execution function. This is a general method to realize debounce. Let's test the debounce decorator.

class Editor {
  constructor() {
    this.content = '';
  }

  @debounce(500)  
  updateContent(content) {
    console.log(content);
    this.content = content;
  }
}

const editor1 = new Editor();
editor1.updateContent(1);
setTimeout(() => { editor1.updateContent(2); }, 400);


const editor2= new Editor();
editor2.updateContent(3);
setTimeout(() => { editor2.updateContent(4); }, 600);

//Print result: 3 2 4

The updateContent method has been called four times above, and the print result is 3 2 4. 1 was repeatedly called and not printed within 400ms, which is in line with our expectation that the parameter is 500.

Demonstration example -- decorator @ predict implements warning prompt

In the process of using third-party libraries, we will encounter some warnings on the console from time to time, which are used to remind developers that the methods called will be discarded in the next version. Perhaps our normal practice is to add a line of code inside the method. In fact, it is not friendly to read the source code and does not comply with the principle of single responsibility. If the implementation of the @ precate decorator needs to throw more than one warning in front of it.

Now let's implement a decorator of @ decode. In fact, this kind of decorator can also be extended to print log decorator @ log, report information decorator @ fetchInfo, etc.

function deprecate(deprecatedObj) {

  return function(target, key, descriptor) {
    const deprecatedInfo = deprecatedObj.info;
    const deprecatedUrl = deprecatedObj.url;
    // Warning message
    const txt = `DEPRECATION ${target.constructor.name}#${key}: ${deprecatedInfo}. ${deprecatedUrl ? 'See '+ deprecatedUrl + ' for more detail' : ''}`;
    
    return Object.assign({}, descriptor, {
      value: function value() {
        // Print warning message
        console.warn(txt);
        descriptor.value.apply(this, arguments);
      }
    })
  }
}

The decode function above accepts an object parameter, which has two key values: info and url, where info is filled with warning information and url is the optional detailed web page address. Now let's add this decorator to the deprecate dmethod method of a library named MyLib!

class MyLib {
  @deprecate({
    info: 'The methods will be deprecated in next version', 
    url: 'http://www.baidu.com'
  })
  deprecatedMethod(txt) {
    console.log(txt)
  }
}

const lib = new MyLib();
lib.deprecatedMethod('A method to be removed in the next version was called');
// DEPRECATION MyLib#deprecatedMethod: The methods will be deprecated in next version. See http://www.baidu.com for more detail
// A method to be removed in the next version was called

 

Topics: Javascript