设计模式: 设计模式是解决某个特定场景下对某种问题的解决方案。因此,当我们遇到合适的场景时,可能会条件反射一样自然而然想到符合这种场景的设计模式。
为什么要学习设计模式:
设计原则:
设计原则是设计模式的指导理论,它可以帮助我们规避不良的软件设计。SOLID 指代的五个基本原则分别是:
工厂模式:其实就是将创建对象的过程单独封装。
工厂模式很像我们去餐馆点菜:比如说点一份西红柿炒蛋,我们不用关心西红柿怎么切、怎么打鸡蛋这些菜品制作过程中的问题,我们只关心摆上桌那道菜。在工厂模式里,我传参这个过程就是点菜,工厂函数里面运转的逻辑就相当于炒菜的厨师和上桌的服务员做掉的那部分工作——这部分工作我们同样不用关心,我们只要能拿到工厂交付给我们的实例结果就行了。
简单工厂模式:专门定义一个类(工厂类)来负责创建其他类的实例。可以根据创建方法的参数来返回不同类的实例,被创建的实例通常都具有共同的父类。
举例:公司添加新员工,每一个工种都有所对应的职责,因此将对应职责进行封装。
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 = ['喝茶', '看报', '见客户'] break case 'xxx': // 其它工种的职责分配 ... return new User(name, age, career, work) } //创建实例对象: let person1 = factory('张三','20','coder');
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
抽象工厂模式的定义,是围绕一个超级工厂创建其他工厂。
举例:厂商生产手机,由操作系统和硬件组成,而操作系统和硬件这两样东西背后也存在不同的厂商。
//1.抽象工厂 约定住这台手机的基本组成 class MobilePhoneFactory { // 提供操作系统的接口 createOS(){ throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!"); } // 提供硬件的接口 createHardWare(){ throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!"); } } ___________________________________________________________________________ //2.具体工厂 继承自抽象工厂 class FakeStarFactory extends MobilePhoneFactory { createOS() { // 提供安卓系统实例 return new AndroidOS() } createHardWare() { // 提供高通硬件实例 return new QualcommHardWare() } } ___________________________________________________________________________ //3.抽象产品 //3.1 定义操作系统这类产品的抽象产品类 class OS { controlHardWare() { throw new Error('抽象产品方法不允许直接调用,你需要将我重写!'); } } //3.2 定义手机硬件这类产品的抽象产品类 class HardWare { // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性 operateByOrder() { throw new Error('抽象产品方法不允许直接调用,你需要将我重写!'); } } ___________________________________________________________________________ //4.具体产品 //4.1 定义具体操作系统的具体产品类 class AndroidOS extends OS { controlHardWare() { console.log('我会用安卓的方式去操作硬件') } } class AppleOS extends OS { controlHardWare() { console.log('我会用苹果的方式去操作硬件') } } ... //4.2 定义具体硬件的具体产品类 class QualcommHardWare extends HardWare { operateByOrder() { console.log('我会用高通的方式去运转') } } class MiWare extends HardWare { operateByOrder() { console.log('我会用小米的方式去运转') } } ... ___________________________________________________________________________ // 创建实例对象: 生产手机 const myPhone = new FakeStarFactory() // 让它拥有操作系统 const myOS = myPhone.createOS() // 让它拥有硬件 const myHardWare = myPhone.createHardWare() // 启动操作系统(输出‘我会用安卓的方式去操作硬件’) myOS.controlHardWare() // 唤醒硬件(输出‘我会用高通的方式去运转’) myHardWare.operateByOrder()
简单工厂和抽象工厂对比:
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
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() // true s1 === s2
优点:
缺点:
生产实践:Vuex中的单例模式
近年来,基于 Flux 架构的状态管理工具层出不穷,其中应用最广泛的要数 Redux 和 Vuex。无论是 Redux 和 Vuex,它们都实现了一个全局的 Store 用于存储应用的所有状态。这个 Store 的实现,正是单例模式的典型应用。
// 安装vuex插件 Vue.use(Vuex) // 将store注入到Vue实例中 new Vue({ el: '#app', store })
Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到Vue实例里去。也就是说每 install 一次,都会尝试给 Vue 实例注入一个 Store。
let Vue // 这个Vue的作用和楼上的instance作用一样 ... export function install (_Vue) { // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state) if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } // 若没有,则为这个Vue实例对象install一个唯一的Vuex Vue = _Vue // 将Vuex的初始化逻辑写进Vue的钩子函数里 applyMixin(Vue) }
装饰器模式:不改变原有对象的前提下,动态地给一个对象增加一些额外的功能。
举例:给tree方法加一些装饰器
var tree = { decorate:function (){ } }; tree.getDecorate = function (deco){ tree[deco].prototype = this; return new tree[deco]; }; tree.RedBall = function (){ this.decorate = function (){ this.RedBall.prototype.decorate(); } }; tree.BlueBall = function (){ this.decorate = function (){ this.BlueBall.prototype.decorate(); } }; tree.getDecorate('RedBall'); tree.getDecorate('BlueBall'); tree.decorate();
举例:React中的装饰器:HOC
编写一个高阶组件,它的作用是把传入的组件丢进一个有红色边框的容器里
HOC:
import React, { Component } from 'react' const BorderHoc = WrappedComponent => class extends Component { render() { return <div style={{ border: 'solid 1px red' }}> <WrappedComponent /> </div> } } export default borderHoc
装饰目标组件:
import React, { Component } from 'react' import BorderHoc from './BorderHoc' // 用BorderHoc装饰目标组件 @BorderHoc class TargetComponent extends React.Component { render() { // 目标组件具体的业务逻辑 } } // export出去的其实是一个被包裹后的组件 export default TargetComponent
优点:
缺点:
代理模式:为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。
代理模式像一个房屋中介,买家只能通过中介来买房,代理具备被代理类的所有功能,就像房东有卖房功能,中介也具有卖房功能。
1、虚拟代理
虚拟代理是将调用本体方法的请求进行管理,等到本体适合执行时,再执行。
作用:将开销很大的对象,延迟到真正需要它的时候再执行。
举例:实现图片预加载:
/**在图片预加载中实现虚拟代理 */ var myImage = (function(){ var imageNode = document.createElement('img'); document.body.appendChild(imageNode); return { setSrc: function(src){ imageNode.src = src; } } })() //代理类 var proxyImage = (function(){ var img = new Image(); img.onload = function(){ myImage.setSrc(this.src); } return { setSrc: function(src){ myImage.setSrc('本地的图片地址'); img.src = src; //缓存完毕之后会触发img的onload事件 } }
2、缓存代理
缓存代理可以为开销大的一些运算结果提供暂时性的存储,如果再次传进相同的参数是,直接返回结果,避免大量重复计算。
举例:对传入的参数进行求和:
// 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) } })()
3、保护代理
保护代理主要用于控制不同权限的对象对本体对象的访问权限。
目前实现保护代理时,考虑的首要方案就是 ES6 中的 Proxy。
所谓“保护代理”,就是在访问层面做文章,在 getter 和 setter 函数里去进行校验和拦截,确保一部分变量是安全的。
优点:
缺点:
观察者模式:定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
观察者模式有一个“别名”,叫 发布 - 订阅模式,两个核心的角色要素——“发布者”与“订阅者”。
在这种模式中,并不是一个对象调用另一个对象的方法,而是一个订阅者对象订阅发布者对象的特定活动,并在发布者对象的状态发生改变后,订阅者对象获得通知。订阅者也称为观察者,而被观察的对象称为发布者或主题。当发生了一个重要的事件时,发布者将会通知(调用)所有订阅者,并且可能经常以事件对象的形式传递消息。
1、Vue数据双向绑定(响应式)的实现原理:
在 Vue 中,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的 观察者模式。
首先需要实现一个方法,这个方法会对需要监听的数据对象进行遍历、给它的属性加上定制的 getter 和 setter 函数。这样但凡这个对象的某个属性发生了改变,就会触发 setter 函数,进而通知到订阅者。
2、实现一个Event Bus/ Event Emitter:
Event Bus/Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用。
1、创建一个 Event Bus(本质上也是 Vue 实例)并导出:
const EventBus = new Vue() export default EventBus
2、在主文件里引入EventBus,并挂载到全局:
import bus from 'EventBus的文件路径' Vue.prototype.bus = bus
3、订阅事件:
// 这里func指someEvent这个事件的监听函数 this.bus.$on('someEvent', func)
4、发布(触发)事件:
// 这里params指someEvent这个事件被触发时回调函数接收的入参 this.bus.$emit('someEvent', params)
实现一个Event Bus:
class EventEmitter { constructor() { // handlers是一个map,用于存储事件与回调之间的对应关系 this.handlers = {} } // 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 handlers = this.handlers[eventName].slice() // 如果有,则逐个调用队列里的回调函数 handlers.forEach((callback) => { callback(...args) }) } } // 移除某个事件回调队列里的指定回调函数 off(eventName, cb) { const callbacks = this.handlers[eventName] const index = callbacks.indexOf(cb) if (index !== -1) { callbacks.splice(index, 1) } } // 为事件注册单次监听器 once(eventName, cb) { // 对回调函数进行包装,使其执行完毕自动被移除 const wrapper = (...args) => { cb(...args) this.off(eventName, wrapper) } this.on(eventName, wrapper) } }
拓展:观察者模式与发布-订阅模式的区别是什么?
发布者直接触及到订阅者的操作,叫观察者模式。
发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。
优点:
缺点: