JavaScript Design pattern learning and practice (3)

This article summarizes the iterator pattern and publish-subscribe pattern. These two patterns are relatively classic patterns, even to the extent that the syntax itself supports them.

Iterator pattern

Definition

The iterator pattern provides a way to sequentially access the elements of an aggregate object without exposing the internal representation of the object. The iterator pattern can separate the iterative process from business logic. After using the iterator pattern, each element of the object can be accessed sequentially even if the internal structure of the object is not concerned.

For example, we define a data structure, the internal structure is a queue with a linked list, only need to provide a method to traverse the queue on the line, do not need to let the outside know what kind of technology is used to achieve the queue.

Internal iterator and external iterator

Let’s implement an iterator for each function that takes two parameters, the first bit is the array that is looping, and the second is the callback function that will be fired after each step in the loop:

1
2
3
4
5
6
7
8
9
var each = function(arr, callback) {
for(var i = 0, l = arr.length; i < l; i++) {
callback.call(arr[i], i, arr[i])
}
}

each([1,2,3], function(index, n)) {
console.log(index, n);
}

This each function belongs to the internal iterator. The internal iteration rules of each function have been defined and completely take over the entire iterative process. The external only needs an initial call.

Internal iterators are very convenient when called. The outside world does not care about the internal implementation of the iterator. The interaction with the iterator is only an initial call, but this also happens to be the disadvantage of internal iterators.

Since the iteration rules of the internal iterators have been defined in advance, each function above cannot iterate two arrays at the same time.

For example, there is now a requirement to compare whether the elements of two arrays are exactly the same. If we do not rewrite each function, we can start with only the callback function of each. The code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var compare = function(arr1, arr2) {
if (arr1.length ! arr2.length) {
throw new Error("arr1 and arr2 is not equal")
}

each(arr1, function(i, n) {
if (n ! arr2[i]) {
throw new Error("arr1 and arr2 is not equal")
}
})

alert("arr1 and arr2 is equal")
}

compare([1,2,3],[1,2,4])

In some languages without closures, the implementation of the internal iterator itself is also quite complex. For example, the internal iterator in C language is implemented using function pointers, and the loop-processed functions must be explicitly passed in from the outside in the form of parameters.

In contrast to internal iterators, external iterators must explicitly request iteration of the next element.

External iterators increase the complexity of some calls, but also increase the flexibility of iterators. We can manually control the process and order of iterations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var Iterator = function(obj) {
var current = 0;

var next = function() {
current++;
}

var isDone = function() {
current >= obj.length;
}

var getCurrent = function() {
return obj[current];
}

return {
next,
isDone,
getCurrent,
}
}

var compare = function(iterator1, iterator2) {
while(!iterator1.isDone() && !iterator2.isDone()) {
if (iterator1.getCurrent() ! iterator2.getCurrent()) {
throw new Error("iterator1 and iterator2 is not equal")
}
iterator1.next();
iterator2.next();
}

if (iterator1.isDone() && iterator2.isDone()) {
alert('iterator1 and iterator2 is equal');
} else {
throw new Error('iterator1 and iterator2 is not equal');
}
}

compare(Iterator([1,2,3]), Iterator([1,2,3]));

Abort iterator

Iterators can provide a way to get out of the loop, just like break in a normal for loop. For example:

1
2
3
4
5
6
7
8
9
10
11
12
var each = function(arr, callback) {
for(var i = 0, l = arr.length; i < l; i++) {
if (callback.call(arr[i], i, arr[i]) = false) {
break;
}
}
}

each([1,2,3,4,5,6], function(i, n) {
if (n > 3) return false;
console.log(n);
})

Example of Iterator Pattern

Assuming you have the following code, you can get different upload component objects according to different browsers:

1
2
3
4
5
6
7
8
9
10
11
12
13
var getUploadObj = function() {
try {
return new ActiveXObject("TXFINActiveX.FTNUpload");
} catch(e) {
if (supportFlash()) {
var str = "<object type='application/x-shockwave-flash'></object>"
return $(str).appendTo($('body'));
} else {
var str = "<input name='file' type='file'/>"
return $(str).appendTo($('body'))
}
}
}

Let’s modify the above code with iterator pattern, encapsulating each method of obtaining uploaded components as a function, and then see these functions put into an array to iterate until one can return correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var iteratorUploadObj = function() {
const uploadObjCreators = Array.prototype.slice(arguments);
for(var i = 0; i < uploadObjCreators.length; i++) {
var uploadObj = uploadObjCreators[i]();
if (uploadObj ! false) {
return uploadObj;
}
}
}

var getActiveUploadObj = function() {
try {
return new ActiveXObject("TXFINActiveX.FTNUpload");
} catch(e) {
return false;
}
}

var getFlashUploadObj = function() {
if (supportFlash()) {
var str = "<object type='application/x-shockwave-flash'></object>"
return $(str).appendTo($('body'));
} else {
return false;
}
}

var getFormUploadObj = function() {
var str = "<input name='file' type='file'/>"
return $(str).appendTo($('body'))
}

var uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj);

Publish-subscribe model

The publish-subscribe pattern, also known as the observer pattern, defines a one-to-many dependency relationship between objects. When the state of an object changes, all objects that depend on it will be notified.

In JavaScript, we generally use the event pattern instead of the publish-subscribe pattern.

The publish-subscribe pattern can be widely used in asynchronous programming, which is an alternative to passing callback functions. At the same time, the publish-subscribe pattern can replace the hardcoding notification mechanism between objects, where one object no longer explicitly calls an interface of another object. The publish-subscribe pattern allows two objects to be loosely coupled together.

DOM events

In fact, as long as we have bound event functions to DOM nodes, we have used the publish-subscribe pattern.

1
2
3
4
5
document.body.addEventListener('click', function() {
alert(2);
}, false)

document.body.click();

Custom events

In addition to DOM events, we often implement custom events. This publish-subscribe pattern that relies on custom events can be used in any JavaScript code.

Let’s see how to implement the publish-subscribe model step by step

  • First, specify who will act as the publisher
  • then add a cache list to the publisher for the callback function to notify subscribers
  • When the last message is published, the publisher will traverse this cache list and trigger the subscriber callback function stored in it in turn

In addition, we can also fill some parameters into the callback function, and the subscriber can receive these parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var salesOffices = {}; 

salesOffices.clientList = [];

salesOffices.listen = function(fn) {
this.clientList.push(fn);
}

salesOffices.trigger = function() {
for(const fn of this.clientList) {
fn.apply(this, arguments)
}
}

salesOffices.listen(function(price, squareMeter) {
console.log("A know:", price, squareMeter);
})

salesOffices.listen(function(price, squareMeter) {
console.log("B know:", price, squareMeter);
})

salesOffices.trigger(2000000, 80);
salesOffices.trigger(3000000, 110);

So far, we have implemented a simple publish-subscribe model, but there are still some problems here. We see that the subscriber receives every message released by the publisher. Although A only wants to buy a house of 88 square meters, the publisher will also push the message of 110 houses to A, which is not necessary, so we can optimize it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var salesOffices = {}; 

salesOffices.clientList = {};

salesOffices.listen = function(key, fn) {
if (!this.clientList[key]) {
this.clentList[key] = [];
}
this.clentList[key].push(fn);
}

salesOffices.trigger = function() {
const key = Array.prototype.shift.call(arguments);
const fns = this.clentList[key];

if (!fns || fns.length = 0) {
return false;
}
for(const fn of fns) {
fn.apply(this, arguments)
}
}

salesOffices.listen('squareMeter80', function(price) {
console.log("A know:", price);
})

salesOffices.listen('squareMeter110', function(price) {
console.log("B know:", price);
})

salesOffices.trigger('squareMeter80', 2000000);
salesOffices.trigger('squareMeter110', 3000000);

General Implementation of Publish Subscribe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var event = {
clientList: {},
listen: function(key, fn) {
if (!this.clientList[key]) {
this.clentList[key] = [];
}
this.clentList[key].push(fn);
},
trigger: function() {
const key = Array.prototype.shift.call(arguments);
const fns = this.clentList[key];

if (!fns || fns.length = 0) {
return false;
}
for(const fn of fns) {
fn.apply(this, arguments)
}
}
}

Install publish-subscribe mode for any object
var installEvent = function(obj) {
for(var i in event) {
obj[i] = event[i];
}
}

Now we can

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var salesOffices = {}; 

installEvent(salesOffices);

salesOffices.listen('squareMeter80', function(price) {
console.log("A know:", price);
})

salesOffices.listen('squareMeter110', function(price) {
console.log("B know:", price);
})

salesOffices.trigger('squareMeter80', 2000000);
salesOffices.trigger('squareMeter110', 3000000);

Of course, we can also publish first, then subscribe, cache the message when there are no subscribers for the time being, and call the subscribers in turn once they appear and clear the message