设计模式
设计模式
1. 设计模式
2. 工厂模式
3. 单例模式
4. 原型模式
5. 装饰器模式
6. 适配器模式
7. 代理模式
8. 策略模式
9. 状态模式
10. 观察者模式 & 发布订阅模式
11. 迭代器模式
12. vue中用到的设计模式
设计模式
- 设计模式基本原则:单一功能原则、开放封闭原则、里氏替换原则、接口隔离原则、依赖反转原则;
- JavaScript设计模式中,主要用到的设计模式基本都围绕单一功能和开放封闭这两个原则展开;
- 开放封闭原则的内容:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改
- 设计模式的核心思想:封装变化;在实际开发中,不发生变化的代码可以说是不存在的,我们能做的只有将这个变化造成的影响最小化,将变与不变分离,确保变化的部分灵活,不变的部分稳定,这个过程就叫封装变化;
- 23种设计模式分类:
- 创建型:单例模式、原型模式、构造器模式、工厂模式、抽象工厂模式;
- 结构型:桥接模式、外观模式、组合模式、装饰器模式、适配器模式、代理模式、享元模式;
- 行为型:迭代器模式、解释器模式、观察者模式、中介者模式、访问者模式、状态模式、备忘录模式、策略模式、模板方法模式、职责链模式、命令模式;
- 无论是创建型、结构型还是行为型,这些具体的设计模式都是在用自己的方式去封装不同类型的变化 —— 创建型模式封装了创建对象过程中的变化,如工厂模式,它做的事情就是将创建对象的过程抽离;结构型模式封装的是对象之间组合方式的变化,目的在于灵活地表达对象间的配合与依赖关系;而行为型模式则将是对象千变万化的行为进行抽离,确保我们能够更安全、更方便地对行为进行更改。
- 设计模式的核心操作是去观察你整个逻辑里面的变与不变,然后将变与不变分离,达到使变化的部分灵活、不变的地方稳定的目的
工厂模式
- 工厂模式就是将创建对象的过程单独封装;目的,就是为了实现无脑传参;
- 有构造函数的地方,我们就应该想到简单工厂;在写了大量构造函数、调用了大量的 new、自觉非常不爽的情况下,我们就应该思考是不是可以掏出工厂模式重构我们的代码了。
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)
}
// 创建工厂实例
const factory = new Factory('张三', 28, 'coder');
// 打印创建的用户信息
console.log(factory.name);
console.log(factory.age);
console.log(factory.career);
console.log(factory.work);
// Product 类表示产品,Factory 类是工厂,通过 createProduct 方法根据传入的类型创建不同的产品对象。
class Product {
constructor(name) {
this.name = name;
}
showName() {
console.log(`产品名称: ${this.name}`);
}
}
class Factory {
createProduct(type) {
if (type === 'A') {
return new Product('产品 A');
} else if (type === 'B') {
return new Product('产品 B');
}
}
}
// 使用工厂创建产品
const factory = new Factory();
const productA = factory.createProduct('A');
const productB = factory.createProduct('B');
productA.showName();
productB.showName();
- 抽象工厂:一种创建型设计模式,它提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
// 抽象产品 - 椅子
class Chair {
sitOn() {}
}
// 具体产品 - 现代风格椅子
class ModernChair extends Chair {
sitOn() {
console.log('Sitting on a modern chair');
}
}
// 具体产品 - 古典风格椅子
class ClassicChair extends Chair {
sitOn() {
console.log('Sitting on a classic chair');
}
}
// 抽象产品 - 桌子
class Table {
putThingsOn() {}
}
// 具体产品 - 现代风格桌子
class ModernTable extends Table {
putThingsOn() {
console.log('Putting things on a modern table');
}
}
// 具体产品 - 古典风格桌子
class ClassicTable extends Table {
putThingsOn() {
console.log('Putting things on a classic table');
}
}
// 抽象工厂
class FurnitureFactory {
createChair() {}
createTable() {}
}
// 具体工厂 - 现代风格家具工厂
class ModernFurnitureFactory extends FurnitureFactory {
createChair() {
return new ModernChair();
}
createTable() {
return new ModernTable();
}
}
// 具体工厂 - 古典风格家具工厂
class ClassicFurnitureFactory extends FurnitureFactory {
createChair() {
return new ClassicChair();
}
createTable() {
return new ClassicTable();
}
}
// 使用示例
const modernFactory = new ModernFurnitureFactory();
const classicFactory = new ClassicFurnitureFactory();
const modernChair = modernFactory.createChair();
const modernTable = modernFactory.createTable();
const classicChair = classicFactory.createChair();
const classicTable = classicFactory.createTable();
modernChair.sitOn();
modernTable.putThingsOn();
classicChair.sitOn();
classicTable.putThingsOn();
- 抽象工厂和简单工厂对比
- 共同点:尝试去分离一个系统中变与不变的部分;
- 不同点:场景的复杂度;
- 在简单工厂的使用场景里,处理的对象是类,并且是一些非常好对付的类——它们的共性容易抽离,同时因为逻辑本身比较简单,故而不苛求代码可扩展性。
- 抽象工厂本质上处理的其实也是类,但是是一帮非常棘手、繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着千变万化的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色:
- 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里,抽象工厂可以有多个(大家可以想象我们的手机厂后来被一个更大的厂收购了,这个厂里除了手机抽象类,还有平板、游戏机抽象类等等),每一个抽象工厂对应的这一类的产品,被称为“产品族”。
- 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的那些方法,用于创建具体的产品的类。
- 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如操作系统、硬件等),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
- 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 比如我们上文中具体的一种操作系统、或具体的一种硬件等。
- 抽象工厂模式的定义,是围绕一个超级工厂创建其他工厂。
- 在创建组件实例的过程中,从某种角度来看确实可以视作一种工厂模式的应用。当我们使用 Vue 3 的 createApp 或在单文件组件(.vue 文件)中定义组件时,实际上是在一个特定的机制或函数中,根据给定的配置和参数来创建组件的实例。
- 工厂模式是一种创建对象的设计模式,定义了一个用于创建对象的接口,但让子类决定实例化哪一个类,工厂模式把对象的创建和使用分离,使得代码更具灵活性和可维护性;
- 前端开发中,使用工厂模式的场景包括:
- 创建复杂的组件,当组件的创建需要进行一系列复杂的初始化,如设置大量的属性、加载数据、订阅事件等,使用工厂模式可以将这些复杂的创建逻辑封装起来;
- 动态创建不同类型的元素,根据不同的条件或用户操作,需要动态创建不同类型的DOM元素或组件,工厂模式可以根据条件返回相应类型的元素或组件实例;
- 数据模型的创建,如根据不同的数据格式或来源,创建不同的数据模型对象;
- 模拟数据的生成,在开发过程中,需要模拟不同类型和结构的数据,工厂模式可以根据需求创建模拟数据;
- 依赖注入,前端框架中,通过工厂函数来创建和注入依赖的服务或模块,以提高模块的可测试性和可扩展性;
单例模式
- 单例模式是一种设计模式,保证一个类仅有一个实例,并提供一个全局访问点来获取该实例,这样的模式叫单例模式;
- 前端开发中,使用单例模式的场景:
- 全局状态管理,如一个应用的全局配置对象,如主题、语言、服务器地址等配置信息,整个应用都需要访问和可能修改这个配置,使用单例模式可以确保只有一个配置对象存在,避免多份配置导致的不一致;
- 登录模块:处理用户登录状态的模块可以设计为单例,这样在应用的任何部分都能方便地获取当前用户的登录状态,而无须在多个地方传递状态信息;
- 全局事件总线,用于在组件之间进行全局的事件通信,确保只有一个事件总线实例,避免重复注册和触发不必要的事件;
- 数据缓存模块,当需要缓存一些频繁使用但获取成本较高的数据时,使用单例模式来管理缓存,保证数据的一致性和唯一性;
- 浏览器窗口对象的封装:对一些浏览器特定的全局对象(如 window 对象的某些特定功能)进行封装,以提供更统一和可控的接口。
- 单例模式想要做到的是,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。要做到这一点,就需要构造函数具备判断自己是否已经创建过一个实例的能力。
// 如何在 JavaScript 中实现一个简单的单例模式?
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()
s1 === s2 // true
- 单例模式优点:全局访问,方便共享和操作数据;资源控制,避免重复创建导致的资源浪费;单一状态,避免了多个实例状态不同步的问题;
- 单例模式缺点:测试困难;违反单一职责原则;隐藏的依赖关系;不易被替换;
- 请举例说明一个实际的前端项目中,哪些功能可以使用单例模式来实现,并解释原因。
- 全局的模态框Modal组件
- 原因:1.唯一性需求:在一个页面中,通常只需要一个模态框同时显示,如果多个模态框实例同时存在,可能导致用户体验混乱;2.资源节省,避免创建多个模态框实例,节省内存和其他相关资源;3.统一控制,可以通过单例模式对模态框的显示、隐藏、内容设置等行为进行集中和统一的管理,确保整个应用中的模态框行为一致;
class ModalSingleton {
constructor() {
if (!ModalSingleton.instance){
this.isVisible = false;
ModalSingleton.instance = this;
}
return ModalSingleton.instance;
}
showModal(content){
this.isVisible = true;
// 设置模态框内容,执行显示模态框相关操作
}
hideModal() {
this.isVisible = false;
// 执行隐藏模态框相关操作
}
}
const modal = new ModalSingleton();
modal.showModal('模态框内容kkk')
<!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>
- 假设一个前端应用中有一个全局的配置对象,如何使用单例模式来确保只有一个配置对象实例存在?
// ConfigSingleton 类确保了在整个应用中只有一个配置对象实例。通过构造函数中的判断,第一次创建实例时初始化配置,后续创建都返回已存在的实例。提供了 getConfig 方法获取配置,updateConfig 方法更新配置。
class ConfigSingleton {
constructor() {
if (!ConfigSingleton.instance){
this.config = {
theme: 'light',
language: 'en',
}
ConfigSingleton.instance = this;
}
return ConfigSingleton.instance;
}
getConfig() {
return this.config;
}
updateConfig(newConfig){
this.config = {...this.config, ...newConfig}
}
}
const config = new ConfigSingleton();// 获取配置对象实例
const currentConfig = config.getConfig();// 获取配置
config.updateConfig({theme: 'dark'});// 更新配置
- 如果多个模块都需要访问和修改同一个单例对象,如何处理可能出现的并发问题?
- 1.加锁机制,在修改单例对象的关键代码段使用互斥锁,确保同一时间只有一个模块能够进行修改操作;2.版本控制,为单例对象添加版本号属性,每次修改时增加版本号,其他模块在访问时先检查版本号,如果版本不一致则重新获取最新数据;3.事务处理,将对单例对象的一组相关修改操作视为一个事务,要么全部修改成功,要么都不生效,以保证数据的一致性;4.数据复制与合并,每个模块在需要修改时获取单例对象的副本进行修改,然后将修改后的副本合并回单例对象,合并时需要处理冲突和合并逻辑;5.消息队列,模块将修改请求放入消息队列,由一个单独的处理程序按顺序处理这些请求,从而避免并发冲突;6.乐观并发控制,模块在读取单例对象时记录一个版本或标记,在修改并提交时检查版本或标记是否有变化,没有变化则允许修改,否则提示冲突并要求重新获取数据再尝试修改;
- 单例模式如何与模块加载系统(如 ES6 模块)相结合?
// Singleton.js
class Singleton {
constructor(){
if (!Singleton.instance){
this.value = 0;
Singleton.instance = this;
}
return Singleton.instance;
}
increment() {
this.value++;
}
getValue(){
return this.value;
}
}
export default new Singleton();
// test.js
import singleton from './Singleton';
console.log(singleton.getValue());//0
singleton.increment();
console.log(singleton.getValue());//1
- Vuex实现了一个全局的Store用于存储应用的所有状态,这个Store的实现,正是单例模式的典型应用;
- 假单例,虽然没有严格遵循单例模式的设计原则,但在实际应用中仍然能够保证实例的唯一性。Vuex 中的 Store 就是这样一个”假单例“—— 尽管在实际应用中通常 Store 只有一个全局实例,但从实现上来看,它并不是一个严格意义上的单例模式。
- install()函数通过拦截 Vue.use(Vuex) 的多次调用,保证了在同一个Vue应用只会安装唯一的一个Vuex实例;而 vuexInit() 函数则保证了同一个Vue应用只会被挂载唯一一个Store。
- 在同一个 Vue 应用中,只会存在一个 Store 实例,但在多个 Vue 应用中,可以存在多个 Store 实例。在不同的 Vue 应用中,当我们想共享唯一的一个 Store 时,仍然需要通过在全局范围内使用单例模式来确保 Store 的唯一性。
- 单例模式和工厂模式在前端开发中的适用场景的不同:
- 单例模式
- 适用场景:1.全局共享的资源或服务;2.唯一性的组件或对象;
- 优点:1.减少资源消耗,避免重复创建相同的对象;2.方便全局访问和管理;
- 缺点:1.可能导致强耦合,因为其他模块对单例有直接依赖;2.单例对象的复杂逻辑可能使代码难以测试和维护;
- 工厂模式
- 适用场景:1.创建复杂的对象系列,当需要创建一系列相关但具有不同配置或属性的对象时,工厂模式可以根据不同的条件生成不同的对象实例。2.解耦对象的创建和使用,将对象的创建逻辑封装在工厂中,使用者不需要关心对象创建的细节。3.动态决定对象的创建,根据运行时的条件或配置来决定创建哪种具体的对象。
- 优点:1.提高了代码的灵活性和可扩展性,方便添加新的产品类型。2.降低了对象创建和使用之间的耦合度。
- 缺点:1.可能会增加代码的复杂性,特别是当工厂类本身变得复杂时。2.对于简单的对象创建,可能会引入不必要的复杂性。
原型模式
- 原型模式,一种编程范式,为了得到与构造函数(类)相对应的类型的实例、实现数据/方法的共享。
- JavaScript 里,Object.create方法就是原型模式的天然实现——准确地说,只要我们还在借助Prototype来实现对象的创建和原型的继承,那么我们就是在应用原型模式。
- 原型编程范式的核心思想就是利用实例来描述对象,用实例作为定义对象和继承的基础,在JavaScript中,原型编程范式的体现就是原型链的继承。
- 原型:在JavaScript在,每个构造函数都有一个prototype属性,它指向构造函数的原型对象,这个原型对象中有一个constructor属性指回构造函数,每个实例都有一个__proto__属性,当我们使用构造函数去创建实例时,实例的__proto__属性就会指向构造函数的原型对象。
- 对JavaScript原型的理解
- 在JavaScript中,原型是实现对象继承和属性共享的重要机制。
- 每个函数在创建时都会自动拥有一个prototype属性,这个属性指向一个对象,称为原型对象。当通过这个函数创建对象实例时,实例会自动拥有一个内部属性__proto__,该属性指向函数的原型对象。
- 通过原型,可以实现属性和方法的共享。如果在对象实例上查找一个属性或方法时没有找到,JavaScript引擎会沿着__proto__链在原型对象上继续查找。
- 原型的优点:1.节省内存,多个实例可以共享原型上的属性和方法,而不是每个实例都拥有一份独立的副本,从而节省内存空间;2.实现继承,可以基于原型来实现对象之间的继承关系,使得子类能够继承父类的属性和方法。
// sayHi方法定义在Person函数的原型对象上,p1、p2这两个实例都可以访问和调用这个方法。
function Person(name){
this.name = name;
}
Person.prototype.sayHi = function(){
console.log(`hi, ${this.name}`)
}
const p1 = new Person('a');
const p2 = new Person('b');
p1.sayHi();
p2.sayHi();
- JavaScript原型链的工作原理是什么
- 当我们访问一个对象的属性或方法时,JavaScript引擎首先在对象本身查找,如果在对象自身没有找到,就会通过对象内部的__proto__指针沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(Object.prototype)仍未找到,此时返回undefined。
- 每个对象都有__proto__指针,除了Object.prototype,它的__proto__为null,标志着原型链的终点。
- 访问一个 JavaScript 实例的属性/方法时,它首先搜索这个实例本身;当发现实例没有定义对应的属性/方法时,它会转而去搜索实例的原型对象;如果原型对象中也搜索不到,它就去搜索原型对象的原型对象,这个搜索的轨迹,就叫做原型链。
function Parent() {}
Parent.prototype.parentMethod = function() {
console.log('Parent method');
};
function Child() {}
Child.prototype = new Parent();
const childInstance = new Child();
- __proto__与prototype关系
- prototype是函数对象的一个属性,而__proto__是对象的一个内部属性。
- prototype属性用于定义通过该函数创建的对象的原型对象,当使用构造函数创建对象时,新对象的__proro__会指向构造函数的prototype对象。
- __proto__建立了对象实例和其构造函数的prototype之间的链接,形成了原型链,使得对象可以访问其原型对象上的属性和方法。
- ps:__proto__是非标准的属性,在现代JavaScript中,更推荐Object.getPrototypeOf()和Object.setPrototypeOf() 方法来操作对象的原型。
- 在 JavaScript 中,原型对象是指与函数的 prototype 属性相关联的对象。当使用构造函数创建对象实例时,这些实例会通过内部的 proto 链接指向构造函数的原型对象。原型对象的主要作用是实现属性和方法的共享。也就是说,如果在原型对象上定义了属性或方法,通过该构造函数创建的所有对象实例都可以访问和使用这些属性和方法。Person.prototype 就是原型对象,sayHello 方法定义在原型对象上,person1 和 person2 都可以调用这个方法。
- 深拷贝
- 只有当你的对象是一个严格的 JSON 对象时,可以顺利使用这个方法
JSON.stringify
const liLeiStr = JSON.stringify(liLei)
const liLeiCopy = JSON.parse(liLeiStr)
// 调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。
function deepClone(obj) {
// 如果是值类型或null,直接return
if(typeof obj !== 'object' || obj === null){
return obj;
}
// 定义结构对象
let copy = {};
// 如果对象是数组,则定义结果数组
if(obj.constructor === Array){
copy = []
}
// 遍历对象的key
for(let key in obj) {
// 如果key是对象的自有属性,递归调用深拷贝方法
if(obj.hasOwnProperty(key)){
copy[key] = deepClone(obj[key])
}
}
return copy;
}
- 浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝;
// 浅拷贝
function shallowClone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
- 常见的深拷贝实现方法:
// 1. JSON.parse(JSON.stringify()) 方法:使用简单.不能处理函数、正则表达式、Symbol 类型等,并且对于包含循环引用的对象会报错。
function cloneJSON(source) {
return JSON.parse(JSON.stringify(source));
}
// 2. 递归实现:可以处理常见的对象和数组。对于大型嵌套对象可能会导致递归爆栈,并且无法处理循环引用。
function deepClone(obj) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
let newObj;
if (Array.isArray(obj)) {
newObj = [];
} else {
newObj = {};
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}
// 3. 为了解决递归爆栈和循环引用的问题,使用循环和栈代替递归
function deepClone(obj) {
const stack = [obj];
const map = new WeakMap();
while (stack.length > 0) {
const current = stack.pop();
if (typeof current!== 'object' || current === null) {
continue;
}
if (map.get(current)) {
continue;
}
map.set(current, true);
let newObj;
if (Array.isArray(current)) {
newObj = [];
} else {
newObj = {};
}
for (let key in current) {
if (current.hasOwnProperty(key)) {
newObj[key] = deepClone(current[key]);
}
}
stack.push(newObj);
}
return obj;
}
// 4. 为了解决递归爆栈和循环引用的问题,利用哈希表处理循环引用:
function deepClone(obj, map = new WeakMap()) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
if (map.has(obj)) {
return map.get(obj);
}
let newObj;
if (Array.isArray(obj)) {
newObj = [];
} else {
newObj = {};
}
map.set(obj, newObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key], map);
}
}
return newObj;
}
- 关于循环引用的问题解决思路有两种,一直是循环检测,一种是暴力破解,
- 其实破解递归爆栈的方法有两条路,第一种是消除尾递归,第二种方法就是干脆不用递归,改用循环,
// 解决了递归爆栈问题,循环引用问题还存在
function cloneLoop(x) {
const root = {};
// 栈
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
代理模式
- 代理模式,在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。如Chrome上网-VPN(虚拟专用网络),即是代理模式的典型案例;
- A 不能直接访问 B,A 需要借助一个帮手来访问 B,这个帮手就是代理器。需要代理器出面解决的问题,就是代理模式发光发热的应用场景。
- ES6中的Proxy:代理器,
const proxy = new Proxy(obj, handler)
,obj是目标对象,handler是一对对象,用来定义代理的行为,当通过proxy去访问目标对象的适合,handler会对我们的行为作一层拦截,每次访问都需要经过handler这个第三方;
- 4种代理类型:事件代理、虚拟代理、缓存代理和保护代理;
- 事件代理,由父元素对事件进行代理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。
- 一个父元素下有多个子元素,希望鼠标点击每个子元素时都能弹出特定的提示,不用代理的化需要在每个子元素上都安装监听函数,子元素多了性能开销就大了,考虑搭配事件本身具有冒泡特性,当点击子元素时,点击事件会冒泡到父元素上,从而被监听到。因此,点击事件的监听函数只需要在div元素上
// 点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。
const father = document.getElementById('father');
father.addEventListener('click', function(e){
// 识别是否是目标子元素
if(e.target.tagName === 'A'){
e.preventDefault();
alert(`${r.target.innerText}`)
}
})
- 懒加载lazy-load,它是针对图片加载时机的优化,在一些图片量比较大的网站,如果尝试在用户打开页面的适合,就把所有的图片资源都加载完毕,那么很可能白屏、卡顿;可以采取先占位后加载的方式来展示图片,在元素露出之前,给它一个div作占位,当它滚动到可视区域内时,再即时地去加载真实的图片资源,既减轻了性能压力,又保住了用户体验;除了图片懒加载,还要一种操作叫图片预加载;
- 预加载是为了避免网络不好或者图片较大时,页面长时间给用户留白的尴尬,常见的操作是先让这个img标签展示一个占位图,然后创建一个Image实例,让这个Image实例的src指向真实的目标图片地址、观察该实例的加载情况,当其对应的真实图片加载完毕后,即已经有了该图片的缓存内容,再将DOM上的img元素的src指向真实的目标图片地址,此时直接去取了目标图片的缓存,所以展示速度会非常块,从占位图到目标图片的时间差会非常小,小到用户注意不到,体验就好了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Lazy-Load</title>
<style>
.img {
width: 200px;
height:200px;
background-color: gray;
}
.pic {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="container">
<div class="img">
<!-- // 注意我们并没有为它引入真实的src -->
<img class="pic" alt="加载中" data-src="./images/1.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/2.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/3.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/4.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/5.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/6.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/7.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/8.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/9.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/10.png">
</div>
</div>
</body>
<script>
// 在懒加载的实现中,有2个关键的数值:
// 1.当前可视区域的高度;`window.innerHeight || document.documentElement.clientHeight`
// 2.元素距离可视区域顶部的高度`getBoundingClientRect()`获取返回元素的大小及其相对于视口的位置。
const imgs = document.getElementsByTagName('img');
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// num统计当前显示到了哪一张图片,避免每次都从第一张图片考试检查是否露出
let num = 0;
function lazyload() {
console.log(num);
for(let i = num;i<imgs.length;i++){
// 用可视区域高度减去元素顶部距离可视区域顶部的高度
let distance = viewHeight - imgs[i].getBoundingClientRect().top;
// 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
if(distance >= 0){
// 给元素写入真实src,展示图片
imgs[i].src = imgs[i].getAttribute('data-src');
// 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
num = i + 1;
}
}
}
window.addEventListener('scroll', lazyload, false)
lazyload();
</script>
</html>
// 预加载
// ProxyImage帮我们调度了预加载相关的工作,可以通过 ProxyImage 这个代理,实现对真实 img 节点的间接访问,并得到我们想要的效果。PreLoadImage专心去做 DOM 层面的事情(真实 DOM 节点的获取、img 节点的链接设置)
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
}
}
// 这个实例中,virtualImage 这个对象是一个“幕后英雄”,它始终存在于 JavaScript 世界中、代替真实 DOM 发起了图片加载请求、完成了图片加载工作,却从未在渲染层面抛头露面。因此这种模式被称为“虚拟代理”模式。
- 缓存代理,应用于一些计算量较大的场景里,当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。
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 = {};
console.log('resultCache: ', resultCache);
return function() {
const args = Array.prototype.join.call(arguments, ',');
if(args in resultCache) {
return resultCache[args]
}
return resultCache[args]=addAll(...arguments)
}
})()
// 类数组转换为数组
// let argsArray = Array.from(arguments);
// let argsArray = Array.prototype.slice.call(arguments);
// let argsArray = [...arguments];
- 所谓“保护代理”,就是在访问层面做文章,在 getter 和 setter 函数里去进行校验和拦截,确保一部分变量是安全的。
策略模式
- 策略模式,一种行为设计模式,定义了一系列算法,并将每个算法封装起来,使得它们可以相互替换;
// 定义一个询价处理器对象
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.newUser = function (originPrice) {
if (originPrice >= 100) {
return originPrice - 50;
}
return originPrice;
}
装饰器模式
- 装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。
- 装饰器模式,一种结构型设计模式,允许向一个现有的对象添加新的功能,同时不改变其结构。装饰器模式动态地将责任附加到对象上,相比于通过子类化来扩展功能,装饰器提供了更灵活的替代方案。
- 装饰器本质上是一个函数,它可以接收以下参数:
- 对于类的装饰器,接收类的构造函数作为参数。
- 对于类成员(方法、属性)的装饰器,接收成员的属性描述符作为参数。
// 组件接口
class Component {
operation() {}
}
// 具体组件
class ConcreteComponent extends Component {
operation() {
console.log('执行具体组件的操作');
}
}
// 装饰器基类
class Decorator extends Component {
constructor(component){
super();
this.component = component;
}
operation(){
this.component.operation();
}
}
// 具体装饰器1
class ConcreteDecoratorA extends Decorator {
operation(){
super.operation();
console.log('添加具体装饰器A的额外操作');
}
}
// 具体装饰器2
class ConcreteDecoratorB extends Decorator {
operation() {
super.operation();
console.log('添加具体装饰器B的额外操作')
}
}
const comp = new ConcreteComponent();
const decoratedCompA = new ConcreteDecoratorA(comp);
const decoratedCompB = new ConcreteDecoratorB(decoratedCompA)
// Component 是组件的接口,定义了一个操作方法。
// ConcreteComponent 是具体的组件,实现了基本的操作。
// Decorator 是装饰器的基类,它持有一个组件对象,并实现了与组件相同的接口。
// ConcreteDecoratorA 和 ConcreteDecoratorB 是具体的装饰器,它们扩展了装饰器的操作,在调用组件的操作后添加了额外的功能。
--
// 一个简单的类装饰器示例
function logClass(target) {
console.log('装饰器被应用到类:', target);
target.prototype.log = function() {
console.log('这是添加的日志方法');
};
}
@logClass
class MyClass {}
const instance = new MyClass();
instance.log();
// 一个方法装饰器的示例:
function logMethod(target, key, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`调用方法 ${key} ,参数: ${JSON.stringify(args)}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
class MyClass {
@logMethod
myMethod(arg) {
console.log(`执行方法 myMethod ,参数: ${arg}`);
}
}
const instance = new MyClass();
instance.myMethod(10);
- 在 Vue 3 中引入了 Composition API ,其中的 provide 和 inject 组合在某些情况下可以被视为一种装饰器模式的应用。
- provide 用于向子孙组件提供数据或方法,inject 用于在子孙组件中接收父组件通过 provide 提供的数据或方法。然而,Vue 本身并没有直接使用类似于 ES7 中那种语法形式的装饰器。
// 类装饰器,一个参数就够了,target 就是被装饰的类本身
function classDecorator(target) {
target.hasDecorator = true
return target
}
// 将装饰器“安装”到Button类上
@classDecorator
class Button {
// Button类的相关逻辑
}
// 方法装饰器,3个参数,第一个参数,被装饰的目标对象,第二个参数,修饰的目标属性属性名,第三个参数,属性描述对象
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的原有逻辑')
}
}
- 装饰器函数调用的时机:装饰器函数执行的时候,Button 实例还并不存在。这是因为实例是在我们的代码运行时动态生成的,而装饰器函数则是在编译阶段就执行了。所以说装饰器函数真正能触及到的,就只有类这个层面上的对象。
- React中的装饰器:HOC
- 高阶组件,就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
class React, {Component} from "react";
const BorderHoc = WrapperComponent => class extends Component {
render() {
return <div style={{border: '1px solid red'}}>
<WrappedComponent />
</div>
}
}
export default BorderHoc;
// 用它来装饰目标组件
import React, {Component} from 'react';
import BorderHoc from './BorderHoc';
// 用BorderHoc装饰目标组件
@BorderHoc
class TargetComponent extends Component {
render() {
// 目标组件具体的业务逻辑
}
}
// export出去的其实是一个被包裹后的组件
export default TargetComponent
适配器模式
- 适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题;
// axios 浏览器端/在Node环境下,都可使用; axios 完美地抹平了两种环境下api的调用差异,靠的正是对适配器模式的灵活运用。
// Make a request for a user with a given ID
axios.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
})
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
状态模式
- 状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
- 状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
- 策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。但策略模式中的行为函数是”潇洒“的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。把状态-行为映射对象作为主体类对应实例的一个属性添加进去就行了
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');
观察者模式 & 发布订阅模式
- 观察者模式
- 观察者模式,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
- 在观察者模式里,至少应该有2个关键角色是一定要出现的-发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类。
- 代表发布者的类Publisher,基本操作是增加订阅者,通知订阅者,移除订阅者;
- 生活中的观察者模式:前端开发A被项目经理B拉进一个需求变更群,群里还有后端开发B、测试C,然后进群之后,项目经理B说需求还待确认,后面跟区域需求方确认好了之后再同步大家,这样前端开发、后端开发、测试都不用立即投入该项目开发,但都记得有这个需求要做,等待项目经理同步。然后几天过去了,项目经理终于确认好了需求,然后在群里同步需求详情,并@每个人,这时被@的人就进行消息和文件查收,根据消息和文件需求信息,投入到各自的开发中。上述的过程,即是观察者模式。
- 钉钉群里,一个需求信息对象对应了多个观察者(技术同学),当需求信息对象的状态发生变化(从无到有)时,产品经理通知了群里的所有同学,以便这些同学接收信息进而开展工作:角色划分 --> 状态变化 --> 发布者通知到订阅者,这就是观察者模式的“套路”。
- 在上述的过程中,需求文档(目标对象)的发布者只有一个——产品经理韩梅梅。而需求信息的接受者却有多个——前端、后端、测试同学,这些同学的共性就是他们需要根据需求信息开展自己后续的工作、因此都非常关心这个需求信息,于是不得不时刻关注着这个群的群消息提醒,他们是实打实的订阅者,即观察者对象。
- 一个需求信息对象对应了多个观察者(技术同学),当需求信息对象的状态发生变化(从无到有)时,产品经理通知了群里的所有同学,以便这些同学接收信息进而开展工作:角色划分 --> 状态变化 --> 发布者通知到订阅者,这就是观察者模式的“套路”。
- 在观察者模式里,至少应该有两个关键角色是一定要出现的——发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类。
- Publisher,代表发布者的类,基本技能:拉群(增加订阅者),然后是@所有人(通知订阅者),踢走项目组成员(移除订阅者)的能力;
// 定义发布者类
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);
})
}
}
- 订阅者,被通知、去执行(本质上是接受发布者的调用,这在Publisher中已经做掉了),既然在Publisher中做的是方法调用,那么在订阅者类里要做的是方法的定义;
// 定义订阅者类
class Observer {
constructor() {
console.log('observer created');
}
update() {
console.log('observer update invoked');
}
}
// 拓展发布者类,来使所有的订阅者来监听某个特定状态的变化
// 定义一个具体的需求文档发布类
class PrdPublisher extends Publisher {
constructor() {
super();
// 初始化需求文档
this.observers = [];
console.log('PrdPublisher created')
}
// 该方法用于获取当前的prdState
getState() {
console.log('PrdPublisher getState invoked');
return this.prdState;
}
// 该方法用于改变prdState的值
setState(state) {
console.log('prdPublisher setState invoked')
this.prdState = state
// 需求文档变更,立刻通知所有开发者
this.notify()
}
}
// 作为订阅方,开发者的任务也变得具体起来:接收需求文档、并开始干活:
class DeveloperObserver extends Observer {
constructor(){
super();
this.prdState = {};
}
update(publisher) {
this.prdState = publisher.getState();
this.work();
}
work() {
const prd = this.prdState;
// ...
}
}
// new 一个 PrdPublisher 对象(产品经理),她可以通过调用 setState 方法来更新需求文档。需求文档每次更新,都会紧接着调用 notify 方法来通知所有开发者,这就实现了定义里所谓的:目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
const A = new DeveloperObserver(); // 创建订阅者1
const B = new DeveloperObserver(); // 创建订阅者2
const C = new DeveloperObserver(); // 创建订阅者3
const KK = new PrdPublisher();
const prd = {
// ...
}
KK.add(A);
KK.add(B);
KK.add(C);
KK.setState(prd);
- vue3中的观察者模式:在vue3中,观察者模式主要体现在其响应式系统中。当创建一个响应式数据时,就相当于创建了一个被观察者,而在组件中使用这些响应式数据的地方,如计算属性、模板渲染等,就相当于观察者;当响应式数据发生变化时,vue3的内部机制会自动通知所有依赖于该数据的观察者进行相应的更新操作。如如果一个计算属性依赖于某个响应式数据,当数据变化时,计算属性会自动重新计算并更新其值。这种观察者模式的实现使得vue3能够高效地处理数据变化和试图更新,开发者无需手动管理数据和视图之间的同步,大大提高了开发效率和代码的可维护性。
- 在 Vue 中,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式。
- 在vue响应式系统的实现逻辑里,有3个关键角色:监听器observer;订阅者watcher;编译器compile;
- observer监听器,不仅是一个数据监听器,还需要对监听到的数据进行转发,也就是说它还是一个发布者;
- watcher订阅者,observer把数据转发给了真正的订阅者watcher对象,watcher接收到新的数据后,会去更新视图;
- compile编译器,MVVM框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建等都归编译器管;
// 实现observer,该方法会对需要监听的数据对象进行遍历,给它的属性加上定制的getter和setter函数,只要这个对象的某个属性发生了改变,就会触发setter函数,进而通知到订阅者,这个setter函数,就是我们的监听器;
// observer方法遍历并包装对象属性
function observe(target){
if(target && typeof target === 'object'){
Object.keys(target).forEach((key) => {
defineReactive(target, key, target[key])
})
}
}
function defineReactive(target, key, val){
const dep = new Dep()
// 属性值也可能是object类型,此时需要调用observe进行递归遍历
observe(val)
// 为当前属性安装监听器
Object.defineProperty(target, key, {
enumerable: true,
configurable: false,
get: function() {
return val;
},
// 监听器函数
set: (value) => {
// 通知所有订阅者
dep.notify()
}
})
}
// 实现Dep
class Dep {
constructor() {
// 初始化订阅队列
this.subs = [];
}
// 增加订阅者
addSub(sub) {
this.subs.push(sub);
}
// 通知订阅者
notify() {
this.subs.forEach((sub) => {
sub.update()
})
}
}
- 发布订阅模式
- Event Bus、Event Emitter,都对应一个共同的角色--全局事件总线;
- 全局事件总线,严格来说不能说是观察者模式,而是发布订阅模式。
- 设计模式面试必考题:Event Bus、Event Emitter的代码实现?
- 在Vue中使用Event Bus来实现组件间的通讯。
- Event Bus/Event Emitter作为全局事件总线,起到一个沟通桥梁的作用。可以理解为一个事件中心,所有的订阅、发布都不能由订阅方和发布方私下沟通,必须委托这个事件中心帮我们实现。如vue中任意组件通信,除了可以使用Vuex,还可以通过Event Bus来实现。
// 全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!
// eventbus.js 创建一个EventBus并导出
const EventBus = new Vue();
export default EventBus;
// 主文件中引入EventBus,并挂载到全局
import bus from './eventbus.js';
Vue.prototype.bus = bus;
// 订阅事件
this.bus.$on('someEvent', func);
// 发布(触发)事件
this.bus.$emit('someEvent', params);
// 实现一个EventBus(重点)
class EventEmitter {
constructor() {
this.handlers = {};// handlers是一个map,用于存储事件与回调之间的对应关系
}
// 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] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
const handler = this.handlers[eventName].slice();
handlers.forEach((callback) => {
callback(...args);
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName];
const index = callbacks.index(cb);
if (index !== -1){
callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb){
const wrapper = (...args) => {
cb(...args);
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
- vue3中用到的发布订阅模式:
- 如在事件总线的实现中,组件可以发布事件,其他组件可以订阅感兴趣的事件;
- 使用 watch 选项来监听一个数据的变化。当数据发生变化时,执行相应的回调函数,这类似于数据变化的“发布”和回调函数的“订阅”。
- 当计算属性所依赖的数据发生变化时,计算属性会重新计算并更新其值,相关使用计算属性的地方也会得到更新。这可以看作是依赖数据变化的“发布”和计算属性及相关使用处的“订阅”。
- 如 mounted、updated 等生命周期钩子,当组件经历特定的生命周期阶段时会被触发执行相应的函数,这也有点类似于一种“发布-订阅”,组件在特定阶段“发布”事件,而开发者在相应的钩子函数中“订阅”并处理逻辑。
- 观察者模式与发布-订阅模式的区别是什么?
- 核心思想、运作机制上没有本质的差别;
- 韩梅梅把所有的开发者拉了一个群,直接把需求文档丢给每一位群成员,这种发布者直接触及到订阅者的操作,叫观察者模式。但如果韩梅梅没有拉群,而是把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者,这种发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。
- 观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者;
- 韩梅梅拉钉钉群的操作,就是典型的观察者模式;而通过EventBus去实现事件监听/发布,则属于发布-订阅模式。
- 在 Vue 3 中,发布-订阅模式和观察者模式有一些相似之处,但也存在明显的区别:
- 相似点:两者都用于处理数据变化时的通知和相应的处理逻辑,以实现组件或模块之间的解耦和通信。
- 不同点
- 角色和关系:
- 观察者模式:通常有被观察的对象(主题)和观察者。观察者直接注册到主题上,主题在状态改变时通知所有注册的观察者。
- 发布-订阅模式:存在发布者、订阅者和事件通道(中间件)。发布者和订阅者不直接交流,而是通过事件通道进行消息传递。
- 解耦程度:
- 观察者模式的解耦程度相对较低,观察者需要知道主题的存在并直接注册。
- 发布-订阅模式的解耦更彻底,发布者和订阅者完全不知道对方的存在,只通过事件通道交互。
- 灵活性:
- 发布-订阅模式更加灵活,支持在运行时动态添加和删除订阅者,以及发布不同类型的事件。
- 观察者模式在添加和删除观察者方面可能相对不太灵活。
- 在 Vue 3 中的体现:
- 自定义事件的处理更接近发布-订阅模式,组件通过 $emit 发布事件,其他组件通过 $on 订阅,它们之间通过 Vue 内部的事件系统(事件通道)进行通信。
- 响应式数据的更新机制更倾向于观察者模式,当响应式数据变化时,直接通知依赖于该数据的计算属性和组件进行更新
- 观察者模式,解决的其实是模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用,两者之间还是有着说不清、道不明的关系。
- 发布-订阅模式,则是快刀斩乱麻了——发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。发布-订阅模式下,实现了完全地解耦。
- 在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦。如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。
迭代器模式
- 迭代器模式,遍历集合的同时,我们不需要关心集合的内部结构。
- 迭代器模式是设计模式中少有的目的性极强的模式。所谓“目的性极强”就是说它不操心别的,它就解决这一个问题——遍历。
- jQuery的迭代器为我们统一了不同类型集合的遍历方式,使我们在访问集合内每一个成员时不用去关心集合本身的内部结构以及集合与集合间的差异,这就是迭代器存在的价值~
// 借助jQuery的each方法,可以用同一套遍历规则遍历不同的集合对象:
const arr = [1,2,3];
const aNodes = document.getElementsByTagName('a');
$.each(arr, function(index, item){
console.log(`数组的第${index}个元素是${item}`)
})
$.each(aNodes, function(index,aNode){
console.log(`DOM类数组的第${index}个元素是${aNode.innerText}`)
})
- ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。
- 在ES6中,针对Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。
const arr = [1, 2, 3]
const len = arr.length
for(item of arr) {
console.log(`当前元素是${item}`)
}
// Symbol.iterator生成了它对应的迭代器对象,通过反复调用迭代器对象的next方法访问了数组成员
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()
// for...of
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 初始化一个迭代结果
let now = { done: false }
// 循环往外迭代成员
while(!now.done) {
now = iterator.next()
if(!now.done) {
console.log(`现在遍历到了${now.value}`)
}
}
// 编写一个迭代器生成函数
function *iteratorGenerator(){
yield '11'
yield '22'
yield '33'
}
const iterator = iteratorGenerator();
iterator.next();
iterator.next();
iterator.next();
// 用ES5去写一个能够生成迭代器对象的迭代器生成函数
// 定义生成器函数,入参是任意集合
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()
vue中用到的设计模式
- 观察者模式:用于响应式数据更新,当数据发生变化时,通知依赖于该数据的组件进行重新渲染;在组件中修改数据,视图会自动更新,无需手动操作 DOM 来反映数据的变化。
- 发布-订阅模式:如在事件总线的实现中,组件可以发布事件,其他组件可以订阅感兴趣的事件;通过 $emit 方法发布自定义事件,使用 $on 方法订阅事件,实现组件之间的交互。
- 工厂模式:根据不同的条件创建不同的对象实例。在创建组件实例时,可以看作是一种工厂模式的应用;在 Vue 组件的注册中,可以根据组件的名称或配置来创建对应的组件实例。
- 单例模式:确保一个类只有一个实例,并提供一个全局访问点。Vuex中的store通常被设计为单例模式,在整个应用中只有一个实例,以保证整个应用共享唯一的状态管理对象.
- 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。vue组件可以由多个子组件组合而成,形成一个层次结构;
- 策略模式:在vue的过渡效果transition中,可以选择不同的过渡策略;
- Vue 2:
- 观察者模式:数据的变化会自动触发视图的更新,实现了数据和视图的双向绑定,这是观察者模式的一种应用。
- 发布-订阅模式:在事件总线(Event Bus)的实现中,组件可以发布事件,其他组件可以订阅这些事件,从而实现组件之间的通信。
- 工厂模式:在创建 Vue 实例、组件实例时,使用了工厂模式的思想。
- Vue 3:
- 组合式 API:提供了更灵活的方式来组织和复用逻辑,体现了组合模式的思想。
- 响应式系统:进一步优化了数据的响应式处理,依然基于观察者模式。
- 依赖注入:在某些情况下,如 provide/inject 机制,类似于依赖注入模式,方便在组件树中共享和传递数据。
参考&感谢各路大神
1. JavaScript设计模式核心原理与应用实践
2. 设计模式之道
3. [ChatGPT4.5]
宝剑锋从磨砺出,梅花香自苦寒来。