前端设计模式

430 阅读8分钟

前言

由于很多小伙伴看了 《普通人如何进大厂》这篇文章对我的笔记比较感兴趣,所以我花时间整理提炼了一下跟大家分享,不保证内容完全正确,仅供参考哈

本文内容来自修言的《JavaScript 设计模式核⼼原理与应⽤实践》掘金小册子的核心总结,侵权删。

其他笔记传送门

学习目的:提高驾驭技术的能力

具体来说,它分为以下三个层次:

  • 能用健壮的代码去解决具体的问题
  • 能用抽象的思维去应对复杂的系统
  • 能用工程化的思想去规划更大规模的业务

工厂模式

简单工厂- 处理变与不变的

工厂模式:将创建对象的过程单独封装,实现无脑传参,核心:处理变与不变的
改进前:

function Coder(name , age) {
    this.name = name
    this.age = age
    this.career = 'coder' 
    this.work = ['写代码','写系分', '修Bug']
}
function ProductManager(name, age) {
    this.name = name 
    this.age = age
    this.career = 'product manager'
    this.work = ['订会议室', '写PRD', '催更']
}
function Factory(name, age, career) {
    switch(career) {
        case 'coder':
            return new Coder(name, age) 
            break
        case 'product manager':
            return new ProductManager(name, age)
            break
        ...
}

改进后

function User(name,age,career,word){
    this.name = name 
    this.age = age
    this.career = career
    this.word = word 
}

function Factory(name,age,career){
   let word=[]
   switch(career){
     case 'coder':
       word=['写代码','写系分', '修Bug']
       break;
     case 'product manager':
       word=['写原型','催进度']
       break; 
     case 'boss':
       word=['喝茶','见客户','谈合作']
       break; 
  }
 return new User(name,age,career,word)
}

抽象工厂- 开放封闭原则

简单工厂因为没有遵守开放封闭原则, 暴露一个很大的缺陷。例如若我们添加管理层一些考评权限,难道我们要重新去修改Factory函数吗?这样做会导致Factory会变得异常庞大,而且很容易出bug,最后非常难维护

class MobilePhoneFactory {
    // 提供操作系统的接口
    createOS(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
    // 提供硬件的接口
    createHardWare(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
}
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
    createOS() {
        // 提供安卓系统实例
        return new AndroidOS()
    }
    createHardWare() {
        // 提供高通硬件实例
        return new QualcommHardWare()
    }
}

// 定义操作系统这类产品的抽象产品类
class OS {
    controlHardWare() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我会用安卓的方式去操作硬件')
    }
}

class AppleOS extends OS {
    controlHardWare() {
        console.log('我会用🍎的方式去操作硬件')
    }
}
// 定义手机硬件这类产品的抽象产品类
class HardWare {
    // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
    operateByOrder() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体硬件的具体产品类
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('单例方法')
 }
}
let single1=new SingleDog()
let single2=new SingleDog()

single1===single2 // false

单例模式要求不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例

class SingleDog {
 show(){
  console.log('单例方法')
 }
 static getInstance(){
   if(!SingleDog.instance){
    SingleDog.instance = new SingleDog()    
    }
    return SingleDog.instance
  }
}
let single1 = SingleDog.getInstance()
let single2 = SingleDog.getInstance()
single1===single2 // true

单例实际应用 vuex

Store 存放共享数据的唯一数据源,要求一个 Vue 实例只能对应一个 Store,即Vue 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件

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)
}

装饰器模式 - 实现只添加不修改

拓展弹窗功能

// 定义打开按钮
class OpenButton {
    // 点击后展示弹框(旧逻辑)
    onClick() {
        const modal = new Modal()
    	modal.style.display = 'block'
    }
}

// 定义按钮对应的装饰器
class Decorator {
    // 将按钮实例传入
    constructor(open_button) {
        this.open_button = open_button
    }
    onClick() {
        this.open_button.onClick()
        // “包装”了一层新逻辑
        this.changeButtonStatus()
    }
    changeButtonStatus() {
        this.changeButtonText()
        this.disableButton()
    }
    disableButton() {
        const btn =  document.getElementById('open')
        btn.setAttribute("disabled", true)
    }
    changeButtonText() {
        const btn = document.getElementById('open')
        btn.innerText = '快去登录'
    }
}

const openButton = new OpenButton()
const decorator = new Decorator(openButton)

document.getElementById('open').addEventListener('click', function() {
    // openButton.onClick()
    // 此处可以分别尝试两个实例的onClick方法,验证装饰器是否生效
    decorator.onClick()
})

es7 装饰器

function classDecorator(target) {
    target.hasDecorator = true
  	return target
}

// 将装饰器“安装”到Button类上
@classDecorator
class Button {
    // Button类的相关逻辑
}

装饰器的原型

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

装饰器只能用于类或者类的方法原因:普通函数声明会提升

实现 react 的高阶函数

定义装饰器

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

适配器模式 - 兼容就是一把梭

原 ajax 定义

function Ajax(type, url, data, success, failed){
    // 创建ajax对象
    var xhr = null;
    if(window.XMLHttpRequest){
        xhr = new XMLHttpRequest();
    } else {
        xhr = new ActiveXObject('Microsoft.XMLHTTP')
    }
 
   ...(此处省略一系列的业务逻辑细节)
   
   var type = type.toUpperCase();
    
    // 识别请求类型
    if(type == 'GET'){
        if(data){
          xhr.open('GET', url + '?' + data, true); //如果有数据就拼接
        } 
        // 发送get请求
        xhr.send();
 
    } else if(type == 'POST'){
        xhr.open('POST', url, true);
        // 如果需要像 html 表单那样 POST 数据,使用 setRequestHeader() 来添加 http 头。
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        // 发送post请求
        xhr.send(data);
    }
 
    // 处理返回数据
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
            if(xhr.status == 200){
                success(xhr.responseText);
            } else {
                if(failed){
                    failed(xhr.status);
                }
            }
        }
    }
}

fetch 请求封装

export default class HttpUtils {
  // get方法
  static get(url) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url)
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // post方法,data以object形式传入
  static post(url, data) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        // 将object类型的数据格式化为合法的body参数
        body: this.changeData(data)
      })
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // body请求体的格式化方法
  static changeData(obj) {
    var prop,
      str = ''
    var i = 0
    for (prop in obj) {
      if (!prop) {
        return
      }
      if (i == 0) {
        str += prop + '=' + obj[prop]
      } else {
        str += '&' + prop + '=' + obj[prop]
      }
      i++
    }
    return str
  }
}

fetch 兼容 ajax(放弃ajax)

// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
    const type = type.toUpperCase()
    let result
    try {
         // 实际的请求全部由新接口发起
         if(type === 'GET') {
            result = await HttpUtils.get(url) || {}
        } else if(type === 'POST') {
            result = await HttpUtils.post(url, data) || {}
        }
        // 假设请求成功对应的状态码是1
        result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
    } catch(error) {
        // 捕捉网络错误
        if(failed){
            failed(error.statusCode);
        }
    }
}

// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
    await AjaxAdapter(type, url, data, success, failed)
}

代理模式

事件代理:点击子元素,用父元素代理

缓存代理

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)
    }
})()

策略模式 - 消除 if-else 能手

// 定义一个询价处理器对象
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)
}

观察者模式

应用例子:需求发布

流程产品经理开群然后拉人(开发)进群,需求更新时通知开发者,开发者接到到需求开始工作。

    // 发布者
    class Publisher{
      constructor(){
        this.observers=[]
      }
      add(observer){
        this.observers.push(observer)
        
      }
      remove(observer){
        const index =this.observers.findIndex(item=>item===observer)
        this.observers.splice(index,1)
      }
      notify(state){
        this.observers.forEach(observer=>observer.update(state))
      }
    }
  // 观察者
    class Observer{
      constructor() {
        console.log('Observer created')
    }
      update(){
        console.log('我干活辣')
      }
    }
    // 产品经理类 (文档发布者)
    class Prdpublisher extends Publisher{
      constructor(){
        super()
        this.prdState = {}
        this.observers=[]
        console.log('Prdpublisher created')
      }
      getState(){
        return this.prdState
      }
      setState(state){
        console.log('this.observers',this.observers)
       this.prdState = state
       this.notify(state)
      }
    }
    // 开发者类
    class DeveloperObserver extends Observer{
     constructor(){
       super()
       this.prdState={}
       console.log('DeveloperObserver created')
     }
     update(state){
      this.prdState = state
      this.word()
     }
     word(){
       const prdState = this.prdState
       console.log('start wording',prdState)
     }
    }
    const observeA = new DeveloperObserver() //前端
    const observeB = new DeveloperObserver() //后端
    const lilei = new Prdpublisher() // 产品经理
    lilei.add(observeA) // 拉群
    lilei.add(observeB)
    let prd={
      // 需求内容
      'login':3,
      'auth':2
    }
    // 更新需求 同时通知开发者
    lilei.setState(prd)

vue 响应试原理-观察者模式

观察者模式和发布-订阅模式的区别是:

发布-订阅模式,事件的注册和触发发生在独立于双方的第三方平台。观察者模式:发布者会直接触及到订阅者

 function observe(target){
     if(target && typeof target==='object'){
       Object.keys(target).forEach(key=>{
        defineReactive(target,key,target[key])
       })
     }
    }
    function defineReactive(target, key,val) {
        observe(val)
        let dep = new Dep()
        Object.defineProperty(target, key, {
          enumerable:true,
          configurable:false,
          get() {
            return val
          },
          set(value) {
            val=value
            dep.notify()
          },
        });
      }
    class Dep {
      constructor(dep) {
        this.deps = [];
      }
      add(dep) {
        this.deps.push(dep);
      }
      notify() {
        this.deps.forEach((dep) => dep.update());
      }
    }

vue eventBus

 class EventEmitter {
      constructor() {
        this.handlers = {};
      }
      on(eventName, cb) {
        if (!this.handlers[eventName]) {
          this.handlers[eventName] = [cb];
        } else {
          this.handlers[eventName].push(cb);
        }
      }
      emit(eventName, data) {
        if (!this.handlers[eventName]) {
          console.log('监听器不存在');
          return;
        }
        const events = this.handlers[eventName].slice();
        events.forEach((cb) => {
          cb(data);
        });
      }
      off(eventName, cb) {
        if (!this.handlers[eventName]) {
          console.log('监听器不存在');
          return;
        }
        const callBacks = this.handlers[eventName];
        const index = callBacks.findIndex((item) => item === cb);
        callBacks.splice(index, 1);
      }
      once(eventName, cb) {
        const wrap = (data) => {
          let fn = this.handlers[eventName];
          cb(data);
          this.off(eventName, fn);
        };
       
        this.on(eventName, wrap);
      }
    }
    let eventBus = new EventEmitter();
    eventBus.once('success', (data) => {
      console.log('data', data);
    });
    eventBus.emit('success', 456);
    eventBus.emit('success', 577);