无论如何,你都必须得掌握的js设计模式

420 阅读7分钟

前言

  大家好,我是前端贰货道士。最近这一个月很忙,一直想抽空整理一篇关于如何写出可维护代码的文章,于是这篇文章横空出世。其实使用GPT可以一键生成很多篇文章,但这样的做法的确违背初心(记录并分享一名前端小白的学习心得),于是被我毅然决然的舍弃了。愚以为好文需要长篇大论,侃侃而谈,殊不知好文也可以细水长流,源远流长。既然如此,那就返璞归真,回到最初的状态。听君一席话胜似一席话, 本文会持续更新,有需要的小伙伴可以一键收藏, 蟹蟹大家~

1. 善用策略模式思想(取缔if else的多层嵌套,本质上是switch case,但更简便)

a. 栗子1

  举个栗子: 比如我们封装一个组件,需要同时兼容批量上传效果图批量上传刀版图这两个功能。我们需要依据不同功能,展示不同的title、选择不同的key值以及调用不同的接口fun等。

  一般的写法是根据父组件传入的type,使用三目运算符,定义一个用于判断当前状态的计算属性,然后根据这个计算属性引出其他的计算属性(如title、key、fun)。但这种写法往往不具备可维护性,因为需求的车轮是不断迭代向前的。如果此时产品经理拿出40m长刀,需要在原先的基础上,再添加一个批量上传原图的功能。此时你就只能大费周章地修改各种判断条件逻辑,然后化身容嬷嬷,拿着针对产品经理说,你看我扎不扎你就完了。

  其实这些问题是可以用更好的方法去规避的,这个方法就是使用设计模式中的策略模式的思想去解决问题。而在我们前端小组的日常开发中,这种思想也是我们最常用的设计模式思想。那么该如何解决问题呢?

`定义计算属性:`

props: {
  type: {
    type: String,
    default: 'knifeLayout'
  }
},

computed: {
   `这个计算属性就是我们当前需要处理的对象,包含当前需要使用的各种字段`
   `后续如果需要添加N个功能,都可以继续往下添加配置项,这样写出来的代码维护性较高`
   
    option({ type }) {
      return {
        knifeLayout: {
          title: '批量上传刀版图',
          key: 'knifePath',
          fun: productApi.updateknife,
          showBtn: true,
          `策略模式中,也可以定义函数,比如:`
          beforeOpen: () => { return true }
        },

        showImage: {
          title: '批量上传效果图',
          key: 'showImagePath',
          `xxxApi`
          fun: productApi.updateShowImage,
          showBtn: false
        }
      }[type]
    }
}

Tips: 永远不要使用变量 == 字符串这样的代码作为判断条件,因为字符串是可变的。 比如在上述栗子中,如果我们使用按钮名称去判断当前状态。而产品后续如果需要修改按钮名称,那么我们也需要修改对应的判断条件,这种方式是不可取的,总不能满头黑线呆在原地画个圈圈吧。遇到这种情况,最好的解决方式是,使用唯一属性去作为判断,这种属性不会因为外部因素的变化而变化,最具备稳健性。 那么对应这个案例最好的解决方式是,添加计算属性,对父组件传递来的自定义type进行判断。因为这个prop值是我们自定义的,所以代码会比较稳定。

b. 栗子2

`使用策略模式的思想,定义对象映射表,简化代码:`

computed: {
  message({ activeName }) {
    const mappingList = {
        1: '不同产品组合',
        2: '相同产品组合(图案相同)',
        3: '相同产品组合(图案不同)',
    }
    return mappingList[activeName]
  }
}

c. 栗子3 (栗子2的变种——多层判断)

  假定有这么一个应用场景:平台字段管理详情路由上有type(标识新增或者编辑),platId(标识是属于哪个平台)。对于速卖通和亚马逊平台的新增和编辑,需要请求不同的接口。

  如果按照栗子2的写法,得到的结果会是:

`AliExpress, Amazon为平台id常量,isEdit为根据type得到的计算属性`

const { fun1, fun2, fun3, fun4 } = fieldApi  
const mappingList = {  
    [AliExpress]: this.isEdit ? fun1 : fun2,  
    [Amazon]: this.isEdit ? fun3 : fun4  
}  
const fun = mappingList[platId]

  栗子2的写法其实是不具备可扩展性的。如果我们不止有新增或者编辑这两个功能,比如多出一个复制的功能,上述代码就要重构了,是不利于后期维护的。为此,我们可以通过多层判断来完善栗子2的写法:

const { fun1, fun2, fun3, fun4, fun5, fun6 } = fieldApi  

`方法一(按店铺划分):`
const mappingList = {  
  [AliExpress]: {
     edit: fun1,
     add: fun2,
     
     `新增的功能只需要往下写就好了:`
     copy: fun5
  },
  
  [Amazon]: {
    edit: fun3,
    add: fun4,
     
    `新增的功能只需要往下写就好了:`
    copy: fun6
  }
}  
const fun = mappingList[platId][type]

`方法二(按type划分):`

const mappingList = {  
  edit: {
    [AliExpress]: fun1,
    [Amazon]: fun3
  },
  
  add: {
    [AliExpress]: fun2,
    [Amazon]: fun4
  },
  
  `新增的功能只需要往下写就好了:`
  
  copy: {
    [AliExpress]: fun5,
    [Amazon]: fun6
  }
  
  `...`
}  
const fun = mappingList[type][platId]

d. 栗子4

`借用el-button的组件封装思想,这其实也是利用策略模式,不同按钮根据type值给予动态类名。`
`然后单独分别对这些按钮类的样式进行修改, 后续如果需要添加类名也可以继续往下设置`
`也因为如此,同一个组件就可以拥有不同样式`

`template:`
<div class="search-radio-group" :class="`search-radio-group--${theme}`">

</div>

`script`

props: {
  theme: {
    type: String,
    default: 'default'
  }
}

`scss`
.search-radio-group--default {
  `此处写默认的样式`
}

.search-radio-group--border {
  `此处写带有边框的样式`
}

`后续如果需要增加额外的主题样式,以此格式继续往下配置即可`

2. 观察者模式 vs 发布订阅模式

  观察者模式是发布订阅模式的一种特殊形式,但两者并不完全相同。言简意赅、一针见血来说,观察者模式是去嘉丽敦公司上班,而发布订阅模式是给嘉丽敦公司做外包。

观察者模式(发布者知道观察者的存在)

  • 发布者:

      a. 存在添加订阅者的方法,为多个订阅者提供订阅功能;

      b. 在自身发生改变时,会将变化同时通知给多个订阅者;

  • 订阅者:

      在接收到发布者的变化后,会执行自己的一套方法,对变化做出响应。

`observe.js`

`定义订阅者`

export class Observer {
  constructor(name) {
    this.name = name
  }

  update(cb) {
    cb(this.name)
  }
}

`定义发布者`

export class Subject {
  constructor() {
    this.observerList = []
  }
  
  `添加订阅方法`
  addObserver(observer) {
    this.observerList.push(observer)
  }

  notify(cb) {
    console.log('那个会唱跳rap的男人要开演唱会了!')
    this.observerList.forEach((observer) => observer.update(cb))
  }

  `取消订阅方法`
  unsubscribe(observer) {
    console.log(`${observer}取消订阅啦`)
    const cbIndex = this.observerList.findIndex(({ name }) => name === observer)
    cbIndex != -1 && this.observerList.splice(cbIndex, 1)
  }
 
  `全部取消订阅方法`
  unsubscribeAll() {
    console.log('已全部取消订阅')
    if (this.observerList.length) this.observerList = []
  }
}
<template>
  <div class="app-container">
    <el-button type="primary" size="small" @click="clickHandler">观察者模式</el-button>
  </div>
</template>

<script>
import { Observer, Subject } from './module/observe.js'

export default {
  methods: {
    clickHandler() {
      `创建发布者`
      const subject = new Subject()
      
      `创建订阅者`
      const person1 = new Observer('渣渣辉1')
      const person2 = new Observer('渣渣辉2')
      const person3 = new Observer('渣渣辉3')
      
      `发布者添加需要监听的订阅者`
      subject.addObserver(person1)
      subject.addObserver(person2)
      subject.addObserver(person3)
      
      `发布者通知`
      subject.notify((person) => {
        console.log(`${person}: 那个会唱跳rap的男人是谁? 我都没听说过。不听不听,不如贪玩蓝月,一刀999`)
      })
       
      subject.notify((person) => {
        console.log(`${person}: 原来是cxk,社会我坤哥,我必须得去捧场!`)
      })
      
      subject.unsubscribe('渣渣辉1')

      subject.notify((person) => {
        console.log(`${person}: 社会我坤哥,人帅动作多`)
      })

      subject.unsubscribeAll()

      subject.notify((person) => {
        console.log(`${person}: 社会我坤哥,人帅动作多`)
      })
    }
  }
}
</script>
image.png

发布订阅模式(由任务中心统一管理,发布者不清楚订阅者的存在)

  不同于观察者模式,在发布订阅模式中,多出一个类似中间商的事件调度中心角色。发布者订阅者之间互不认识,老死不相往来,通过事件调度中心进行交流。举栗来说,最近狂飙电视剧爆火,下班后你拖着疲惫的身体,就想一睹狂飙的剧情。那么你会如何处理呢?首先你会登录奇异果平台,在平台上关注并订阅狂飙电视剧的消息。当狂飙电视剧有更新时,出品方会将最新的剧集授权给奇异果平台,平台会以手机短信的方式提醒你电视剧更新了。那么在这个过程中,狂飙出品方就是发布者,关注狂飙电视剧的用户就是订阅者,而充当中介的奇异果就是事件调度中心。 出品方不会直接提示用户电视剧有更新,而用户也只能从平台上观察到剧集的变化,用户的订阅和发布者的通知都在平台上实现。

`定义事件调度中心:`

export class EventCenter {
  constructor() {
    this.observerList = []
  }

  // 订阅方法
  addObserver(observer) {
    this.observerList.push(observer)
  }

  // 发布方法
  notify(cb) {
    console.log('那个会唱跳rap的男人要开演唱会了!')
    this.observerList.forEach((observer) => cb(observer))
  }

  // 取消订阅方法
  unsubscribe(observer) {
    console.log(`${observer}取消订阅啦`)
    const cbIndex = this.observerList.findIndex((e) => e === observer)
    cbIndex != -1 && this.observerList.splice(cbIndex, 1)
  }

  unsubscribeAll() {
    console.log('已全部取消订阅')
    if (this.observerList.length) this.observerList = []
  }
}
<template>
  <div class="app-container">
    <el-button type="primary" size="small" @click="clickHandler">发布订阅模式</el-button>
  </div>
</template>

<script>
import { EventCenter } from './module/eventCenter.js'

export default {
  methods: {
    clickHandler() {
      const eventCenter = new EventCenter()

      eventCenter.addObserver('渣渣辉1')
      eventCenter.addObserver('渣渣辉2')
      eventCenter.addObserver('渣渣辉3')

      eventCenter.notify((person) => {
        console.log(`${person}: 那个会唱跳rap的男人是谁? 我都没听说过。不听不听,不如贪玩蓝月,一刀999`)
      })

      eventCenter.notify((person) => {
        console.log(`${person}: 原来是cxk,社会我坤哥,我必须得去捧场!`)
      })

      eventCenter.unsubscribe('渣渣辉1')

      eventCenter.notify((person) => {
        console.log(`${person}: 社会我坤哥,人帅动作多`)
      })

      eventCenter.unsubscribeAll()

      eventCenter.notify((person) => {
        console.log(`${person}: 社会我坤哥,人帅动作多`)
      })
    }
  }
}
</script>
image.png

结语

  大概就这样吧~