w字总结《JavaScript设计模式与开发实践》(设计模式)(上)

设计模式(上)

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点

实现单例模式

举个最常用的例子,我们登录的时候,页面中会出现一个登录浮窗,而它是唯一的,无论点多少次登录按钮,浮窗只会被创建一次,不会每次点击都创建新的实例。

简易的单例模式实现起来并不复杂,无非就是用一个变量来记录当前是否已为该类创建过对象,是则在下次获取该类实例时直接返回之前创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Singleton = function( name ){ 
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function(){
alert ( this.name );
};
Singleton.getInstance = function( name ){
if ( !this.instance ){
this.instance = new Singleton( name );
}
return this.instance;
};
var a = Singleton.getInstance( 'sven1' );
var b = Singleton.getInstance( 'sven2' );
console.log( a === b ); // true

代码很简单,但是这样的写法意义不大,因为类是“不透明”的,使用者必须知道这是一个单例类,且需要使用Singleton.getInstance获取对象。

透明的单例模式(代理)

我们需要做的是能够实现一个透明的单例类,使用方式像其他任何普通类一样且足够灵活。

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
var CreateDiv = function( html ) {
this.html = html
this.init()
}
CreateDiv.prototype.init = function() {
var div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}

// 代理类
var ProxySingletonCreateDiv = (function() {
var instance
return function(html) {
if(!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()

var a = new ProxySingletonCreateDiv('a')
var b = new ProxySingletonCreateDiv('b')

console.log(a === b)

上述代码中我们通过代理类ProxySingletonCreateDiv实现了对CreateDiv的单例化(代理模式,在后面介绍),与之前不同的是,CreateDiv变成了普通的类,如果业务场景中需要创建多个实例时,我们可以直接使用CreateDiv创建。二者组合达到了单例模式的效果。

惰性单例

顾名思义,惰性单例只有在需要的时候才创建对象实例。实现思路如最开始,用一个变量来标志是否创建过对象,是则返回创建好的对象。

1
2
3
4
5
6
var getSingle = function(fn) {
var result
return function() {
return result || (result = fn.apply(this,arguments))
}
}

这里结合了基础篇中的许多知识,我们可以传入任何方法,之后让getSingle返回一个新的函数,并用result保存fn的计算结果,因为result在闭包中,它永远不会被销毁,以后再请求时,如果result存在,那么它将返回这个值。

策略模式

定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式的目的就是将算法的使用与算法的实现分离。

计算奖金

书里举了一个计算奖金的例子。

很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S 的人年终奖有 4 倍工资,绩效为 A的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

在这个例子中,算法的使用方法不变,目的都是为了取得计算后的奖金,而算法的实现不一定相同,每种绩效对应不同计算规则。

基于策略模式的程序由两部分组成,第一部分是策略类,其中封装具体算法,并负责具体计算过程。第二部分是环境类,负责接受客户请求,并把请求委托给某个策略类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var strategies = {
'S': function(num) {
return num * 4
},
'A': function(num) {
return num * 3
},
'B': function(num) {
return num * 2
},
}
//
var calculate = function(level , num) {
return strategies[level](num)
}

console.log(calculate('S' , 4));
console.log(calculate('A' , 3));

多态在策略模式中的体现

通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在Context中,而是分布在各个策略对象中。Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换 Context 中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。

策略模式优缺点

  • 优点

    • 策略模式利用组合、委托、多态等技术及思想,避免多重条件选择语句。
    • 策略模式完美支持开放-封闭原则,将算法封装在strategy中,易于切换、理解和扩展。
    • 策略模式中的算法也可以复用在其他地方。
    • 策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
  • 缺点

    • 在程序中添加许多策略类或策略对象
    • 必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy、

代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

送花问题

在四月一个晴朗的早晨,小明遇见了他的百分百女孩,我们暂且称呼小明的女神为A。两天之后,小明决定给A送一束花来表白。刚好小明打听到 A 和他有一个共同的朋友B,于是内向的小明决定让B来代替自己完成送花这件事情。当A在心情好的时候收到花,小明表白成功的几率有60%,而当 A 在心情差的时候收到花,小明表白的成功率无限趋近于0。

这时候需要用到代理模式,小明无法得知A的心情,而B却了解,把花交给B,B会监听A的心情变化,选择心情好的时候再转交。

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
var Flower = function(){}

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower()
target.receiveFlower(flower)
}
}

var B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
A.receiveFlower(flower)
})
}
}

var A = {
receiveFlower: function(flower) {
console.log('收到花', flower)
},
listenGoodMood: function( fn ) {
setTimeout(function() {
fn()
},1000)
}
}

xiaoming.sendFlower(B)

保护代理和虚拟代理

保护代理:代理B可以帮助A过滤掉一些请求,不符合要求的请求在代理B处被拒绝掉,如上文的例子。

虚拟代理:把操作交给代理B去执行,代理B决定何时去执行操作。虚拟操作把一些开销很大的对象延迟到真正需要的时候再创建。

1
2
3
4
5
6
7
8
9
var B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
var flower = new Flower()
A.receiveFlower(flower)
})
}
}

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var mult = function(){ 
console.log( '开始计算乘积' );
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
mult( 2, 3 ); // 输出:6
mult( 2, 3, 4 ); // 输出:24
var proxyMult = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( args in cache ){
return cache[ args ];
}
return cache[ args ] = mult.apply( this, arguments );
}
})();
proxyMult( 1, 2, 3, 4 ); // 输出:24
proxyMult( 1, 2, 3, 4 ); // 输出:24

高阶函数动态创建代理

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 mult = function(){
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
/**************** 计算加和 *****************/
var plus = function(){
var a = 0;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a + arguments[i];
}
return a;
};
/**************** 创建缓存代理的工厂 *****************/
var createProxyFactory = function( fn ){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( args in cache ){
return cache[ args ];
}
return cache[ args ] = fn.apply( this, arguments );
}
};
var proxyMult = createProxyFactory( mult ),
proxyPlus = createProxyFactory( plus );
console.log( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
console.log( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
console.log( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
console.log( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10

迭代器模式

提供一种方法顺序访问一个聚合对象中的各元素,而又不需要暴露该对象的内部表示。

迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。简单来说就是统一“集合”型数据结构的遍历接口,实现可循环遍历获取集合中各数据项(不关心数据项的数据结构)。

实现自己的迭代器

接受两个参数:被循环数组、循环每一步后触发的回调

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

var arr = [1,2,3]
each(arr , function(i , n)) {
console.log([i , n])
}

内部迭代器和外部迭代器

  1. 内部迭代器

我们刚刚编写的 each 函数属于内部迭代器,each函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅仅是一次初始调用,但这也刚好是内部迭代器的缺点。由于内部迭代器的迭代规则已经被提前规定,上面的each函数就无法同时迭代2个数组了。比如现在有个需求,要判断 2 个数组里元素的值是否完全相等, 如果不改写each函数本身的代码,我们能够入手的地方似乎只剩下each的回调函数了。

1
2
3
4
5
6
7
8
9
10
11
12
var compare = function( ary1, ary2 ){ 
if ( ary1.length !== ary2.length ){
throw new Error ( 'ary1 和 ary2 不相等' );
}
each( ary1, function( i, n ){
if ( n !== ary2[ i ] ){
throw new Error ( 'ary1 和 ary2 不相等' );
}
});
alert ( 'ary1 和 ary2 相等' );
};
compare( [ 1, 2, 3 ], [ 1, 2, 4 ] ); // throw new Error ( 'ary1 和 ary2 不相等' );
  1. 外部迭代器

外部迭代器必须显式地请求迭代下一个元素。
外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。外部迭代器虽然调用方式复杂,但适用面更广,更能满足多变的需求。

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
var Iterator = function( obj ){ 
var current = 0;
var next = function(){
current += 1;
};
var isDone = function(){
return current >= obj.length;
};
var getCurrItem = function(){
return obj[ current ];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem
}
};

// 改写compare函数

var compare = function( iterator1, iterator2 ){
while( !iterator1.isDone() && !iterator2.isDone() ){
if ( iterator1.getCurrItem() !== iterator2.getCurrItem() ){
throw new Error ( 'iterator1 和 iterator2 不相等' );
}
iterator1.next();
iterator2.next();
}
alert ( 'iterator1 和 iterator2 相等' );
}

var iterator1 = Iterator( [ 1, 2, 3 ] );
var iterator2 = Iterator( [ 1, 2, 3 ] );
compare( iterator1, iterator2 ); // 输出:iterator1 和 iterator2 相等

迭代类数组对象和字面量对象

迭代器模式不仅可以迭代数组,还可以迭代一些类数组的对象。比如arguments、{“0”:’a’,”1”:’b’}等。通过上面的代码可以观察到,无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代。在 JavaScript 中,for in 语句可以用来迭代普通字面量对象的属性。

倒序迭代器

1
2
3
4
5
6
7
8
9
var reverseEach = function(ary , callback) {
for(var l = ary.length - 1;l >= 0; l--) {
callback(l,ary[l])
}
}

reverseEach([0,1,2] , function(i, n) {
console.log(n)
})

中止迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
var each = function( ary, callback ){ 
for ( var i = 0, l = ary.length; i < l; i++ ){
if ( callback( i, ary[ i ] ) === false ){ // callback 的执行结果返回 false,提前终止迭代
break;
}
}
};
each( [ 1, 2, 3, 4, 5 ], function( i, n ){
if ( n > 3 ){ // n 大于 3 的时候终止循环
return false;
}
console.log( n ); // 分别输出:1, 2, 3
});

发布-订阅模式

它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

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
40
41
42
43
44
45
46
47
48
49

/* 通用发布订阅 */
// 时间上解耦、对象之间解耦,对应的会消耗时间和内存
var Event = (function () {
var clientList = {},
listen, trigger, remove
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = []
}
clientList[key].push(fn)
}
trigger = function () {
var key = Array.prototype.shift.call(arguments)
var fns = clientList[key]
if (!fns || fns.length === 0) {
return false
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments)
}
}
remove = function (key, fn) {
var fns = clientList[key]
if (!fns) {
return false
}
if (!fn) {
fns && (fns.length = 0)
} else {
for (var i = fns.length - 1; i >= 0; i--) {
var _fn = fns[i]
if (_fn === fn) {
fns.splice(i, 1)
}
}
}

}
return {
listen, trigger, remove
}
})()

Event.listen('square88' , function(price) {
console.log('price:' , price);
})

Event.trigger('square88' , 2000000)

发布-订阅模式的应用

书中讲的在这就不多说了,说点书里没有的。

想要对发布-订阅模式有更加深入的学习,推荐自己动手实现一个vue,vue2的响应式就是通过该模式实现的,接下来我们看看这个特性是如何使用的。

vue响应式原理

1
2
3
4
5
6
7
var v = new Vue({
data() {
return {
a:'a'
}
}
})

image

官网有一张关于响应式的图,我们来结合图片分析一下。

数据劫持

我们都知道,数据劫持的核心是Object.defineProperty将属性转化成对应的getter\setter(vue不支持ie8以下的原因)。在数据传递变更的时候,会进入Dep和Watcher中处理。

1
2
3
4
5
6
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; ++i) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}

劫持相关函数及订阅发布

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
/*在闭包中定义一个dep对象*/
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

/*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set

/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
/*如果原本对象拥有getter方法则执行*/
const value = getter ? getter.call(obj) : val
if (Dep.target) {
/*进行依赖收集*/
dep.depend()
if (childOb) {
/*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
dependArray(value)
}
}
return value
},
set: function reactiveSetter(newVal) {
/*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
/*如果原本对象拥有setter方法则执行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}

/*新的值需要重新进行observe,保证数据响应式*/
childOb = observe(newVal)

/*dep对象通知所有的观察者*/
dep.notify()
}
})
}

在初始化时对data内数据开始劫持监听,初始化时调用observe,返回的是Observer实例

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
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
/*
尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。
*/
export function observe(value: any, asRootData: ?boolean): Observer | void {
/*判断是否是一个对象*/
if (!isObject(value)) {
return
}
let ob: Observer | void

/*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
/*这里的判断是为了确保value是单纯的对象,而不是函数或者是Regexp等情况。*/
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
/*如果是根数据则计数,后面Observer中的observe的asRootData非true*/
ob.vmCount++
}
return ob
}
Dep与Watcher

在数据劫持时,数据的获取和修改都会做出对应操作。操作目的就是为了通知“中转站”,它主要是对数据变更起通知作用及存放依赖这些数据的地方。

Dep

Dep用来收集依赖,通知对应的订阅者,让它执行自己的操作。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher
id: number
subs: Array<Watcher>

constructor() {
this.id = uid++
this.subs = []
}

/*添加一个观察者对象*/
addSub(sub: Watcher) {
this.subs.push(sub)
}

/*移除一个观察者对象*/
removeSub(sub: Watcher) {
remove(this.subs, sub)
}

/*依赖收集,当存在Dep.target的时候添加观察者对象*/
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}

/*通知所有订阅者*/
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
/*依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。*/
const targetStack = []
export function pushTarget(_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
// 改变目标指向
Dep.target = _target
}

export function popTarget() {
// 删除当前目标,重算指向
Dep.target = targetStack.pop()
}

上述代码主要进行了两个动作:

  • 定义subs数组,用来搜集订阅者Watcher。
  • 劫持数据变更时,通知Watcher进行update操作。
Watcher

Watcher是订阅者,主要作用是订阅Dep,当Dep发出消息notify的时候,所有订阅了Dep的Watcher执行自己的update操作。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
export default class Watcher {
vm: Component
expression: string
cb: Function
id: number
deep: boolean
user: boolean
lazy: boolean
sync: boolean
dirty: boolean
active: boolean
deps: Array<Dep>
newDeps: Array<Dep>
depIds: ISet
newDepIds: ISet
getter: Function
value: any

constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
/*_watchers存放订阅者实例*/
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression =
process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
// parse expression for getter
/*把表达式expOrFn解析成getter*/
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function() {}
process.env.NODE_ENV !== 'production' &&
warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy ? undefined : this.get()
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
/*获得getter的值并且重新进行依赖收集*/
get() {
/*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
pushTarget(this)
let value
const vm = this.vm

/*
执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
在将Dep.target设置为自生观察者实例以后,执行getter操作。
譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
那么在执行getter的时候就会触发a跟c两个数据的getter函数,
在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
将该观察者对象放入闭包中的Dep的subs中去。
*/
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
/*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
if (this.deep) {
/*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
traverse(value)
}

/*将观察者实例从target栈中取出并设置给Dep.target*/
popTarget()
this.cleanupDeps()
return value
}

/**
* Add a dependency to this directive.
*/
/*添加一个依赖关系到Deps集合中*/
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

/**
* Clean up for dependency collection.
*/
/*清理依赖收集*/
cleanupDeps() {
/*移除所有观察者对象*/
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}

/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
/*
调度者接口,当依赖发生改变的时候进行回调。
*/
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,由调度者调用。*/
queueWatcher(this)
}
}

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
/*
调度者工作接口,将被调度者回调。
*/
run() {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
/*
即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
*/
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
/*设置新的值*/
this.value = value

/*触发回调渲染视图*/
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
/*获取观察者的值*/
evaluate() {
this.value = this.get()
this.dirty = false
}

/**
* Depend on all deps collected by this watcher.
*/
/*收集该watcher的所有deps依赖*/
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}

/**
* Remove self from all dependencies' subscriber list.
*/
/*将自身从所有依赖收集订阅列表删除*/
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
/*从vm实例的观察者列表中将自身移除,由于该操作比较耗费资源,所以如果vm实例正在被销毁则跳过该步骤。*/
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}

命令模式

命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。

命令模式是一种松耦合的方式,使请求发送者和接收者消除彼此的耦合关系。

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
40
41
42
/* 
本质是对命令封装,拆分发出命令的责任和执行命令的责任
优点:降低对象耦合度,易扩展组合命令,调用同一方法实现不同功能
*/

// 命令
var CreateCommand = function (receiver) {
this.receiver = receiver
}
CreateCommand.prototype.execute = function () {
this.receiver.action()
}

// 接收者
var TVOn = function () { }
TVOn.prototype.action = function () {
console.log('TV on now');
}

var TVOff = function () { }
TVOff.prototype.action = function () {
console.log('TV off now');
}

// 调用者
var Invoker = function (tvOnCommand, tvOffCommand) {
this.tvOnCommand = tvOnCommand
this.tvOffCommand = tvOffCommand
}
Invoker.prototype.tvOn = function () {
this.tvOnCommand.execute()
}
Invoker.prototype.tvOff = function () {
this.tvOffCommand.execute()
}

var tvOnCommand = new CreateCommand(new TVOn())
var tvOffCommand = new CreateCommand(new TVOff())

var myInvoker = new Invoker(tvOnCommand, tvOffCommand)
myInvoker.tvOn()
myInvoker.tvOff()

组合模式

组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。

组合模式用途

组合模式将对象组合成树型结构,以表示“部分-整体”的层级结构。组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。

以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令),组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。

请求从上至下传递直到尽头,用户只需要关心最顶层的组合对象,请求该对象请求便会向下传递。

角色:
(1)子对象
(2)组合对象
(3)抽象类:主要定义了参与组合的对象的公共接口,也可以直接在组合对象中定义

举例

github上原有例子

场景:组织内有各种员工,员工有不同姓名、工资,可进行添加操作。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 场景 以员工为例。这里我们有不同的员工类型
// 开发者
class Developer {
constructor(name, salary) {
this.name = name
this.salary = salary
}
getName() {
return this.name
}
setSalary(salary) {
this.salary = salary
}
getSalary() {
return this.salary
}
getRoles() {
return this.roles
}
develop() {
/* */
}
}
// 设计师
class Designer {
constructor(name, salary) {
this.name = name
this.salary = salary
}
getName() {
return this.name
}
setSalary(salary) {
this.salary = salary
}
getSalary() {
return this.salary
}
getRoles() {
return this.roles
}
design() {
/* */
}
}
// 一个由几种不同类型的员工组成的组织
class Organization {
constructor(){
this.employees = []
}
// 追加元素
addEmployee(employee) {
this.employees.push(employee)
}
// 叶对象都有一样的getSalary方法。在根对象执行的时候,可以使用leaf.execute的模式来调用对象的方法。
getNetSalaries() {
let netSalary = 0
this.employees.forEach(employee => {
netSalary += employee.getSalary()
})
return netSalary
}
}
// 调用
// Prepare the employees
const john = new Developer('John Doe', 12000)
const jane = new Designer('Jane', 10000)
// Add them to organization 优势:无论多少员工类型 对整个组合对象只调用一次
const organization = new Organization()
organization.addEmployee(john)
organization.addEmployee(jane)
console.log("Net salaries: " , organization.getNetSalaries()) // Net Salaries: 22000

需要注意的地方

  • 组合模式不是父子关系
  • 对叶对象操作的一致性
  • 双向映射关系
  • 用职责链模式提高组合模式性能

使用场景

  • 含有某种层级结构的对象集合(具体结构在开发过程中无法确定)
  • 希望对这些对象或者其中的某些对象执行某种操作

缺点:因为组合对象的任何操作都会对所有的子对象调用同样的操作,所以当组合的结构很大时会有性能问题。


w字总结《JavaScript设计模式与开发实践》(设计模式)(上)
https://moewang0321.github.io/2021/11/17/w字总结《JavaScript设计模式与开发实践》(设计模式)(上)/
作者
Moe Wang
发布于
2021年11月17日
许可协议