The purpose of this article is to guide you to understand the common design patterns through some familiar scenes in your work, so that you no longer feel that design patterns are bitter and difficult to understand, or even difficult to start. By reading this article, I believe you will have an enlightened sense of design patterns.

Why study design patterns

Many people say things like “I’ve been working for three years and not learning design patterns doesn’t bother me at all”, or “I’ve read design pattern theory a few times and I don’t use it at work”. When it comes to design patterns, it’s a matter of opinion. Here are a few reasons why you should learn design patterns:

  • Improve your code. Design patterns follow the principles of single responsibility, reusability, and minimal knowledge. They make your code readable and reusable at the beginning of your design, and they also play an important role in refactoring your code. Can help us get rid of the tediousif... else...You can read the articles I’ve written beforeSummary of code optimization in the project — alternative if-else writing
  • Solve existing problems. Design pattern is the solution proposed by former people for some specific problems or scenarios in the process of software design and development. When you encounter a similar scenario during development, you can choose a design pattern as one of the solutions to the problem.
  • Enhance its soft power. Mastering design patterns will help you stand out from the crowd in an interview and make you think differently at work…

Design patterns

There are 23 design patterns proposed by GoF, but JavaScript is different from other strongly typed languages in that traditional design patterns are not completely applicable, which is why many people refuse to learn design patterns or find them bitter and difficult to understand. Based on our work, we will introduce only a few common design patterns in JavaScript.

While each of these design patterns has its own characteristics, keep in mind that the core of all of them is the same — encapsulating change. Identifying and encapsulating changes in our code makes our code more flexible and reusable.

1. Singleton mode

Definition: Ensures that a class has only one instance and provides a global access point to access it.

In short, one of the biggest characteristics of the singleton pattern is uniqueness, and the singleton pattern is used when considering global uniqueness, such as window objects in browsers, global caches, and so on.

Implement the singleton pattern

We first to interpret the definition of “a class has only one instance” this sentence, we all know that when we create a class that can generate a new object by new keywords (that is, an instance), from another point of view, show the class in the singleton pattern whether be instantiated by the new key word several times, should return to first create the instance objects.

With that in mind, the following code implements a singleton pattern:

class Singleton{
    getInstance(){
        // Check whether an instance has been created
        if(! Singleton.instance){// If not, a new instance is generated
            Singleton.instance = new Singleton()
        }
        // If yes, return the instance
        return Singleton.instance
    }
}

const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1===s2) // true
Copy the code

In the example above, we use singleton.instance to hold the instance created by Singleton, or generate a new instance if no instance has been created. We can see that no matter how many times singleton.getInstance () is executed, the result is the first instance created.

In JavaScript we don’t use class very often, do we not use the singleton pattern? This is not the only way to implement the singleton pattern. We can also implement it using closures:

Singleton.getInstance = (function() {
  // Define a free variable instance to simulate a private variable
  let instance = null
  return function() {
      // Check whether the free variable is null
      if(! instance) {// If null, the new instance is unique
          instance = new Singleton()
      }
      return instance
  }
})()
Copy the code

The core of the singleton pattern is to ensure that there is only one instance and to provide global access. Let’s take a look at two examples of the common singleton pattern at work to help us better understand the singleton pattern.

The singleton pattern in VUEX

Vuex is the state manager of VUE, and store in VUex is an application of the singleton pattern. An application contains only one store instance, so let’s look at vuex’s source code, which implements an install method that is called when the plug-in is installed. In the install method, there is a piece of logic very similar to the getInstance method we wrote above:

if(! Vue &&typeof window! = ='undefined' && window.Vue) {
  install(window.Vue)
}

function install (_Vue) {
  // Determine whether the incoming Vue instance object has already been installed by the Vuex plug-in
  if (Vue && _Vue === Vue) {
    console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.')
    return
  }
  // If not, install a unique Vuex for the Vue instance object
  Vue = _Vue
}
Copy the code

In this way, it is guaranteed that an application will install the Vuex plug-in only once, so each Vue instance will have only one global Store.

Dialog/modal popup window

At work we often encounter writing modal pop-ups (dialog pop-ups are similar), such as our login pop-ups. The features of the login popover are:

  • Is unique on the page, it is impossible to have two login pop-ups at the same time.
  • A popover is created when the user clicks the button for the first time. When it is triggered again, a new popover is not created, but the previous popover is displayed.

Based on this, we can implement a login popover demo:

<html>
<body>
  <button id="loginBtn">The login</button>
</body>
<script>
  const createLoginLayer = (function () {
    var div;
    return function () {
      if(! div) { div =document.createElement('div');
        div.innerHTML = 'I'm login popover';
        div.style.display = 'none';
        document.body.appendChild(div);
      }
      return div;
    }
  })();
  document.getElementById('loginBtn').onclick = function () {
    const loginLayer = createLoginLayer();
    loginLayer.style.display = 'block';
  };
</script>
</html>
Copy the code

2. Iterator pattern

Definition: Provides a way to access elements of an aggregate object sequentially without exposing the internal representation of the object.

The definition may be tricky, but we can start with the name. Array.prototype.forEach: array.prototype.foreach: array.prototype.foreach: array.prototype.foreach

[1.2.3].forEach((item, index) = > {
    console.log('Current element${item}The subscript for${index}`)})Copy the code

When we use array.prototype. forEach, we don’t care about the internal implementation of forEach, just the implementation of the business layer, which happens to be a characteristic of the iterator pattern. The iterator pattern separates the process of iterating from the business logic. After using the iterator pattern, each element of an object can be accessed sequentially, even without caring about its internal construction.

Implement an iterator

We can implement an iterator each:

const each = function (ary, callback) {
  for (let i = 0, l = ary.length; i < l; i++) {
    callback.call(ary[i], i, ary[i]); // Pass the subscript and element as arguments to the callback function}}; each([1.2.3].function (i, n) {
  console.log([i, n]);
});

Copy the code

Of course, iterators can be used in more ways than that, so I recommend reading the books and articles at the end of this article.

3. Decorator mode

Definition: Dynamically adds new behavior to an object.

Christmas is coming, many friends will decorate the Christmas tree, we will put a lot of festive decorations on the tree, but we will not destroy the original structure of the tree, this is the pattern of decorators in our life.

Decorative function

A good representation of the decorator pattern in JavaScript is the decorator function. For example, when we are maintaining a project, a new requirement comes along that requires us to add new functionality to the original function. The original function was written by a former colleague and several people’s hands, the implementation of which is very messy, the best way is to try not to change the original function, by saving the original reference to rewrite the function.

For example, if we want to bind the onLoad event to the window, but if the event has been bound before, we simply write it to override the behavior in the previous event. To avoid overwriting the behavior of the previous window.onload function, we usually save the old window.onload and place it in the new window.onload:

window.onload = function () {
  console('The action previously performed');
};

const _onload = window.onload || function () {};

window.onload = function () {
  _onload();
  console.log('Add new Behavior');
};
Copy the code

The new window.onload function is our decorator function.

Add new behavior to a function dynamically

There are two problems with this approach:

  • Must be maintained_onloadThis intermediate variable, if the function decorator chain is longer, needs more intermediate variables.
  • thisWhen a function is called as a method of an object,thisThe above example does not have this problem, but we should consider that the original function implementation doesthis).

To solve these two problems, we decorate functions with higher-order functions.

We add new behavior to the original function, either before or after the function is executed. We implement two methods — the Function. The prototype. Before to add Function preceded the new behavior of Function performs the Function. The prototype. After to the Function Function after adding new behavior:

Function.prototype.before = function (beforefn) {
  var __self = this; // Save a reference to the original function
  return function () {
    // Returns a "proxy" function that contains both the original function and the new function
    beforefn.apply(this.arguments); // Execute the new function and ensure that this is not hijacked
    // The new function is executed before the original function
    return __self.apply(this.arguments); // Execute the original function and return the result of the original function.
    // And make sure this is not hijacked
  };
};
Function.prototype.after = function (afterfn) {
  var __self = this;
  return function () {
    var ret = __self.apply(this.arguments);
    afterfn.apply(this.arguments);
    return ret;
  };
};
Copy the code

Going back to the window.onload example above, we could write:

window.onload = function () {
  console('The action previously performed');
};

window.onload = (window.onload || function () {}).after(function () {
  _onload();
  console.log('Add new Behavior');
}).after(function () {
  console.log('Continue adding other new behaviors');
});
Copy the code

Some events

If we add a buried event to the search button, we need to do two things, one is to implement the search function, and the other is to report the data.

// Common implementation
const btnCLick = () = > {
  console.log("Search function");
  console.log("Reported data");
};

// Implement the decorator pattern
const search = () = > {
  console.log("Search function");
};

const sendData = () = > {
  console.log("Reported data");
};

const btnCLick = search.after(sendData);
Copy the code

In the second implementation (decorator pattern implementation), we divided the button responsibilities more finely, preserving the principle of single responsibility for functions.

Axios requests with token arguments

Here is an AXIos request that we wrapped

var request = function(url, type, data){
  console.log(data);
  // The code is omitted
};
Copy the code

Now you need to add the token argument to each request

// Common implementation
var request = function(url, type, data = {}){
  data.token = getToken();
  console.log(data);
};

// Implement the decorator pattern
var request = function(url, type, data = {}){
  console.log(data);
};

request = request.before((url, type, data = {}) = >{
  data.token = getToken();
})

Copy the code

In the decorator mode implementation we do not change the original function, which is a cleaner single responsibility function, improving the reusability of the Request function.

4. Proxy mode

Definition: To provide a substitute for an ontology when direct access to the ontology is inconvenient or undesirable.

Proxy patterns are very common, such as image preloading, event bubbling, caching of results, etc. It’s surprising that some of the techniques we use all the time are actually using proxy patterns, so design patterns aren’t that far away.

Two examples are detailed below.

The event agent

Event broker is a situation that we often encounter. For example, if we want to print the contents of each li tag as we click on it, if we don’t use event broker, it will say:

<html>
<body>
  <ul id="parent">
    <li>List item 1</li>
    <li>List item 2</li>
    <li>List item 3</li>
    <li>List item 4</li>
    <li>List item 5</li>
  </ul>

  <script>
    const aNodes = document.getElementsByTagName('li')
    const aLength = aNodes.length
    for (let i = 0; i < aLength; i++) {
      aNodes[i].addEventListener('click'.function (e) {
        e.preventDefault()
        alert(I was `${aNodes[i].innerText}`)})}</script>
</body>
</html>
Copy the code

In this way, we add listening events to all five LI elements. In the actual project, there are many list elements, and the performance overhead of adding listening events to each list item is very high.

Further considering the bubbling nature of the event itself, we can use the proxy mode to implement the event listening on the child element. We just need to add the listening event to the parent element:

const element = document.getElementById('parent');
element.addEventListener('click'.(e) = > {
  const target = e.target;
  console.log(target.innerHTML);
})
Copy the code

The caching proxy

Cache interface data: store the returned results of the adjusted interface without having to call the interface to get data every time, reducing the pressure on the server. It is suitable for scenarios where the data is rarely updated.

const getData = (function() {
    const cache = {};
    return function(url) {
        if (cache[url]) {
            return Promise.resolve(cache[url]);
        }
        return $.ajax.get(url).then((res) = > {
            cache[url] = res;
            return res;
        }).catch(err= > console.error(err))
    }
})();

getData('/getData'); // Initiate an HTTP request
getData('/getData'); // Return cached data
Copy the code

5. Adapter mode

Definition: Used to solve the problem of incompatibility between interfaces.

There are many examples of adapters in our daily life. For example, the headphone jack of my iPhone 13 has a square head. In order to use round-head earphones, one end of the adaptor has a round hole for holding round-head earphones, and the other end has a square head for holding my phone. The adapter here acts as an adapter.

Similarly, we use the idea of adapter patterns when designing programs.

Compatible with interface data formats

Adapter mode is especially used in our daily work. For example, when we use Echarts to draw graphs, we all know that the data in Echarts has certain data format. For example, when we draw pie charts, the data format is as follows:

data = [
  {
    name: "2020".value: 14800}, {name: "2021".value: 23400,},];Copy the code

The format of the data we get from the back-end interface might look something like this:

data = {
  2020: 14800.2021: 23400};Copy the code

It is obviously impractical for us to change the Echarts source code to fit our data format. In this case, we need an adapter to convert the data format we get from the back end to the data format that the pie chart can accept:

function adpter(data) {
  const result = [];
  for (let key in data) {
    result.push({
      name: key,
      value: data[key],
    });
  }
  return result;
}
Copy the code

Cross-browser compatibility

We all know that event listeners have compatibility issues in browsers, so jQuery wraps an event handling adapter for $(‘selector’).on to address cross-browser compatibility issues.

function on(target, event, callback) {
    if (target.addEventListener) {
        // Standard event listener
        target.addEventListener(event, callback);
    } else if (target.attachEvent) {
        // Listen for events of earlier versions of IE
        target.attachEvent(event, callback)
    } else {
        // Lower version browser event listener
        target[`on${event}`] = callback
    }
}
Copy the code

The difference between decorator pattern and proxy pattern

All three patterns are wrapper patterns and do not change existing interfaces; the main difference is their intent.

model intentions Packing number
Decorator pattern Add functionality to an object Can be wrapped multiple times to form a long decorative chain
The proxy mode is To control access to objects It is usually packaged only once
Adapter mode To solve the problem of mismatch between two existing interfaces It is usually packaged only once

6. Strategic mode

Definition: Define a set of algorithms, encapsulate them one by one, and make them interchangeable.

For example, if you want to sort a set of data from smallest to largest, there are a number of algorithms you can use, you can use bubble sort, you can use quicksort, you can use heap sort. You can choose the sort algorithm you like, or you can look at the characteristics of the data and choose the sort algorithm that works best for you, and that’s the strategy pattern we’re talking about.

The algorithm here is a generalized “algorithm”, which can be a series of encapsulated “business rules”.

Page reuse

In one of our projects, we did add and edit pages, and in most cases they all looked the same. At present, there are four pages of view details, Single Add, batch add, and Edit details in our project. Their contents are the same, and the main differences are as follows:

  • View details: All form items can only be viewed, not edited. Click OK to close the page
  • Single add: Upload only one file. Click OK to invoke a single newly added interface. After the new interface is successfully added, a message “Added successfully” is displayed, and the page is closed and the list is refreshed
  • Batch Add: You can upload multiple files. Click OK to invoke the interface of batch add. After the batch add succeeds, a message “Batch add succeeded” is displayed, and the page is closed and the list is refreshed
  • Edit details: Backfill the details, modify the information and click OK to invoke the modify interface. After the modification is successful, a message “Modify succeeded” will pop up, close the page and refresh the list

Since all four pages have the same content, I assume that as an “efficient developer” you’re not stupid enough to write four pages that are the same, which means we need to implement a certain button click event on a public page, and we need to decide what to do based on the situation.

We agree on a variable to distinguish which public page is currently “playing” :

const source = ' ' 
// view to view details; Add Add a single. BatchAdd batchAdd. Edit Edit details
Copy the code

Without studying strategy patterns, your first thought might be to use if… else… To achieve:

document.getElementById("confirmBtn").addEventListener("click".() = > {
  if (source === "view") {
    console.log("Close the page");
  } else if (source === "add") {
    console.log("Call a single new interface");
    console.log("Other Operations");
  } else if (source === "batchAdd") {
    console.log("Call batch New Interface");
    console.log("Other Operations");
  } else if (source === "edit") {
    console.log("Call modify interface");
    console.log("Other Operations"); }});Copy the code

A pile of the if… else… It is very unfriendly to read and has low reusability.

In JavaScript, we can implement the policy pattern using the properties of the object.

document.getElementById("confirmBtn").addEventListener("click".() = > {
  const strategies = {
    view: () = > {
      console.log("Close the page");
    },
    add: () = > {
      console.log("Call a single new interface");
      console.log("Other Operations");
    },
    addBatch: () = > {
      console.log("Call batch New Interface");
      console.log("Other Operations");
    },
    edit: () = > {
      console.log("Call modify interface");
      console.log("Other Operations"); }}; strategies[source](); });Copy the code

7. Observer mode

Definition: defines a one-to-many dependency that allows multiple observer objects to listen to a target object at the same time. When the state of the target object changes, all observer objects will be notified, so that they can automatically update.

Vue data bidirectional binding principle

For those familiar with Vue, the observer mode is used in Vue v2.x’s bidirectional data binding principle. When we modify the data layer, the view layer will also be updated. For details, please refer to the in-depth introduction of responsive principle on Vue’s official website. Bidirectional binding principle and implementation of the detailed introduction, there have been many articles on the web, we recommend that you read vue bidirectional binding principle and implementation.

In the Vue bidirectional binding principle, there are three key objects, each of which has the following tasks:

  • Listener Observer: Hijacks and listens for all attributes and notifies subscribers of any changes.
  • Subscriber Watcher: Receives notification of property changes and executes corresponding functions to update the view.
  • Compiler: Scans and parses instructions for each node and initializes the corresponding subscriber based on initialization template data.

The coordination process among the three is as follows;

Event Bus Indicates the global Event Bus

In Vue, we often use the Event Bus to implement the communication component, strictly speaking, this is not the observer pattern, but released – subscriber model (hereafter, talk about the difference between between them), the two modes in the thought about, do a lot of books, so this example here, no longer speak alone — the subscriber model.

The Event Bus can be used as follows:

// Create an Event Bus (essentially a Vue instance) and export it:
const EventBus = new Vue()
export default EventBus

// Add EventBus to the main file and mount it globally:
import bus from 'File path for EventBus'
Vue.prototype.bus = bus

// Subscribe to events:
this.bus.$on('someEvent', func) // func refers to the listener function for someEvent

// Publish (trigger) events:
this.bus.$emit('someEvent', params) // Params refers to the input parameter received by the callback function when the event someEvent is raised
Copy the code

You can see that there are no obvious publishers and subscribers in the whole process, only one BUS is coordinating. This is the nature of the global event bus – all publish/subscribe operations for events must go through the event center, with no “sweetheart deals”!

Let’s implement an Event Bus:

class EventEmitter {
  constructor() {
    Handlers are a map that stores the mapping between events and callbacks
    this.handlers = {}
  }

  The // on method is used to install an event listener, which takes a target event name and a callback function as parameters
  on(eventName, cb) {
    // Check whether the target event name has a listener queue
    if (!this.handlers[eventName]) {
      // If not, initialize a listener queue first
      this.handlers[eventName] = []
    }

    // Push the callback into the listener queue of the target event
    this.handlers[eventName].push(cb)
  }

  The emit method is used to fire the target event, which takes the event name and listener function input arguments
  emit(eventName, ... args) {
    // Check whether the target event has a listener queue
    if (this.handlers[eventName]) {
      // A shallow copy is made to avoid sequential problems during removal of listeners installed through once
      const handlers = this.handlers[eventName].slice()
      // If so, call each of the queue's callback functions one by one
      handlers.forEach((callback) = >{ callback(... args) }) } }// Remove the specified callback function from an event callback queue
  off(eventName, cb) {
    const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(cb)
    if(index ! = = -1) {
      callbacks.splice(index, 1)}}// Register a single listener for the event
  once(eventName, cb) {
    // Wrap the callback function so that it is automatically removed after execution
    const wrapper = (. args) = >{ cb(... args)this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }
}
Copy the code

The difference between the observer model and the publisher-subscriber model

I have stepped on this knowledge. Most books refer to publish-subscribe as the observer model, including JavaScript Design Patterns and Development Practices. After I went to understand, in fact, there is a difference between the two.

  • Observer mode: Direct contact with the object being observed. The coupling problem between modules is reduced. Two separate and unrelated modules can also communicate, but they are not completely decouple. The observed must maintain a set of observers, and the observers must implement a unified method for the called by the observed.

  • Publish subscriber model: There is a third party platform for publish/subscribe. Complete decoupling is achieved, with registration and triggering independent of both third platforms.

Publishers do not directly touch subscribers, but a unified third party does the actual communication, called the publish-subscribe model. The difference between the observer model and the publish-subscribe model is the presence of a third party and the ability of the publisher to directly perceive the subscriber (see figure).

conclusion

This article does not cover design patterns very deeply. The main purpose of this article is to take you through some scenarios in which design patterns are used so that you are no longer unfamiliar with them. Then follow these examples to understand the definition of design patterns and believe that there will be a different feeling. After reading this, promise me that next time someone asks you about design patterns, don’t say you don’t know, okay?

reference

  • “JavaScript Design Patterns and Development Practices”, once visited: easy to understand, read several times, each time I will learn something new
  • Digging gold course JavaScript design mode core principle and application practice: repair big writing humor, explain very image, the article some examples will come from here