前言:瞬息万变的前端,也具备一些长久的通用型技术,例如性能优化和设计模式
设计模式就犹如游戏里面的连招技巧一样,这些连招经验就是设计模式
1.工厂模式
用一个工厂函数,创建一个实例,封装创建的过程:
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)
}
Factory("yunmu", 18, "coder")
应用场景:
- jQuery
$('div')
创建一个 jQuery 实例 - React 的
React.createElement()
和 Vue 的h()
创建一个 vnode
2.单例模式
提供全局唯一的对象,无论获取多少次:
应用场景
- Vuex Redux 的 store ,全局唯一的
- 全局唯一的 dialog modal
class SingleTon {
private static instance: SingleTon | null = null
private constructor() {}
public static getInstance(): SingleTon {
if(this.instance == null) {
this.instance = new SingleTon()
}
return this.instance
}
fn1() {}
fn2() {}
}
// const s1 = new SingleTon() // Error: constructor of 'singleton' is private
const s2 = SingleTon.getInstance()
s2.fn1()
s2.fn2()
const s3 = SingleTon.getInstance()
s2 === s3 // true
实现一个全局唯一的Modal弹框:
<!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 的形式调用也可以
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>
3.代理模式
- 使用者不能直接访问真实数据,而是通过一个代理层来访问
- ES Proxy 本身就是代理模式,Vue3 基于它来实现响应式
应用场景实践:
- 保护代理:下面模拟现实中婚介所,它们就有点类似于代理层,顾客只能通过婚介所间接获取对方身份
// 规定礼物的数据结构由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]
}
}
})
- 用事件代理(委托)实现多个子元素事件监听
// 获取父元素
const father = document.getElementById('father')
// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
// 识别是否是目标子元素
if(e.target.tagName === 'A') {
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
} )
- 虚拟代理:下图 virtualImage 代替真实 DOM 像图片发送了请求,却未曾渲染
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
}
}
- 缓存代理:空间换时间、下面通过代理计算的同时进行计算结果的缓存
// 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)
}
})()
4.迭代器模式
- 统一遍历的模式
- 之前可以使用 jQuery 的 each 方法遍历不同的集合对象
- ES6 之后 使用 迭代器(Iterator),任何结构只要具备 Symbol.iterator 属性,就可以被for...of循环和迭代器的next方法遍历
- for...of...的背后正是对next方法的反复调用
自定义Iterator:
// 定义生成器函数,入参是任意集合
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()
5.适配器模式
- 对外统一(入参、调用方式、出参)
- axios靠一套 API 不仅能在浏览器端调用,而且在 Node 环境同样适用,靠的就是灵活得适配器使用
- 类似耳机转换头,把一个接口转换为另一个接口
假如我写了这样一个请求库,基于 fetch
export default class HttpUtils {
// get方法
static get(url) {
return new Promise((resolve, reject) => {
// 调用fetch
fetch(url)
.then(response => response.json())
.then(result => {
resolve(result)
})
.catch(error => {
reject(error)
})
})
}
// post方法,data以object形式传入
static post(url, data) {
return new Promise((resolve, reject) => {
// 调用fetch
fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
// 将object类型的数据格式化为合法的body参数
body: this.changeData(data)
})
.then(response => response.json())
.then(result => {
resolve(result)
})
.catch(error => {
reject(error)
})
})
}
// body请求体的格式化方法
static changeData(obj) {
var prop,
str = ''
var i = 0
for (prop in obj) {
if (!prop) {
return
}
if (i == 0) {
str += prop + '=' + obj[prop]
} else {
str += '&' + prop + '=' + obj[prop]
}
i++
}
return str
}
}
// 使用
// 定义目标url地址
const URL = "xxxxx"
// 定义post入参
const params = {
...
}
// 发起post请求
const postResponse = await HttpUtils.post(URL,params) || {}
// 发起get请求
const getResponse = await HttpUtils.get(URL) || {}
这时候如果老板叫我迁移老项目的请求到我这个请求,这下芭比Q了,因为不仅接口名不同,入参方式也不一样
function Ajax(type, url, data, success, failed){
// 创建ajax对象
var xhr = null;
if(window.XMLHttpRequest){
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP')
}
...(此处省略一系列的业务逻辑细节)
var type = type.toUpperCase();
// 识别请求类型
if(type == 'GET'){
if(data){
xhr.open('GET', url + '?' + data, true); //如果有数据就拼接
}
// 发送get请求
xhr.send();
} else if(type == 'POST'){
xhr.open('POST', url, true);
// 如果需要像 html 表单那样 POST 数据,使用 setRequestHeader() 来添加 http 头。
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// 发送post请求
xhr.send(data);
}
// 处理返回数据
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if(xhr.status == 200){
success(xhr.responseText);
} else {
if(failed){
failed(xhr.status);
}
}
}
}
}
// 使用
// 发送get请求
Ajax('get', url地址, post入参, function(data){
// 成功的回调逻辑
}, function(error){
// 失败的回调逻辑
})
这时候我们就可以编写一个适配器函数AjaxAdapter ,并用适配器去承接旧接口的参数,无缝衔接!
// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
const type = type.toUpperCase()
let result
try {
// 实际的请求全部由新接口发起
if(type === 'GET') {
result = await HttpUtils.get(url) || {}
} else if(type === 'POST') {
result = await HttpUtils.post(url, data) || {}
}
// 假设请求成功对应的状态码是1
result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
} catch(error) {
// 捕捉网络错误
if(failed){
failed(error.statusCode);
}
}
}
// 用适配器适配旧的 Ajax 方法
async function Ajax(type, url, data, success, failed) {
await AjaxAdapter(type, url, data, success, failed)
}
6.装饰器模式
- ES 和 TS 的 Decorator 语法就是装饰器模式。可以为 class 和 method 增加新的功能
- 以下代码可以在 ts playground 中运行
// class 装饰器
function logDec(target) {
target.flag = true
}
@logDec
class Log {
// ...
}
console.log(Log.flag) // true
// method 装饰器
// 每次 buy 都要发送统计日志,可以抽离到一个 decorator 中
function log(target, name, descriptor) {
// console.log(descriptor.value) // buy 函数
const oldValue = descriptor.value // 暂存 buy 函数
// “装饰” buy 函数
descriptor.value = function(param) {
console.log(`Calling ${name} with`, param) // 打印日志
return oldValue.call(this, param) // 执行原来的 buy 函数
};
return descriptor
}
class Seller {
@log
public buy(num) {
console.log('do buy', num)
}
}
const s = new Seller()
s.buy(100)
ngular nest.js 都已广泛使用装饰器。这种编程模式叫做AOP 面向切面编程:关注业务逻辑,抽离工具功能
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create(): string {
return 'This action adds a new cat';
}
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
7.原型模式
- 原型模式不仅是一种设计模式,它还是一种编程范式
- 在 JavaScript 里,
Object.create
方法就是原型模式的天然实现,得到相当于继承的对象 - 在 JavaScript 中,我们使用原型模式,是为了得到与构造函数(类)相对应的类型的实例、实现数据/方法的共享
什么是原型和原型链,什么是深浅拷贝,如何实现,我以前的文章均有涉及,此处就不啰嗦了
8.策略模式
- 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换
比如此时要做一个差异化询价,即同一个商品,我通过在后台给它设置不同的价格类型,可以让它展示不同的价格
- 当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折
- 当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折
- 当价格类型为“返场价”时,满 200 - 50,不叠加
- 当价格类型为“尝鲜价”时,直接打 5 折
到这里就很容易去写出 if else 的逻辑去操作价格,这样会随着时间越难越维护,如果使用策略模式的话
抽取四种询价逻辑:
- prePrice - 处理预热价
- onSalePrice - 处理大促价
- backPrice - 处理返场价
- freshPrice - 处理尝鲜价
- askPrice - 分发询价逻辑
此时就只需要询价逻辑的分发 ——> 询价逻辑的执行
// 处理预热价
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)
}
}
但是我们可以使用更为优雅的对象映射来提高可维护性
// 定义一个询价处理器对象
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;
},
};
使用其中某个询价算法的时候:通过标签名去定位就好了:
// 询价函数
function askPrice(tag, originPrice) {
return priceProcessor[tag](originPrice)
}
如果此时新增一个新人价,只需要给 priceProcessor 新增一个映射关系:
priceProcessor.newUser = function (originPrice) {
if (originPrice >= 100) {
return originPrice - 50;
}
return originPrice;
}
9.状态模式
- 状态模式和策略模式长得像并且解决的问题也没有本质去呗
此时要进行咖啡机的代码逻辑编写
- 美式咖啡态(american):只吐黑咖啡
- 普通拿铁态(latte):黑咖啡加点奶
- 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力
这时候又很可能写出 if else 逻辑,但是当我们使用策略模式:
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'); // 我只吐黑咖啡 加点奶
上面也用到了对象映射的方法,但是不同于策略模式的简答询价,changeState函数有时需要拿到咖啡机这个主体的信息,咖啡机和它的状态处理函数建立关联
我们这时把状态-行为映射对象作为主体类对应实例的一个属性添加进去就行了
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');
// 咖啡机现在的牛奶存储量是: 500ml
// 我只吐黑咖啡
// 加点奶
10.观察者模式和发布订阅模式
观察者模式
- 即常说的绑定事件。一个主题,一个观察者,主题变化之后触发观察者执行
// 一个主题,一个观察者,主题变化之后触发观察者执行
btn.addEventListener('click', () => { ... })
发布订阅模式
即常说的自定义事件,一个 event
对象,可以绑定事件,可以触发事件
// 绑定
event.on("event-key", () => {
// 事件1
});
event.on("event-key", () => {
// 事件2
});
// 触发执行
event.emit("event-key");
// 解绑事件,一般组件销毁前进行
event.off("event-key", fn1);
event.off("event-key", fn2);
两者区别
观察者模式
- Subject 和 Observer 直接绑定,中间无媒介
- 如
addEventListener
绑定事件
发布订阅模式
- Publisher 和 Observer 相互不认识,中间有媒介
- 如
eventBus
自定义事件
11.MVC 和 MVVM
MVC 原理
- View 传送指令到 Controller
- Controller 完成业务逻辑后,要求 Model 改变状态
- Model 将新的数据发送到 View,用户得到反馈
MVVM 直接对标 Vue 即可
- View 即 Vue template
- Model 即 Vue data
- VM 即 Vue 其他核心功能,负责 View 和 Model 通讯