0%

JavaScript设计模式

设计模式

SOLID设计原则

  • 单一功能原则(Single Responsibility Principle
  • 开放封闭原则(Opened Closed Principle
  • 里式替换原则(Liskov Substitution Principle)
  • 接口隔离原则(Interface Segregation Principle)
  • 依赖反转原则(Dependency Inversion Principle)

开放封闭原则:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改

设计模式核心思想-封装变化

将变与不变分离,确保变化的部分灵活、不变的部分稳定。



简单工厂模式

构造器模式

1
2
3
4
5
6
7
function User(name , age, career) {
this.name = name
this.age = age
this.career = career
}

const user = new User(name, age, career)

构造器模式可以很好的区分共性与个性即变与不变

简单工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function User(name , age, career, work) {
this.name = name
this.age = age
this.career = career
this.work = work
}

function Factory(name, age, career) {
let work
switch(career) {
case 'coder':
work = ['写代码','写系分', '修Bug']
break
case 'product manager':
work = ['订会议室', '写PRD', '催更']
break
case 'boss':
work = ['喝茶', '看报', '见客户']
case 'xxx':
// 其它工种的职责分配
...

return new User(name, age, career, work)
}

工厂模式其实就是将创建对象的过程单独封装,其目的就是为了实现无脑传参

小结

将创建对象的过程单独封装,这样的操作就是工厂模式
有构造函数的地方,我们就应该想到简单工厂
构造器解决的是多个对象实例的问题,简单工厂解决的是多个类的问题


抽象工厂模式

创建抽象工厂

1
2
3
4
5
6
7
8
9
10
class MobilePhoneFactory {
// 提供操作系统的接口
createOS(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
// 提供硬件的接口
createHardWare(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
}

抽象工厂不创建实例而是给具体工厂继承

具体工厂

1
2
3
4
5
6
7
8
9
10
11
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
createOS() {
// 提供安卓系统实例
return new AndroidOS()
}
createHardWare() {
// 提供高通硬件实例
return new QualcommHardWare()
}
}

抽象产品

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
// 定义操作系统这类产品的抽象产品类
class OS {
controlHardWare() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}

// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}

class AppleOS extends OS {
controlHardWare() {
console.log('我会用🍎的方式去操作硬件')
}
}


// 定义手机硬件这类产品的抽象产品类
class HardWare {
// 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
operateByOrder() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}

// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
operateByOrder() {
console.log('我会用高通的方式去运转')
}
}

class MiWare extends HardWare {
operateByOrder() {
console.log('我会用小米的方式去运转')
}
}

当我们使用的时候只需要这样:

1
2
3
4
5
6
7
8
9
10
// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()

如果不需要FakeStarFactory,而是创建其他工厂就可以**不对抽象工厂MobilePhoneFactory做任何修改,只需要拓展它的种类:

1
2
3
4
5
6
7
8
class newStarFactory extends MobilePhoneFactory {
createOS() {
// 操作系统实现代码
}
createHardWare() {
// 硬件实现代码
}
}

这样就符合对原有的系统不会造成任何潜在影响的“对拓展开放,对修改封闭”的原则

总结

  • 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里,抽象工厂可以有多个(大家可以想象我们的手机厂后来被一个更大的厂收购了,这个厂里除了手机抽象类,还有平板、游戏机抽象类等等),每一个抽象工厂对应的这一类的产品,被称为“产品族”。
  • 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的那些方法,用于创建具体的产品的类。
  • 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如操作系统、硬件等),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
  • 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 比如我们上文中具体的一种操作系统、或具体的一种硬件等。

单例模式

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

单例模式的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SingleDog {
show() {
console.log('我是一个单例对象')
}
static getInstance() {
// 判断是否已经new过1个实例
if (!SingleDog.instance) {
// 若这个唯一的实例不存在,那么先创建它
SingleDog.instance = new SingleDog()
}
// 如果这个唯一的实例已经存在,则直接返回
return SingleDog.instance
}
}

const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

// true
s1 === s2

还可以用闭包实现

1
2
3
4
5
6
7
8
9
10
11
12
SingleDog.getInstance = (function() {
// 定义自由变量instance,模拟私有变量
let instance = null
return function() {
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new SingleDog()
}
return instance
}
})()

Vuex中的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let Vue // 这个Vue的作用和楼上的instance作用一样
...

export function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue)
}

实现一个 Storage

实现Storage,使得该对象为单例,基于 localStorage 进行封装。实现方法 setItem(key,value) 和 getItem(key)。

静态方法

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
// 定义Storage
class Storage {
static getInstance() {
// 判断是否已经new过1个实例
if (!Storage.instance) {
// 若这个唯一的实例不存在,那么先创建它
Storage.instance = new Storage()
}
// 如果这个唯一的实例已经存在,则直接返回
return Storage.instance
}
getItem (key) {
return localStorage.getItem(key)
}
setItem (key, value) {
return localStorage.setItem(key, value)
}
}

const storage1 = Storage.getInstance()
const storage2 = Storage.getInstance()

storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')

// 返回true
storage1 === storage2

闭包版

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
// 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
function StorageBase () {}
StorageBase.prototype.getItem = function (key){
return localStorage.getItem(key)
}
StorageBase.prototype.setItem = function (key, value) {
return localStorage.setItem(key, value)
}

// 以闭包的形式创建一个引用自由变量的构造函数
const Storage = (function(){
let instance = null
return function(){
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new StorageBase()
}
return instance
}
})()

// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果
const storage1 = new Storage()
const storage2 = new Storage()

storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')

// 返回true
storage1 === storage2

实现一个全局的模拟框

实现一个全局唯一的Modal弹框

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单例模式弹框</title>
</head>
<style>
#modal {
height: 200px;
width: 200px;
line-height: 200px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid black;
text-align: center;
}
</style>
<body>
<button id='open'>打开弹框</button>
<button id='close'>关闭弹框</button>
</body>
<script>
// 核心逻辑,这里采用了闭包思路来实现单例模式
const Modal = (function() {
let modal = null
return function() {
if(!modal) {
modal = document.createElement('div')
modal.innerHTML = '我是一个全局唯一的Modal'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()

// 点击打开按钮展示模态框
document.getElementById('open').addEventListener('click', function() {
// 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
const modal = new Modal()
modal.style.display = 'block'
})

// 点击关闭按钮隐藏模态框
document.getElementById('close').addEventListener('click', function() {
const modal = new Modal()
if(modal) {
modal.style.display = 'none'
}
})
</script>
</html>

原型模式

原型模式不仅是一种设计模式,它还是一种编程范式(programming paradigm),是 JavaScript 面向对象系统实现的根基。

以类为中心的语言和以原型为中心的语言

JAVA中的类

JAVA 中,类才是它面向对象系统的根本。所以说在 JAVA 中,我们可以选择不使用原型模式 —— 这样一来,所有的实例都必须要从类中来,当我们希望创建两个一模一样的实例时,就只能这样做(假设实例从 Dog 类中来,必传参数为姓名、性别、年龄和品种):

1
2
3
Dog dog = new Dog('旺财', 'male', 3, '柴犬')

Dog dog_copy = new Dog('旺财', 'male', 3, '柴犬')

没错,我们不得不把一模一样的参数传两遍,非常麻烦。而原型模式允许我们通过调用克隆方法的方式达到同样的目的,比较方便,所以 Java 专门针对原型模式设计了一套接口和方法,在必要的场景下会通过原型方法来应用原型模式。当然,在更多的情况下,Java 仍以“实例化类”这种方式来创建对象。

JavaScript中的类

JS 的类基于prototype,ES6 的class为语法糖

ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN


原型范式

了解原型与原型链

需要了解深拷贝

深入了解深拷贝


装饰器模式

在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求

给点击按钮弹窗添加逻辑

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
// 定义打开按钮
class OpenButton {
// 点击后展示弹框(旧逻辑)
onClick() {
const modal = new Modal()
modal.style.display = 'block'
}
}

// 定义按钮对应的装饰器
class Decorator {
// 将按钮实例传入
constructor(open_button) {
this.open_button = open_button
}

onClick() {
this.open_button.onClick()
// “包装”了一层新逻辑
this.changeButtonStatus()
}

changeButtonStatus() {
this.changeButtonText()
this.disableButton()
}

disableButton() {
const btn = document.getElementById('open')
btn.setAttribute("disabled", true)
}

changeButtonText() {
const btn = document.getElementById('open')
btn.innerText = '快去登录'
}
}

const openButton = new OpenButton()
const decorator = new Decorator(openButton)

document.getElementById('open').addEventListener('click', function() {
// openButton.onClick()
// 此处可以分别尝试两个实例的onClick方法,验证装饰器是否生效
decorator.onClick()
})

我们把实例传给了Decorator这样方便未来的拓展


单一职责原则

文本修改&按钮置灰这两个变化,被封装在了两个不同的方法里,这是一种单一职责的体现,在日常开发中要首先有尝试拆分的敏感,其次要有该不该拆的判断,如果逻辑颗粒度过小,盲目拆分会导致项目中有过多零碎的小方法。


ES7中的装饰器

ES7 中我们可以通过@语法糖给类添加装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
target.hasDecorator = true
return target
}

// 将装饰器“安装”到Button类上
@classDecorator
class Button {
// Button类的相关逻辑
}

// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator)

也可是使用它来装饰类里面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 具体的参数意义,在下个小节,这里大家先感知一下操作
function funcDecorator(target, name, descriptor) {
let originalMethod = descriptor.value
descriptor.value = function() {
console.log('我是Func的装饰器逻辑')
return originalMethod.apply(this, arguments)
}
return descriptor
}

class Button {
@funcDecorator
onClick() {
console.log('我是Func的原有逻辑')
}
}

// 验证装饰器是否生效
const button = new Button()
button.onClick()

装饰器语法糖背后的故事

函数传参 & 调用

1
2
3
4
5
6
7
8
9
10
function classDecorator(target) {
target.hasDecorator = true
return target
}

// 将装饰器“安装”到Button类上
@classDecorator
class Button {
// Button类的相关逻辑
}

给类添加装饰器时,target是类本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function funcDecorator(target, name, descriptor) {
let originalMethod = descriptor.value
descriptor.value = function() {
console.log('我是Func的装饰器逻辑')
return originalMethod.apply(this, arguments)
}
return descriptor
}

class Button {
@funcDecorator
onClick() {
console.log('我是Func的原有逻辑')
}
}

修饰方法时targetButton.prototype,因为onClick 方法总是要依附其实例存在的,修饰 onClik 其实是修饰它的实例。但我们的装饰器函数执行的时候,Button 实例还并不存在。为了确保实例生成后可以顺利调用被装饰好的方法,装饰器只能去修饰 Button 类的原型对象。

将“属性描述对象”交到你手里

1
2
3
4
5
6
7
8
function funcDecorator(target, name, descriptor) {
let originalMethod = descriptor.value
descriptor.value = function() {
console.log('我是Func的装饰器逻辑')
return originalMethod.apply(this, arguments)
}
return descriptor
}

target是原型,name是要修饰的属性,descriptor就是Object.defineProperty(obj, prop, descriptor)中的descriptor


生产实践

React中的装饰器:HOC

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

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
// 高阶组件
import React, { Component } from 'react'

const BorderHoc = WrappedComponent => class extends Component {
render() {
return <div style={{ border: 'solid 1px red' }}>
<WrappedComponent />
</div>
}
}
export default borderHoc

// 业务组件
import React, { Component } from 'react'
import BorderHoc from './BorderHoc'

// 用BorderHoc装饰目标组件
@BorderHoc
class TargetComponent extends React.Component {
render() {
// 目标组件具体的业务逻辑
}
}

// export出去的其实是一个被包裹后的组件
export default TargetComponent

使用装饰器改写 Redux connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

class App extends Component {
render() {
// App的业务逻辑
}
}

function mapStateToProps(state) {
// 假设App的状态对应状态树上的app节点
return state.app
}

function mapDispatchToProps(dispatch) {
// 这段看不懂也没关系,下面会有解释。重点理解connect的调用即可
return bindActionCreators(action, dispatch)
}

// 把App组件与Redux绑在一起
export default connect(mapStateToProps, mapDispatchToProps)(App)
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
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

function mapStateToProps(state) {
return state.app
}

function mapDispatchToProps(dispatch) {
return bindActionCreators(action, dispatch)
}

// 将connect调用后的结果作为一个装饰器导出
export default connect(mapStateToProps, mapDispatchToProps)
// 在组件文件里引入connect:

import React, { Component } from 'react'
import connect from './connect.js'

@connect
export default class App extends Component {
render() {
// App的业务逻辑
}
}

core-decorators


适配器模式

适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。

axios中的适配器

axios 的核心逻辑中,我们可以注意到实际上派发请求的是 dispatchRequest 方法。该方法内部其实主要做了两件事:

  1. 数据转换,转换请求体/响应体,可以理解为数据层面的适配;
  2. 调用适配器。

调用如下适配器:

1
2
3
4
5
6
7
8
9
10
11
12
function getDefaultAdapter() {
var adapter;
// 判断当前是否是node环境
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// 如果是node环境,调用node专属的http适配器
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 如果是浏览器环境,调用基于xhr的适配器
adapter = require('./adapters/xhr');
}
return adapter;
}

http适配器

1
2
3
4
5
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
// 具体逻辑
}
}

xhr适配器

1
2
3
4
5
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 具体逻辑
}
}

代理模式

ES6 的Proxy

1
const proxy = new Proxy(obj, handler)

每次访问obj都需要通过handler

getter拦截

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
// 普通私密信息
const baseInfo = ['age', 'career']
// 最私密信息
const privateInfo = ['avatar', 'phone']

// 用户(同事A)对象实例
const user = {
...(一些必要的个人信息)
isValidated: true,
isVIP: false,
}

// 掘金婚介所登场了
const JuejinLovers = new Proxy(girl, {
get: function(girl, key) {
if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
alert('您还没有完成验证哦')
return
}

//...(此处省略其它有的没的各种校验逻辑)

// 此处我们认为只有验证过的用户才可以购买VIP
if(user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
alert('只有VIP才可以查看该信息哦')
return
}
}
})

setter拦截

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
// 规定礼物的数据结构由type和value组成
const present = {
type: '巧克力',
value: 60,
}

// 为用户增开presents字段存储礼物
const girl = {
// 姓名
name: '小美',
// 自我介绍
aboutMe: '...'(大家自行脑补吧)
// 年龄
age: 24,
// 职业
career: 'teacher',
// 假头像
fakeAvatar: 'xxxx'(新垣结衣的图片地址)
// 真实头像
avatar: 'xxxx'(自己的照片地址),
// 手机号
phone: 123456,
// 礼物数组
presents: [],
// 拒收50块以下的礼物
bottomValue: 50,
// 记录最近一次收到的礼物
lastPresent: present,
}

// 掘金婚介所推出了小礼物功能
const JuejinLovers = new Proxy(girl, {
get: function(girl, key) {
if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
alert('您还没有完成验证哦')
return
}

//...(此处省略其它有的没的各种校验逻辑)

// 此处我们认为只有验证过的用户才可以购买VIP
if(user.isValidated && privateInfo.indexOf(key)!==-1 && !user.isVIP) {
alert('只有VIP才可以查看该信息哦')
return
}
}

set: function(girl, key, val) {

// 最近一次送来的礼物会尝试赋值给lastPresent字段
if(key === 'lastPresent') {
if(val.value < girl.bottomValue) {
alert('sorry,您的礼物被拒收了')
return
}

// 如果没有拒收,则赋值成功,同时并入presents数组
girl.lastPresent = val
girl.presents = [...girl.presents, val]
}
}

})

常见四种代理模式

事件代理

1
2
3
4
5
6
7
8
9
10
11
12
// 获取父元素
const father = document.getElementById('father')

// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
// 识别是否是目标子元素
if(e.target.tagName === 'A') {
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
} )

虚拟代理

图片预加载

错误的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PreLoadImage {
// 占位图的url地址
static LOADING_URL = 'xxxxxx'

constructor(imgNode) {
// 获取该实例对应的DOM节点
this.imgNode = imgNode
}

// 该方法用于设置真实的图片地址
setSrc(targetUrl) {
// img节点初始化时展示的是一个占位图
this.imgNode.src = PreLoadImage.LOADING_URL
// 创建一个帮我们加载图片的Image实例
const image = new Image()
// 监听目标图片加载的情况,完成时再将DOM上的img节点的src属性设置为目标图片的url
image.onload = () => {
this.imgNode.src = targetUrl
}
// 设置src属性,Image实例开始加载图片
image.src = targetUrl
}
}

正确的做法

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
class PreLoadImage {
constructor(imgNode) {
// 获取真实的DOM节点
this.imgNode = imgNode
}

// 操作img节点的src属性
setSrc(imgUrl) {
this.imgNode.src = imgUrl
}
}

class ProxyImage {
// 占位图的url地址
static LOADING_URL = 'xxxxxx'

constructor(targetImage) {
// 目标Image,即PreLoadImage实例
this.targetImage = targetImage
}

// 该方法主要操作虚拟Image,完成加载
setSrc(targetUrl) {
// 真实img节点初始化时展示的是一个占位图
this.targetImage.setSrc(ProxyImage.LOADING_URL)
// 创建一个帮我们加载图片的虚拟Image实例
const virtualImage = new Image()
// 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage.onload = () => {
this.targetImage.setSrc(targetUrl)
}
// 设置src属性,虚拟Image实例开始加载图片
virtualImage.src = targetUrl
}
}

ProxyImage 帮我们调度了预加载相关的工作,我们可以通过 ProxyImage 这个代理,实现对真实 img 节点的间接访问,并得到我们想要的效果。

在这个实例中,virtualImage 这个对象是一个“幕后英雄”,它始终存在于 JavaScript 世界中、代替真实 DOM 发起了图片加载请求、完成了图片加载工作,却从未在渲染层面抛头露面。因此这种模式被称为“虚拟代理”模式。

缓存代理

对计算结果缓存

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
// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
console.log('进行了一次新计算')
let result = 0
const len = arguments.length
for(let i = 0; i < len; i++) {
result += arguments[i]
}
return result
}

// 为求和方法创建代理
const proxyAddAll = (function(){
// 求和结果的缓存池
const resultCache = {}
return function() {
// 将入参转化为一个唯一的入参字符串
const args = Array.prototype.join.call(arguments, ',')

// 检查本次入参是否有对应的计算结果
if(args in resultCache) {
// 如果有,则返回缓存池里现成的结果
return resultCache[args]
}
return resultCache[args] = addAll(...arguments)
}
})()

保护代理

上面拦截getter和setter就是一种保护代理

小结

代理模式十分多样,可以是为了加强控制、拓展功能、提高性能,也可以仅仅是为了优化我们的代码结构、实现功能的解耦。


策略模式

例子

错误做法

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
function askPrice(tag, originPrice) {

// 处理预热价
if(tag === 'pre') {
if(originPrice >= 100) {
return originPrice - 20
}
return originPrice * 0.9
}
// 处理大促价
if(tag === 'onSale') {
if(originPrice >= 100) {
return originPrice - 30
}
return originPrice * 0.8
}

// 处理返场价
if(tag === 'back') {
if(originPrice >= 200) {
return originPrice - 50
}
return originPrice
}

// 处理尝鲜价
if(tag === 'fresh') {
return originPrice * 0.5
}

// 处理新人价
if(tag === 'newUser') {
if(originPrice >= 100) {
return originPrice - 50
}
return originPrice
}
}

单一功能改造

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
// 处理预热价
function prePrice(originPrice) {
if(originPrice >= 100) {
return originPrice - 20
}
return originPrice * 0.9
}

// 处理大促价
function onSalePrice(originPrice) {
if(originPrice >= 100) {
return originPrice - 30
}
return originPrice * 0.8
}

// 处理返场价
function backPrice(originPrice) {
if(originPrice >= 200) {
return originPrice - 50
}
return originPrice
}

// 处理尝鲜价
function freshPrice(originPrice) {
return originPrice * 0.5
}

function askPrice(tag, originPrice) {
// 处理预热价
if(tag === 'pre') {
return prePrice(originPrice)
}
// 处理大促价
if(tag === 'onSale') {
return onSalePrice(originPrice)
}

// 处理返场价
if(tag === 'back') {
return backPrice(originPrice)
}

// 处理尝鲜价
if(tag === 'fresh') {
return freshPrice(originPrice)
}
}

对扩展开放,对修改封闭

对象映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义一个询价处理器对象
const priceProcessor = {
pre(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
},
onSale(originPrice) {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
},
back(originPrice) {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
},
fresh(originPrice) {
return originPrice * 0.5;
},
};

定义

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换


状态模式

例子

  • 美式咖啡态(american):只吐黑咖啡
  • 普通拿铁态(latte):黑咖啡加点奶
  • 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
  • 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力

    错误写法

    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
    class CoffeeMaker {
    constructor() {
    /**
    这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
    **/
    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
    }

    // 关注咖啡机状态切换函数
    changeState(state) {
    // 记录当前状态
    this.state = state;
    if(state === 'american') {
    // 这里用 console 代指咖啡制作流程的业务逻辑
    console.log('我只吐黑咖啡');
    } else if(state === 'latte') {
    console.log(`给黑咖啡加点奶`);
    } else if(state === 'vanillaLatte') {
    console.log('黑咖啡加点奶再加香草糖浆');
    } else if(state === 'mocha') {
    console.log('黑咖啡加点奶再加点巧克力');
    }
    }
    }

职责分离

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
class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
}
changeState(state) {
// 记录当前状态
this.state = state;
if(state === 'american') {
// 这里用 console 代指咖啡制作流程的业务逻辑
this.americanProcess();
} else if(state === 'latte') {
this.latteProcress();
} else if(state === 'vanillaLatte') {
this.vanillaLatteProcress();
} else if(state === 'mocha') {
this.mochaProcress();
}
}

americanProcess() {
console.log('我只吐黑咖啡');
}

latteProcress() {
this.americanProcess();
console.log('加点奶');
}

vanillaLatteProcress() {
this.latteProcress();
console.log('再加香草糖浆');
}

mochaProcress() {
this.latteProcress();
console.log('再加巧克力');
}
}

const mk = new CoffeeMaker();
mk.changeState('latte');

开放封闭

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
const stateToProcessor = {
american() {
console.log('我只吐黑咖啡');
},
latte() {
this.american();
console.log('加点奶');
},
vanillaLatte() {
this.latte();
console.log('再加香草糖浆');
},
mocha() {
this.latte();
console.log('再加巧克力');
}
}

class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
}

// 关注咖啡机状态切换函数
changeState(state) {
// 记录当前状态
this.state = state;
// 若状态不存在,则返回
if(!stateToProcessor[state]) {
return ;
}
stateToProcessor[state]();
}
}

const mk = new CoffeeMaker();
mk.changeState('latte');

这种方法仅仅是看上去完美无缺,其中却暗含一个非常重要的隐患——stateToProcessor 里的工序函数,感知不到咖啡机的内部状况。

进一步改造

把咖啡机和它的状态处理函数建立关联。

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
class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
// 初始化牛奶的存储量
this.leftMilk = '500ml';
}
stateToProcessor = {
that: this,
american() {
// 尝试在行为函数里拿到咖啡机实例的信息并输出
console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
console.log('我只吐黑咖啡');
},
latte() {
this.american()
console.log('加点奶');
},
vanillaLatte() {
this.latte();
console.log('再加香草糖浆');
},
mocha() {
this.latte();
console.log('再加巧克力');
}
}

// 关注咖啡机状态切换函数
changeState(state) {
this.state = state;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}

const mk = new CoffeeMaker();
mk.changeState('latte');

策略与状态辨析

策略模式是对算法的封装。算法和状态对应的行为函数虽然本质上都是行为,但是算法的独立性可高。
状态模式需要对主体有感知,来判断接下来是否可执行。
总结: 策略模式中函数不依赖调用主体,互相平行。状态模式中的函数和状态主体关联,由状态主体将他们串联,所以不会特别割裂。

定义

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。


观察者模式

定义

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

例子

发布者:

  • 增加订阅者
  • 通知订阅者
  • 移除订阅者
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
// 定义发布者类
class Publisher {
constructor() {
this.observers = []
console.log('Publisher created')
}
// 增加订阅者
add(observer) {
console.log('Publisher.add invoked')
this.observers.push(observer)
}
// 移除订阅者
remove(observer) {
console.log('Publisher.remove invoked')
this.observers.forEach((item, i) => {
if (item === observer) {
this.observers.splice(i, 1)
}
})
}
// 通知所有订阅者
notify() {
console.log('Publisher.notify invoked')
this.observers.forEach((observer) => {
observer.update(this)
})
}
}

订阅者:被通知,去执行

1
2
3
4
5
6
7
8
9
10
// 定义订阅者类
class Observer {
constructor() {
console.log('Observer created')
}

update() {
console.log('Observer.update invoked')
}
}

具体实现

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
// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
constructor() {
super()
// 初始化需求文档
this.prdState = null
// 韩梅梅还没有拉群,开发群目前为空
this.observers = []
console.log('PrdPublisher created')
}

// 该方法用于获取当前的prdState
getState() {
console.log('PrdPublisher.getState invoked')
return this.prdState
}

// 该方法用于改变prdState的值
setState(state) {
console.log('PrdPublisher.setState invoked')
// prd的值发生改变
this.prdState = state
// 需求文档变更,立刻通知所有开发者
this.notify()
}
}

// 订阅者
class DeveloperObserver extends Observer {
constructor() {
super()
// 需求文档一开始还不存在,prd初始为空对象
this.prdState = {}
console.log('DeveloperObserver created')
}

// 重写一个具体的update方法
update(publisher) {
console.log('DeveloperObserver.update invoked')
// 更新需求文档
this.prdState = publisher.getState()
// 调用工作函数
this.work()
}

// work方法,一个专门搬砖的方法
work() {
// 获取需求文档
const prd = this.prdState
// 开始基于需求文档提供的信息搬砖。。。
...
console.log('996 begins...')
}
}

// 创建订阅者:前端开发李雷
const liLei = new DeveloperObserver()
// 创建订阅者:服务端开发小A(sorry。。。起名字真的太难了)
const A = new DeveloperObserver()
// 创建订阅者:测试同学小B
const B = new DeveloperObserver()
// 韩梅梅出现了
const hanMeiMei = new PrdPublisher()
// 需求文档出现了
const prd = {
// 具体的需求内容
...
}
// 韩梅梅开始拉群
hanMeiMei.add(liLei)
hanMeiMei.add(A)
hanMeiMei.add(B)
// 韩梅梅发送了需求文档,并@了所有人
hanMeiMei.setState(prd)

vue中的响应原理

  • observer(监听器):注意,此 observer 非彼 observer。在我们上节的解析中,observer 作为设计模式中的一个角色,代表“订阅者”。但在Vue数据双向绑定的角色结构里,所谓的 observer 不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个发布者。
  • watcher(订阅者):observer 把数据转发给了真正的订阅者——watcher对象。watcher 接收到新的数据后,会去更新视图。
  • compile(编译器):MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”也归它管~

实现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
// observe方法遍历并包装对象属性
function observe(target) {
// 若target是一个对象,则遍历它
if(target && typeof target === 'object') {
Object.keys(target).forEach((key)=> {
// defineReactive方法会给目标属性装上“监听器”
defineReactive(target, key, target[key])
})
}
}

// 定义defineReactive方法
function defineReactive(target, key, val) {
// 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
const dep = new Dep()
observe(val)
// 为当前属性安装监听器
Object.defineProperty(target, key, {
// 可枚举
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 监听器函数
set: function (value) {
dep.notify();
}
});
}

实现订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义订阅者类Dep
class Dep {
constructor() {
// 初始化订阅队列
this.subs = []
}

// 增加订阅者
addSub(sub) {
this.subs.push(sub)
}

// 通知订阅者(是不是所有的代码都似曾相识?)
notify() {
this.subs.forEach((sub)=>{
sub.update()
})
}
}

实现一个Event Bus/ Event Emitter

全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。

在Vue中使用Event Bus来实现组件间的通讯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// event buss
const EventBus = new Vue()
export default EventBus

// main.js
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus

// sub
// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)

// notify
// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)

整个调用过程中,没有出现具体的发布者和订阅者(比如上节的PrdPublisher和DeveloperObserver),全程只有bus这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!

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
// 实现一个event bus
class EventEmitter {
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = {}
}

// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []
}

// 把回调函数推入目标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}

// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 检查目标事件是否有监听函数队列
if (this.handlers[eventName]) {
// 如果有,则逐个调用队列里的回调函数
this.handlers[eventName].forEach((callback) => {
callback(...args)
})
}
}

// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if (index !== -1) {
callbacks.splice(index, 1)
}
}

// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}

FaceBook推出的通用EventEmiiter库

观察者模式与发布-订阅模式的区别是什么

回到我们上文的例子里。韩梅梅把所有的开发者拉了一个群,直接把需求文档丢给每一位群成员,这种发布者直接触及到订阅者的操作,叫观察者模式。但如果韩梅梅没有拉群,而是把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者,这种发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。

观察者模式,解决的其实是模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用,两者之间还是有着说不清、道不明的关系。

而发布-订阅模式,则是快刀斩乱麻了——发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。发布-订阅模式下,实现了完全地解耦。

但这并不意味着,发布-订阅模式就比观察者模式“高级”。在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦。如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。


迭代器模式

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

ES6对迭代器的实现

ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for…of…循环和迭代器的next方法遍历。 事实上,for…of…的背后正是对next方法的反复调用。

1
2
3
4
5
6
7
8
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()

迭代器协议

实现迭代器生成函数

1
2
3
4
5
6
7
8
9
10
11
12
// 编写一个迭代器生成函数
function *iteratorGenerator() {
yield '1号选手'
yield '2号选手'
yield '3号选手'
}

const iterator = iteratorGenerator()

iterator.next()
iterator.next()
iterator.next()

实现这个语法糖

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
// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {
// idx记录当前访问的索引
var idx = 0
// len记录传入集合的长度
var len = list.length
return {
// 自定义next方法
next: function() {
// 如果索引还没有超出集合长度,done为false
var done = idx >= len
// 如果done为false,则可以继续取值
var value = !done ? list[idx++] : undefined

// 将当前值与遍历是否完毕(done)返回
return {
done: done,
value: value
}
}
}
}

var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()