“我正在参加「掘金·启航计划」”
TIP 👉 五更钟漏欲相催,四气推迁往复回。帐里残灯才去焰,炉中香气尽成灰。渐看春逼芙蓉枕,顿觉寒销竹叶杯。守岁家家应未卧,相思那得梦魂来。——唐·孟浩然《除夜有怀》
前言
有句话叫“需求指导设计,设计指导开发” —— 这是大厂研发的流程。
你学会了 HTML CSS JS Vue React 等,这些是为了做开发 你学会了设计模式,就能让你做设计(技术方案设计,不是 UI 设计)—— 只有技术大牛才能指导别人做设计,一般人设计不了
所以,设计模式是高级工程师的必备技能,你可以问问你身边的比较厉害的工程师,他们肯定都精通常用的设计模式。
结合场景
你可能会恍然大悟:哇,原来我一直用这些技术,但我之前却不知道对应设计模式的名字。 我们不能光有实践经验,也要适当的进行理论总结。要知道我们所做的事情是对应到哪个知识点,这样才能慢慢的完善自己的知识体系。
工厂模式
工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替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')
观察者模式
- 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)
})
})
等等
如何学完忘不掉呢?—— 把设计对应的使用场景,你用起来了,或者知道怎么用了,也就忘不了了。
没有场景的设计模式,你是不可能记住的。