前端必备的 7 种设计模式

2,303 阅读4分钟

“我正在参加「掘金·启航计划」”

TIP 👉 五更钟漏欲相催,四气推迁往复回。帐里残灯才去焰,炉中香气尽成灰。渐看春逼芙蓉枕,顿觉寒销竹叶杯。守岁家家应未卧,相思那得梦魂来。——唐·孟浩然《除夜有怀》

前言

有句话叫“需求指导设计,设计指导开发” —— 这是大厂研发的流程。

你学会了 HTML CSS JS Vue React 等,这些是为了做开发 你学会了设计模式,就能让你做设计(技术方案设计,不是 UI 设计)—— 只有技术大牛才能指导别人做设计,一般人设计不了

所以,设计模式是高级工程师的必备技能,你可以问问你身边的比较厉害的工程师,他们肯定都精通常用的设计模式。

结合场景

image.png

你可能会恍然大悟:哇,原来我一直用这些技术,但我之前却不知道对应设计模式的名字。 我们不能光有实践经验,也要适当的进行理论总结。要知道我们所做的事情是对应到哪个知识点,这样才能慢慢的完善自己的知识体系。

工厂模式

工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式

工厂模式可以拆分为三个:

  • 工厂方法模式
  • 抽象工厂模式
  • 建造者模式

前端用不到这么细致,只需要掌握核心的工厂模式即可。

工厂模式在前端 JS 中应用非常广泛,随处可见

  • jQuery $

  • Vue _createElementVNode

  • React createElement

示例:


class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        console.log('init')
    }
    fun() {
        console.log('fun')
    }
}

class Factory {
    create(name) {
        return new Product(name)
    }
}

// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()

单例模式

前端用到严格的单例模式并不多,但单例模式的思想到处都有

单例模式,即对一个 class 只能创建一个实例,即便调用多次。

如一个系统的登录框、遮罩层,可能会被很多地方调用,但登录框只初始化一次即可,以后的直接复用。

再例如,想 Vuex Redux 这些全局数据存储,全局只能有一个实例,如果有多个,会出错的。

伪代码

登录框,初始化多次没必要。

class LoginModal { }

// modal1 和 modal2 功能一样,没必要初始化两次
const modal1 = new LoginModal()
const modal2 = new LoginModal()

全局存储,初始化多个实例,会出错。

class Store { /* get set ... */ }

const store1 = new Store()
store1.set(key, value)

const store2 = new Store()
store2.get(key) // 获取不到

示例:

class CreateUser {
    constructor(name) {
        this.name = name;
        this.getName();
    }
    getName() {
        return this.name;
    }
}
// 代理实现单例模式
var ProxyMode = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {
            instance = new CreateUser(name);
        }
        return instance;
    }
})();
// 测试单体模式的实例
var a = new ProxyMode("aaa");
var b = new ProxyMode("bbb");
// 因为单体模式是只实例化一次,所以下面的实例是相等的
console.log(a === b);    //true

观察者模式

观察者模式是前端最常用的一个设计模式,也是 UI 编程最重要的思想。

例如你在星巴克点了咖啡,此时你并不需要在吧台坐等,你只需要回到位子上玩手机,等咖啡好了服务员会叫你。不光叫你,其他人的咖啡好了,服务员也会叫他们来取。

还有,DOM 事件就是最常用的观察者模式

示例:

// 主题 保存状态,状态变化之后触发所有观察者对象
class Subject {
    constructor() {
        this.state = 0
        this.observers = \[]
    }
    getState() {
        return this.state
    }
    setState(state) {
        this.state = state
        this.notifyAllObservers()
    }
    notifyAllObservers() {
        this.observers.forEach(observer => {
            observer.update()
        })
    }
    attach(observer) {
        this.observers.push(observer)
    }
}
// 观察者
class Observer {
    constructor(name, subject) {
        this.name = name
        this.subject = subject
        this.subject.attach(this)
    }
    update() {
        console.log(`${this.name} update, state: ${this.subject.getState()}`)
    }
}
// 测试
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('02', s)

s.setState(12)

还有,Vue React 的生命周期,也是观察者模式

  • DOM 事件
  • 组件生命周期
  • Vue 组件更新过程
  • 异步回调
  • MutationObserver

vs 发布订阅模式

发布订阅模式,没有在传统 23 种设计模式中,它是观察者模式的另一个版本。

// 绑定
event.on('event-key', () => {
    // 事件1
})
event.on('event-key', () => {
    // 事件2
})

// 触发执行
event.emit('event-key')

image.png

观察者模式

  • Subject 和 Observer 直接绑定,中间无媒介
  • addEventListener 绑定事件

发布订阅模式

  • Publisher 和 Observer 相互不认识,中间有媒介
  • event 自定义事件

一个很明显的特点:发布订阅模式需要在代码中触发 emit ,而观察者模式没有 emit

发布订阅模式发布订阅模式场景

自定义事件

Vue3 推荐使用 mitt,文档 github.com/developit/m…

import mitt from 'mitt'

const emitter = mitt() // 工厂函数

emitter.on('change', () => {
    console.log('change1')
})
emitter.on('change', () => {
    console.log('change2')
})

emitter.emit('change')

但是,mitt 没有 once ,需要可以使用 event-emitter www.npmjs.com/package/eve…

import eventEmitter from 'event-emitter' // 还要安装 @types/event-emitter

const emitter = eventEmitter()

emitter.on('change', (value: string) => {
    console.log('change1', value)
})
emitter.on('change', (value: string) => {
    console.log('change2', value)
})
emitter.once('change', (value: string) => {
    console.log('change3', value)
})

emitter.emit('change', '张三')
emitter.emit('change', '李四')

迭代器模式

用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。

for 循环不是迭代器模式

简单的 for 循环并不是迭代器模式,因为 for 循环需要知道对象的内部结构。

如下面的例子

  • 要知道数组的长度
  • 要知道通过 arr[i] 形式来得到 item
const arr = [10, 20, 30]
const length = arr.length
for (let i = 0; i < length; i++) {
    console.log(arr[i])
}

简易迭代器

有些对象,并不知道他的内部结构

  • 不知道长度
  • 不知道如何获取 item
const pList = document.querySelectorAll('p')
pList.forEach(p => console.log(p))

forEach 就是最简易的迭代器

场景

JS 有序对象,都内置迭代器

  • 字符串
  • 数组
  • NodeList 等 DOM 集合
  • Map
  • Set
  • arguments

【注意】对象 object 不是有序结构

Symbol.iterator

每个有序对象,都内置了 Symbol.iterator 属性,属性值是一个函数。 执行该函数讲返回 iterator 迭代器,有 next() 方法,执行返回 { value, done } 结构。

// 拿数组举例,其他类型也一样

const arr = [10, 20, 30]
const iterator = arr[Symbol.iterator]()

iterator.next() // {value: 10, done: false}
iterator.next() // {value: 20, done: false}
iterator.next() // {value: 30, done: false}
iterator.next() // {value: undefined, done: true}

另外,有些对象的 API 也会生成有序对象

const map = new Map([ ['k1', 'v1'], ['k2', 'v2'] ])
const mapIterator = map[Symbol.iterator]()

const values = map.values() // 并不是 Array
const valuesIterator = values[Symbol.iterator]()

// 还有 keys entries

自定义迭代器

interface IteratorRes {
    value: number | undefined
    done: boolean
}

class CustomIterator {
    private length = 3
    private index = 0

    next(): IteratorRes {
        this.index++
        if (this.index <= this.length) {
            return { value: this.index, done: false }
        }
        return { value: undefined, done: true }
    }

    [Symbol.iterator]() {
        return this
    }
}

const iterator = new CustomIterator()
console.log( iterator.next() )
console.log( iterator.next() )
console.log( iterator.next() )
console.log( iterator.next() )

有序结构的作用

for...of

所有有序结构,都支持 for...of 语法

数组操作

数组解构

const [node1, node2] = someDomList

扩展操作符

const arr = [...someDomList]

Array.from()

const arr = Array.form(someDomList)

创建 Map 和 Set

const map = new Map([
    ['k1', 'v1'],
    ['k2', 'v2']
])

const set = new Set(someDomList)

Promise.all 和 Promise.race

Promise.all([promise1, promise2, promise3])
Promise.race([promise1, promise2, promise3])

原型模式

定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象

传统的原型模式就是克隆,但这在 JS 中并不常用。

class CloneDemo {
    name: string = 'clone demo'

    clone(): CloneDemo {
        return new CloneDemo()
    }
}

JS 中并不常用原型模式,但 JS 对象本身就是基于原型的,原型和原型链是非常重要的概念。

场景

最符合原型模式的应用场景就是 Object.create ,它可以指定原型。

const obj1 = {}
obj1.__proto__

const obj2 = Object.create({x: 100})
obj2.__proto__ 

装饰器模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

例如,手机上套一个壳可以保护手机,壳上粘一个指环,可以挂在手指上不容易滑落,这就是一种装饰。手机还是那个手机,手机的功能一点都没变,只是在手机的外面装饰了一些其他附加的功能。日常生活中,这样的例子非常多。

function decorate(phone) {
    phone.fn3 = function () {
        console.log('指环')
    }
}
const phone = {
    name: 'iphone12',
    fn1() {}
    fn2() {}
}
const newPhone = decorate(phone)
//
@decorate
const phone = { ... }

代理模式

为其他对象提供一种代理以控制对这个对象的访问。在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

例如,你通过房产中介买房子,中介就是一个代理。你接触到的是中介这个代理,而非真正的房主。

再例如,明星都有经纪人,某活动想请明星演出,需要对接经纪人。艺术家不方便谈钱,但可以和经纪人谈。经纪人就是一个代理。

class RealImg {
    fileName: string
    constructor(fileName: string) {
        this.fileName = fileName
        this.loadFromDist()
    }
    display() {
        console.log('display...', this.fileName)
    }
    private loadFromDist() {
        console.log('loading...', this.fileName)
    }
}

class ProxyImg {
    readImg: RealImg
    constructor(fileName: string) {
        this.readImg = new RealImg(fileName)
    }
    display() {
        this.readImg.display()
    }
}

const proxImg = new ProxyImg('xxx.png') // 使用代理
proxImg.display()

场景

代理模式在前端很常用

DOM 事件代理

<div id="div1">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
</div>
<button>点击增加一个 a 标签</button>

<script>
    var div1 = document.getElementById('div1')
    div1.addEventListener('click', function (e) {
        var target = e.target
        if (e.nodeName === 'A') {
            alert(target.innerHTML)
        }
    })
</script>

webpack devServer

第一,配置 webpack ,参考 webpack.docschina.org/configurati…

// webpack.config.js
module.exports = {
  // 其他配置...
  devServer: {
    proxy: {
      '/api': 'http://localhost:8081',
    },
  },
};

第二,启动 nodejs 服务,监听 8081 端口

第三,借用 axios 发送请求

import axios from 'axios'

document.getElementById('btn1')?.addEventListener('click', () => {
    axios.get('/api/info')
        .then(res => {
            console.log(res)
        })
})

等等

如何学完忘不掉呢?—— 把设计对应的使用场景,你用起来了,或者知道怎么用了,也就忘不了了。

没有场景的设计模式,你是不可能记住的。