欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 适合前端学习的设计模式有哪些?

程序员文章站 2022-03-21 19:40:07
1.单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点1.场景登录弹框案例:点击登录按钮生成弹窗无论点击多少次,只生成一次2.代理单例使用代理实现单例模式用户可以通过类来创建一个普通的实例也可以通过代理来设置这个实例只能创建一次优点提高了该类的复用性和可扩展性首先创建一个构造函数(我们假设这个单例模式是用来创建div的)定义一个init方法const CreateDiv = function(html) {this.ht...

1.单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点

1.场景

  • 登录弹框案例:
    • 点击登录按钮生成弹窗
    • 无论点击多少次,只生成一次

2.代理单例

  • 使用代理实现单例模式
    • 用户可以通过类来创建一个普通的实例
    • 也可以通过代理来设置这个实例只能创建一次
  • 优点
    • 提高了该类的复用性和可扩展性
  1. 首先创建一个构造函数(我们假设这个单例模式是用来创建div的)
    1. 定义一个init方法
const CreateDiv = function(html) {
	this.html = html
  this.init()
}
CreateDiv.prototype.init = function (){
  let div = document.createElement('div')
  div.innerHTML = this.html
  document.body.appendChild(div)
}
  1. 下面我们来创建一个普通的实例
var a = new CreateDiv('我是普通的div1')
var b = new CreateDiv('我是普通的div2')
// <div>我是普通的div1<div>
// <div>我是普通的div2<div>
  1. 下面我们引入代理,来实现单例模式
// 代理
let ProxySingletonCreateDiv = (function(){
  let instance
  return function(html) {
    if(!instance){
      // 如果不存在instance实例,则创建一个
      instance = new CreateDiv(html)
    }else {
      // 如果存在,则直接返回该实例
      return instance
    }
  }
})
// 创建实例
var a = new CreateDiv('我是普通的div1')
var b = new CreateDiv('我是普通的div2')
// (a === b)

代理单例模式的分层:

  1. 构造函数:
    1. 获取参数
    2. 执行初始化方法
  2. 初始化方法:
    1. 功能实现
  3. 代理:
    1. 对实例是否创建过进行判断
      1. 如果创建过,直接返回实例
      2. 如果没创建过,创建一个实例

3.惰性单例

  • 惰性单例是指在需要的时候才创建对象实例
    • 惰性单例是单例模式的重点,开发中使用频率高

实现

登录框案例:点击多次,只能创建一个登录框

  1. 创建一个只用来返回单例的方法
const getSingle = function (fn) {
  let result
  return function() {
    return result || (result = fn.apply(this,arguments))
  }
}
  1. 创建一个创造登录框的方法
const createLoginLayer = function () {
  let div = document.createElement('div')
  div.innerHTML = '我是登录框'
  div.style.display = 'block'
  document.body.appendChild('div')
  return div
}
  1. 使用
let createSingleLoginLayer = getSingle(createLoginLayer)

document.querySelect('#btn').onclick = function (){
  let loginLayer = createSingleLoginLayer()
  loginLayer.style.display = 'block'
}

惰性单例模式的分层:

  1. 返回单例的方法层
    1. 判断是否已有单例
      1. 使用短路运算
        1. 如果已有直接返回单例
        2. 如果没有创建新单例返回
  2. 功能方法层
    1. 只关注功能实现
    2. Do something
  3. 创建单例层
    1. 使用单例方法创建实例,参数为功能方法

2.策略模式

定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换

1.场景

  • 根据员工的绩效等级来发放年终奖:
    • 绩效S的年终奖为工资的4倍
    • 绩效A的年终奖为工资的3倍
    • 绩效B的年终奖为工资的2倍
  • 代码实现:
// 定义各个等级的计算函数,用一个总的对象包裹起来
const strategies = {
  'S':function(salary) {
    return salary * 4
  },
  'A':function(salary) {
    return salary * 3
  },
  'B':function(salary) {
    return salary * 2
  }
}

// 暴露接口
let calculateBonus = function(level,salary){
   return strategies[levels](salary)
}

// 计算等级S的工资
calculateBonus('S',20000) // 80000
// 计算等级A的工资
calculateBonus('A',10000) // 30000

2.策略模式实现缓动动画

  1. 定义Animate类,并初始化它
const Animate = function (dom) {
  this.dom = dom
  this.startTime = 0 // 动画开始时间
  this.startPos = 0 // dom初始位置
  this.endPos = 0 // dom结束位置
  this.propertyName = null // dom节点需要操控的属性名
  this.easing = null // 缓动算法
  this.duration = null // 动画持续时间
}
  1. 写出动画启动的方法
// 动画的启动方法
Animate.prototype.start = function (propertyName,endPos,duration,easing) {
  this.startTime = Date.now() // 动画启动的时间
  this.startPos = this.dom.getBoundingClientRect()[propertyName] // get方法是原生api,获取当前位置
  this.propertyName = propertyName // 操控的css属性名
  this.endPos = endPos // 目标位置
  this.duration = duration // 动画持续时间
  this.easing = tween[easing] // 缓动算法 tween方法未定义
  const self = this
  let timeId = setInterval(() => {
    if (self.step() === false) {
      // 如果动画已结束,则清楚定时器
      clearInterval(timeId)
    }
  }, 1000)
  }
  1. 写出每一帧需要做的事情
Animate.prototype.step = function () {
  let t = Date.now()
  if (t >= this.startTime + this.duration) {
    // 如果到了预定的时间(当前时间+每次运动的时间),更新css属性
    this.update(this.endPos)
    return false
  }
  let pos = this.easing(
    t - this.startTime, // 总共运行了多久
    this.startPos, // 开始位置
    this.endPos - this.startPos, // 运动的距离
    this.duration // 每帧的时间
  )
  this.update(pos)
}
  1. 动画节点的更新方法
Animate.prototype.update = function (pos) {
  this.dom.style[this.propertyName] = pos + 'px'
}
  1. 实例化并使用它
let div = document.querySelector('.ball')
let animate = new Animate(div)
animate.start('right', 500, 1000, 'strongEaseIn')

3.更广义的策略模式

实际开发中,通常会把使用策略模式来封装一系列的‘业务规则’,只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们

表单校验

4.策略模式优缺点

  • 优点:
    • 利用组合,委托,多态等技术和思想,有效避免多重条件选择语句
    • 对开放-封闭原则完美支持,将算法封装在独立的对象中,使得他们易于切换、理解、扩展
    • 策略模式中的算法也可以复用在系统的其他地方
  • 缺点:
    • 会在程序中增加许多策略类或策略对象
    • 要使用策略模式,必须了解制定好的每一条策略,才能选出最优最合适的策略。这就会把一个策略对象的细节向外界暴露出来,违反了隐藏原则

3.代理模式

定义:代理模式是为一个对象提供一个代用品,以便控制对它的访问

1.保护代理

  • 代理代替主体过滤掉一些不符合规则的请求,或处理一些主体不易处理的请求,这就叫保护代理
// 某明星
let star = {
  name: 'tongmian',
  age: '20',
  height: 175
}
// 经纪人
let proxy = new Proxy(star, {
  get: function (target, key) {
    if (key === 'height') {
      // 有人获取明星的身高时,谎报身高
      return target.height + 5
    }
    return target[key]
  }
})

console.log(proxy.height) // 180

2.虚拟代理

  • 对于一些比较耗性能的请求,代理选择在合适的时机(真正需要的时候)再向主体请求,这就叫虚拟代理
虚拟代理实现图片懒加载(不使用defineproperty和Proxy)
// 创建图片的方法
let myImage = (function(){
  let imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return function(src){
      imgNode.src = src
    }
})()
// 中间的代理
let proxyImage = (function(){
  let img = new Image
  img.onload = function(){
    myImage.setSrc(this.src)
  }
  return function(src){
      myImage.setSrc('占位用的图片地址')
      img.src = src
    }
})()

proxyImage('图片真正的地址')
虚拟代理合并HTTP请求
  • 假设有一个文件列表,选择每个文件时就会发送请求上传文件,那么这样是十分耗性能的
  • 解决的办法有两个:
    • 选择文件时储存文件ID ,在点击确定时同时进行上传(更改了需求)
    • 使用虚拟代理进行合并:
// 文件上传的方法(主体)
let updateFile = function(id) {
  console.log('开始上传文件')
}

// 代理
let proxyUpdateFile = (function(){
  let cache = []
  let timer
  return function(id){
    cache.push(id)
    if(timer){
      return
    }
    timer = setTimeout(function(){
      updateFile(cache.join(','))
      clearTimeout(timer)
      timer = null
      cache.length = 0
    },2000)
  }
})()


let checkbox = document.querySelect('.input')
checkbox.forEach(item=>{
  item.onclick = ()=>{
    if(this.checked === true) {
      proxyUpdateFile(this.id) // 执行代理传入ID
    }
  }
})
虚拟代理实现惰性加载
  • 需求:
    • 一个名为miniConsole的工具
    • 按F2时弹出div框,里面呈现用户使用miniConsole.log打印的内容
  • 梳理:
    • miniConsole.js不需要在一开始就引入,因为用户很可能不会去F2查看
    • 写入一个代理方法,对用户的miniConsole.log操作进行储存
      • 在用户按下F2的时候将miniConsole.js引入
let miniConsole = (function(){
  let cache = []
  let handler = function(e) {
    if(e.keyCode === 113) {
      let script = document.createElement('script')
      script.onload = function(){
        for(let i = 0,fn ; fn = cache[i++]) {
          fn()
        }
      }
      script.src = 'miniConsole.js'
        document.getElementByTagName('head')[0].appendChild(script)
        document.body.removeEventListener('keydown',handler) // 只加载一次
    }
  }
  document.body.addEventListener('keydown',handler,false)
    return {
      log:function(){
        let args = arguments
        cache.push(function(){
          return miniConsole.log.apply(miniConsole,args)
        })
      }
    }
})()
miniConsole.log(1)
// miniConsole.js代码
miniConsole = {
  log:function(){
    console.log(Array.prototype.join.call(arguments))
  }
}

3.缓存代理

  • 缓存代理可以为一些开销大得运算结果提供暂时得储存,在下次运算时,如果传递进来得参数跟之前一直,则直接返回前面储存得计算结果
斐波那契数列计算
  • 普通写法
let count = 0
let fbnqFn = n => {
count = count + 1
if (n === 1 || n === 2) {
return 1
}
return fbnqFn(n - 2) + fbnqFn(n - 1)
}

console.log(fbnqFn(41)) // 165580141
console.log(count+'次计算') // 331160281次计算
  • 代理写法
let count = 0 // 计数

// 本体 只用来计算斐波那契数列 只计算新式子
let fbnqFn = n => {
count = count + 1
if (n === 1 || n === 2) {
return 1
}
return proxyFbnq(n - 2) + proxyFbnq(n - 1) // 这里不再是调用自身
}

// 代理 保存已经计算的式子
let proxyFbnq = (() => {
let temp = {}
return function (n) {
if (n in temp) {
return temp[n]
}
return (temp[n] = fbnqFn(n)) // 新的式子丢回本体计算 并保存
}
})()

console.log(proxyFbnq(41)) // 165580141 
console.log(count + '次计算') // 41次计算
  • 小结:
    • 使用缓存代理处理一些需要大量计算的函数时,计算量大大减少了!

4.迭代器模式

定义:迭代器模式是指提供一种方法(顺序)访问一个对象中的各个元素,而又不需要暴露该对象的内部

小结:

  • 迭代器模式是一种相对简单的模式,简单到我都认为它不是一种设计模式,目前的绝大部分语言都内置了迭代器

5.发布-订阅模式(观察者)

定义:发布-订阅模式又叫观察者模式,它定义了对象间的一种一对多的依赖关系。

当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知

1.场景

  • 小明看中一个小区的房子,售楼中心告诉小明,过一段时间有活动,到时候通知他
    • 小明在售楼中心留下电话,同时留下电话的还有小红,小白
    • 过了一段时间活动开始,售楼中心根据他们留下的电话,依次给他们发送短信通知
    • 小明、小红、小白就是订阅者
    • 售楼中心就是发布者
    • 这就是发布-订阅模式
  • 小甲下载了一个app,第一次打开的时候app提示小甲:是否接受通知
    • 小甲选择了是,同样选择是的还有小乙,小丙,小丁
    • 过了几天app推出了一个满10减5的活动,将这个消息推送到了app上
    • 订阅了app消息的小甲等人的手机在第一时间弹出了这个活动的消息
    • 小甲等人就是订阅者
    • app就是发布者
    • 这就是发布-订阅模式

DOM事件就是最好的发布-订阅模式的实例

// 订阅者 订阅了点击事件,持续监听它
document.querySelect('#btn').onclick = function () {
  alert('我被点击了')
}

// 发布者
document.querySelect('#btn').click() // 模拟用户点击

2.通用的发布-订阅模式

// 拥有发布 和 订阅 功能的一个对象
let event = {
  clientList : {}, // 缓存列表,存放给订阅者发送的信息
  listen: (key,fn)=>{ // 设置key是保证订阅者只接收到它想要的消息
    if(!this.clientList[key]){
      // 如果这个key不存在则新建一个
      this.clientList[key] = []
    }
    this.clientList[key].push( fn ) // 将消息添加到缓存
  },
  trigger: ()=>{
    // 把参数的第一个取出来给key(关键字必须放在参数第一位)
    let key = Array.prototype.shift.call( arguments ) 
    let fns = this.clientList[ key ]
    if( !fns || fns.length === 0 ) { // 如果没有绑定的消息
      return false
    }
    for( let i = 0,fn; fn = fns[i++] ) {
      fn.apply(this,arguments)
    }
  }
}
// 一个拷贝的函数 实际开发中这个可以去掉,直接使用event来实现
let installEvent = (obj)=>{
  for(let i in event){
    obj[i] = event[i]
  }
}
  • 使用通用的发布-订阅模式来处理售楼事件
let saleOffices = {} // 创建一个发布-订阅对象

installEvent(saleOffices)

saleOffices.listen('小户型',(price)=>{console.log('价格' + price)})//小明的订阅
saleOffices.listen('大户型',(price)=>{console.log('价格' + price)})//小明的订阅

// 售楼中心开始发布消息
saleOffices.trigger('小户型',10000)
saleOffices.trigger('大户型',12000)

小明如果突然不想买房,那么他就不愿意再接受售楼处发送的短信了,这时候应该取消订阅

  • 给event添加一个取消订阅的方法
event.remove = (key,fn) {
  let fns = this.clientList[key]
  if( !fns ){ // 如果对应的key没有被人订阅,那么直接返回
    return false
  }
  if( !fn ){ // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
    fns && (fns.length = 0)
  }else {
    for(let i = fns.length -1 ; i >= 0 ; i--){
      let _fn = fns[i]
      if(_fn === fn){
        fns.splice(i,1)
      }
    }
  }
}

6.适配器模式

犹如插头转换器一样,适配器的作用就是将插座的插孔转换成手机充电器可用的插孔

// 插座类
class Power{
  constructor(){
    this.serveVoltage = 110 // 电压为110伏
    this.serveShape = 'triangle' // 插头形状为三角形
  }
}

// 适配器
class Adaptor{
  constructor(){
    // 插向插座的一面
    this.consumeVoltage = 110
    this.consumeShape = 'triangle'
  }
  // 面向手机充电器的一面
  userPower(power){
    if(!power){
      throw new Error('请接入电源')
    }
    if(power.serveVoltage !== this.consumeVoltage || power.serveShage !== this.consumeShape){
      throw new Error('电源规格不对')
    }
    // 修改面向手机充电器一面的接口参数
    this.serveVoltage = 220
    this.serveShage = 'double'
  }
}

// 手机充电器
class User {
  constructor(){
    this.serveVlotage = 220
    this.serveShage = 'double'
  }
  userPower(power){
    if(!power){
      throw new Error('请接入电源')
    }
    if(power.serveVoltage !== this.consumeVoltage || power.serveShage !== this.consumeShape){
      throw new Error('电源规格不对')
    }
  }
}

// 使用
let power = new Power()
let user = new User()
let adaptor = new Adaptor()
adaptor.usePower(power)
user.usePower(adaptor)

使用场景

  1. 开发中需要对接口的提供者和消费者进行兼容时
  2. 对旧老接口进行改造升级,但是无法一次性改造完成时

7.装饰器模式

定义

向一个现有的对象添加新的功能,同时又不改变其结构。

使代码更加的优雅

手机壳就像是装饰器模式,不会改变手机原有的功能,但是增加了防摔,防窥,防水等效果

ES5下的装饰器

// 手机
function Phone(){}
Phone.prototype.mackcall = function(){
  console.log('打电话')
}

// 装饰器
function decorate(target){
  target.prototype.shell = function(){
    console.log('手机壳')
  }
  return target
}

// 使用
Phone = decorate(Phone)
let phone = new Phone()

ES7装饰器语法

目前ES7已加入装饰器语法,但nodejs和浏览器都尚未支持,如果想使用,需要通过babel进行转译

  • 需要安装babel以及另一个babel插件
  1. 用装饰器装饰类
// 装饰器
function writeCode(){
	console.log('写代码')
}

@writeCode // 可以写多个装饰器
class Phone{
  // 手机
}

var phone = new Phone()
phone.writeCode() // 写代码
  1. 用装饰器装饰函数
function log(target,name,descriptor){
  // target此时为Math.prototype
  // name此时为方法名 add
  // descriptor:
  	// value:函数本身
  	// enumerable:是否可遍历
  	// configurable:是否可配置
  	// writale:是否可重写
}

class Math{
  @log
  add(a,b){
    return a+b
  }
}

装饰器工具

  • core-decoratorsnpm包将常用的装饰器工具进行了封装
使用
import {readonly,autobind,deprecate} from 'core-decorators'
// import为ES6语法,需要用webpack编译后引入
// readonly:只读
// autobind:自动绑定,把this强制绑定到实例上
// deprecate:弃用 警告你的用户这个方法即将弃用

8.区分装饰、适配、代理

  • 适配器模式:提供不同的新接口,用作接口转换、处理兼容
  • 代理模式:提供一摸一样的新接口,对行为进行拦截
  • 装饰器模式:直接访问原接口,对原接口进行功能上的增强

9.外观模式

定义

把一堆复杂的接口逻辑放在一起(函数),对外提供一个更高级的统一接口,使外部对于子接口的访问和控制更加容易

使用场景

  • DOM事件监听有兼容问题,使用外观模式对其进行处理
function addEvent(dom,type,fn){
  if(dom.addEventListener){
    // 支持addEventListener方法的浏览器
    dom.addEventListener(type,fn,false)
  }else if(dom.attachEvent){
    // IE浏览器
    dom.attachEvent('on'+type,fn)
  }else {
    // 都不支持
    dom['on'+type] = fn
  }
}
// 不需要考虑兼容性的问题,直接使用即可
addEvent(myDom,'click',function(){ Do Something })

10-状态模式

通常的封装,一般都是封装对象的行为,

在状态模式中是把对象的每种状态都进行独立封装

1.场景

  • 以下情况中,我们应该考虑使用状态模式
  1. 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
  2. 一个操作含有大量的分支语句,而且这些分支语句依赖于该对象的状态
  • 比如:
    • 游戏中一个人物的技能,有冷却、禁用、可用等状态,每次只可能存在一种状态,而且必定是从其中一种状态转换成另一种状态
  • 再比如:
    • 音乐播放器的播放模式有,单曲循环模式、顺序播放模式、列表模式、随机模式,且每次只会存在一种状态,且必定是从其中一种状态切换成另一种状态

2.案例

  • 一盏台灯有关闭,弱光,强光、三种状态
class Lamp{
  constructor(){
    this.offLightState = FSM.offLightState()
  }
  pressButton(){
    this.state.trigger.call(this)
  }
}
// FSM 有限状态机的缩写
// FSM定义:
	// 1.状态总数是有限的
	// 2.任一时刻,只处在一种状态之中
	// 3.某种条件下,会从一种状态转变到另一种状态
const FSM = {
  offLightState:{
    // 关闭状态下触发trigger
    trigger(){
      console.log('弱光')
      this.state = FMS.weakLightState
    }
  },
  weakLightState:{
    // 弱光状态下触发trigger
    trigger(){
      console.log('强光')
      this.state = FMS.strongLightState
    }
  },
  strongLightState:{
    // 强光状态下触发trigger
    trigger(){
      console.log('关闭')
      this.state = FMS.offLightState
    }
  }
}

其它模式等有人看的时候再补充吧

本文地址:https://blog.csdn.net/weixin_45550048/article/details/107381861

相关标签: vue常用 vue