前端设计模式总结
菜鸟看书的一点总结,请大佬给出宝贵意见
创建型
工厂模式
- 创建对象的工厂,使用者不必关心对象生成的过程,也就是不需要显示的调用new 操作符,只需要调用对象工厂暴露出来的创建对象的方法,并传入需要创建的对象的类型;缺点是扩展该工厂需要往工厂里不断加入子类,会使代码越来越臃肿
抽象工厂模式
- 在工厂模式的基础上,有多个工厂,每个工厂负责创建同类型的对象, 抽象工厂实现了获取每个工厂实例的接口,使用者可以调用对应的方法获取对应类型工厂实例,使用该工厂可以创建对象;缺点和工厂模式一样,扩展麻烦
单例模式
一个类只能被实例化一次,构造函数私有化,在类内部实例化,有多种实现方法
/** java 建议写法 类加载时就初始化,浪费内存 线程安全(对java来说) **/ class SingleTon { public static getInstance() {return this.instance} private instance = new SingleTon() private constructor() {} } // 使用 SingleTon.getInstance()
/** js 建议写法 使用时再初始化,节约内存 线程不安全(对java来说, js单线程) **/ class SingleTon { public static getInstance() { if (!instance) { this.instance = new SingleTon(); } return this.instance } private instance; private constructor() {} } // 使用 SingleTon.getInstance()
/** js 闭包版本 getInstance 返回的函数保存了对 instance 变量的引用 **/ class SingleTon { public static getInstance = (function () { let instance = null; return function () { if (!instance) { return new SingleTon(); } return instance; } })() private constructor() {} } // 使用 SingleTon.getInstance()
建造者模式
- 把简单对象一步一步组装成复杂对象
- 场景:简单对象固定,简单对象的组合是变化的
原型模式
缓存对象,每次返回对象深拷贝对象(java/C++)
(JS)在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象
在 JavaScript 里,
Object.create
方法就是原型模式的天然实现——准确地说,只要我们还在借助Prototype
来实现对象的创建和原型的继承,那么我们就是在应用原型模式。前端常见的考点就是原型链和深拷贝
结构型
适配器模式
- 作为两个不同接口的桥梁
- 比如Mac Pro 2019 只有四个雷电口,但我想插usb怎么办,这就需要一个适配器来连接两个接口
- 不能滥用,适用于要处理接口冲突,但不能重构老代码的情况下
- 把变化留给自己,把统一留给用户
装饰器模式
允许向一个现有的对象添加新的功能,同时又不改变其结构
装饰器和被装饰的类实现了同一个接口,在装饰器的构造函数中把类的实例传入,装饰器实现该接口时先执行传入类实例的方法,再执行一系列扩展的方法
ES7 @语法糖支持装饰器
// 以下是 ts 语法 interface BaseFunction { move: () => void; } // 比如有个机器人 实现 基本功能 move class Robot implements BaseFunction { public move() { console.log('move'); } } // 然后有高级需求 机器人需要边移动边跳舞并大笑 // 基于开闭原则 我们不修改原来的 机器人类 interface AdvanceFunction { dance: () => void; laugh: () => void; } class Decorator implements BaseFunction, AdvanceFunction { private instance; constructor(instance: BaseFunction) { this.instance = instance; } public move() { this.instance.move(); this.dance(); this.laugh(); } public dance() { console.log('dance'); } public laugh() { console.log('laugh'); } } const robot = new Robot(); robot.move(); // move const robotDecorator = new Decorator(robot); robotDecorator.move(); // move dance laugh // 只要实现了move 的类的实例都可以当作装饰器构造函数的参数传入以获取高级功能
// ES7 装饰器写法 // ES7 装饰器 分为类装饰器,方法装饰器 // 给move添加额外的动作,所以我们使用方法装饰器 interface BaseFunction { move: () => void; } class Robot implements BaseFunction { @decorator // 装饰move public move() { console.log('move'); } } /** @param target 类的原型对象 class.prototype @param name 修饰的目标属性属性名 @param descriptor 属性描述对象 它是 JavaScript 提供的一个内部数据结构、一个对象,专门用来描述对象的属性。它由各种各样的属性描述符组成,这些描述符又分为数据描述符和存取描述符: 数据描述符:包括 value(存放属性值,默认为默认为 undefined)、writable(表示属性值是否可改变,默认为true)、enumerable(表示属性是否可枚举,默认为 true)、configurable(属性是否可配置,默认为true)。 存取描述符:包括 get 方法(访问属性时调用的方法,默认为 undefined),set(设置属性时调用的方法,默认为 undefined ) **/ function decorator (target, name, descriptor) { // 保存装饰的方法 let originalMethod = descriptor.value // 在这里扩展装饰的方法 descriptor.value = function() { dance(); laugh(); return originalMethod.apply(this, arguments) } return descriptor function dance() { console.log('dance'); } function laugh() { console.log('laugh'); } } const robot = new Robot(); robot.move(); // dance laugh move // 装饰器函数执行的时候,实例还并不存在。这是因为实例是在我们的代码运行时动态生成的,而装饰器函数则是在编译阶段就执行了。所以说装饰器函数真正能触及到的,就只有类这个层面上的对象。
生产实践: REACT的高阶组件
代理模式
代理就像一个中介,处理你和target之间的通信,比如vpn
ES6为代理而生的代理器 —— Proxy
// 第一个参数是我们的目标对象。handler 也是一个对象,用来定义代理的行为。当我们通过 proxy 去访问目标对象的时候,handler会对我们的行为作一层拦截,我们的每次访问都需要经过 handler 这个第三方 const proxy = new Proxy(obj, handler)
业务开发中最常见的四种代理类型:事件代理、虚拟代理、缓存代理和保护代理
事件代理
基于事件的冒泡特性,在子元素上的点击事件会向父级冒泡,所以我们只需要在父元素上绑定一次事件,根据event.target来判断实际触发事件的元素,节省了很多绑定事件的开销
虚拟代理
// 常见案例为图片的预加载 // 图片的预加载指,避免用户网络慢时或者图片太大时,页面长时间给用户留白的尴尬 // 图片URL先指向占位图url, // 在后台新建一个图片实例,该图片实例 的URL指向真实的图片地址, // 当该图片实例加载完毕时再把页面图片的地址指向真实的图片地址, // 这样页面图片就可以直接使用缓存展示, // 因预加载使用的图片实例的生命周期全程在后台从未在渲染层面抛头露面。 // 因此这种模式被称为“虚拟代理”模式 class LoadImage { constructor(imgNode: Element) { // 获取真实的DOM节点 this.imgNode = imgNode } // 操作img节点的src属性 setSrc(imgUrl) { this.imgNode.src = imgUrl } } class PreLoadProxy { // 占位图的url地址 static LOADING_URL = 'xxxxxx' private targetImage: LoadImage; constructor(targetImage: LoadImage) { this.targetImage = targetImage } // 该方法主要操作虚拟Image,完成加载 setSrc(targetUrl): Promise<boolean> { // 真实img节点初始化时展示的是一个占位图 this.targetImage.setSrc(ProxyImage.LOADING_URL) // 创建一个帮我们加载图片的虚拟Image实例 const virtualImage = new Image() return new Promise((resolve, reject) => { // 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url virtualImage.onload = () => { this.targetImage.setSrc(targetUrl); resolve(true); } virtualImage.onerror = () => { reject(false); } // 设置src属性,虚拟Image实例开始加载图片 virtualImage.src = targetUrl; }); } } const imageList: Element[] = Array.from(document.getElementByClassName('preload-image'))); Promise.all(imageList.map(image => new PreLoadProxy(new LoadImage(image)).setSrc('realUrl'))) .then() .catch()
缓存代理
interface Cal { addAll: (...args: number[]) => : number } // 缓存上一次的结果 // 比如一个累加器 class Calculator implements Cal { addAll(...args: number[]): number { if (!args) { return 0; } return args.reduce((pre, next) => pre + next, 0) } } const calculator = new Calculator() // 连续执行两次相同的累加函数会遍历两次 calculator.addAll(1, 2, 3, 5); calculator.addAll(1, 2, 3, 5); class CacheProxy implements Cal { private caches: {[key: string]: number} = {} private target: Cal; constructor(cal: Cal) { this.target = cal; } addAll(...args: number[]): number { const key = args.join(); if (this.caches[key] === undefined) { this.caches[key] = this.target.addAll(); } return this.caches[key]; } } const calculator = new Calculator(); const calculatorProxy = new CacheProxy(calculator); // 连续执行两次相同的累加函数第二次会使用缓存 calculatorProxy.addAll(1, 2, 3, 5); calculatorProxy.addAll(1, 2, 3, 5); // 写到这里,我想这不就是给原来的累加器加了一个缓存功能吗? // 加额外的功能又不改变原来的结构这不就符合装饰器模式的定义吗 // 看看是否可以改造成一个缓存装饰器 // 没印象的可以看上一小节 // 装饰器使用闭包保存caches对象 function cacheDecorator(caches = {}) { return function decorator(target, name, descriptor) { // 保存装饰的方法 let originalMethod = descriptor.value // 在这里扩展装饰的方法 descriptor.value = function(...args) { const key = args.join(); if (caches[key] === undefined) { caches[key] = originalMethod.apply(this, args); } return caches[key]; } return descriptor } } // 使用缓存装饰器 class Calculator implements Cal { @cacheDecorator() addAll(...args: number[]): number { if (!args) { return 0; } return args.reduce((pre, next) => pre + next, 0) } } const calculator = new Calculator() // 连续执行两次相同的累加函数同样会缓存 calculator.addAll(1, 2, 3, 5); calculator.addAll(1, 2, 3, 5);
保护代理
就是ES6的Proxy, 劫持对象的属性,VUE3.0的双向绑定实现原理
行为型
策略模式
消灭if else , 定义一系列策略,把它们封装起来,并使它们可替换
// 比如你出去旅游 // 可以选择以下交通工具:步行,自行车,火车,飞机 // 对应需要花的时间 // 步行 48h // 自行车 30h // 火车 8h // 飞机 1h enum Tool { WALK = 'walk', BIKE = 'bike', TRAIN = 'train', PLANE = 'plane' } /** * 计算花费的时间 * if else 一把梭 * @param tool */ function timeSpend(tool: Tool): number { if (tool === Tool.WALK) { return 48; } else if (tool === Tool.BIKE) { return 30; } else if (tool === Tool.TRAIN) { return 8; } else if (tool === Tool.PLANE) { return 1; } else { return NaN } } // 此时新增了一种交通工具 motoBike : 18h // 你就必须去改timeSpend函数,在里面加else if , // 然后你和测试同学说帮忙回归一下整套旅游时间花费逻辑 // 测试同学嘴上说好的,心里说了一句草泥马 // 策略模式重构 // 把策略抽出来并封装成一个个函数 // 使用映射代替if else // 此时新增一种策略,只需要新增一个策略函数并把它放入映射中 // 这样你就可以自信的和测试同学说,我增加了一种旅行方式, // 你只要测新增的方式,老逻辑不需要回归 // 于是你从人人喊打的if else 侠摇身一变成了测试之友 const timeMap = { walk, bike, train, plane } function timeSpend(tool: Tool): number { return timeMap[tool]() || NaN; } function walk() { return 48; } function bike() { return 30; } function train() { return 8; } function plane() { return 1; }
状态模式
一个对象有多种状态,每种状态做不同的事情,状态的改变是在状态内部发生的, 对象不需要清楚状态的改变,它只用调用状态的方法就行,可以看看这个例子加深理解
观察者模式
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
有两个关键角色: 发布者,订阅者
- 发布者添加订阅者
- 发布者发生变化通知订阅者
- 订阅者执行相关函数
// 以vue的响应式更新视图原理为例 // 数据发生变化,更新视图 // observe方法遍历并包装对象属性 function observe(target, cb) { // 若target是一个对象,则遍历它 if(target && typeof target === 'object') { Object.keys(target).forEach((key)=> { // defineReactive方法会给目标属性装上“监听器” defineReactive(target, key, target[key], cb) }) } } // 定义defineReactive方法 function defineReactive(target, key, val, cb) { // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历 observe(val) // 为当前属性安装监听器 Object.defineProperty(target, key, { // 可枚举 enumerable: true, // 不可配置 configurable: false, get: function () { return val; }, // 监听器函数 set: function (value) { // 执行render函数 render(); val = value; } }); } class Vue { constructor(options) { this._data = options.data; //代理传入的对象,数据发生变化,执行render函数 observe(this._data, options.render) } } let app = new Vue({ el: '#app', data: { text: 'text', text2: 'text2' }, render(){ console.log("render"); } })
观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者,
angular的ngrx, react的redux 和 vue的vuex,event-bus都是典型的发布订阅模式