「学习记录」浅学一下几种前端常用设计模式

228 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

前提

这段时间在code view的时候发现以前写的代码真的一塌糊涂,可读性非常糟糕。

所以后期用了一些设计模式去改造了一下代码

在此记录一下

注意:设计模式是非常值得深入研究的,它不仅仅体现在代码上,在日常生活中也会体现。

设计模式有5大设计原则:

  1. 单一职责原则
  2. 开放封闭原则
  3. 李氏置换原则
  4. 接口独立原则
  5. 依赖导致原则

这些都是概念上的东西,在本文中暂不提及,但还是希望大家可以在开发中使用这些设计模式后,去研究一下这些概念上的东西。

目标

  1. 了解前端常用的一些设计模式

文章示例代码

我的示例代码 示例代码

创建型

这里创建型设计模式主要介绍如下

  1. 工厂模式
  2. 单例模式

工厂模式

工厂模式也细分成了3种

  1. 简单工厂模式
  2. 工厂方法模式
  3. 抽象工厂模式

这里我只简单介绍一下简单工厂模式的用法,这在日常的开发中会经常被用到

场景

假设我们设计一套电子问卷,问卷里有很多种题型,ok这里我们抽象出了一个问题目类Question,由于题目类型很多,包括简答题,多选,单选等,所以Question实例里面有很多属性,如果每次都要new则会非常麻烦,这里我们试着用简单工厂模式去改造它

代码

class Question {
	constructor(index, name) {
		this.index = index;
		this.name = name;
	}
	show() {
		console.log(`${this.index}.${this.name}`);
	}
}
// 简答
function createInputQuestion(index) {
	return new Question(index, '简答题');
}
// 多选
function createMultipleQuestion(index) {
	return new Question(index, '多选题');
}

// 单选
function createSingleQuestion(index) {
	return new Question(index, '单选题');
}

let q1 = createInputQuestion(1)
let q2 = createMultipleQuestion(2)
let q3 = createSingleQuestion(3)
q1.show()
q2.show()
q3.show()

我们可以看到,这里用了三个工厂函数来对new Question的过程进行了封装。

三个函数分别产出三种不同的question,这就是简单工厂模式的一种简单应用场景。

createInputQuestion是生产简答类型的question,我们内部已经预设好了对应的参数,其它两个也是一样的道理。

其它场景也类似,在遇到要使用new的时候,我们就要考虑是否需要使用工厂模式了。

实际运用场景

在知名的前端框架中工厂模式运用非常广泛

react中的createElement

vue中的createElement

vue的异步组件

jq的$('div')

单例模式

单例模式也是运用非常广泛的一种设计模式,在现在的前端开发中也是使用非常频繁的。

可以简单的理解,它最大的特点就是全局实例的唯一性,这使得在现在的前端领域广泛应用。

场景

实现一个全局的登录框,要求全局只有一个登录框实例。

代码

class LoginBox {
	constructor() {
		this.isLogin = false;
	}
	doLogin() {
		console.log(this.isLogin ? '已经登录' : '登录成功');
		this.isLogin = true
	}
}
// 利用闭包
LoginBox.getInstance = (function () {
	let Instance = null;
	return function () {
		return Instance || (Instance = new LoginBox());
	};
})();

let loginInstance1 = LoginBox.getInstance();
let loginInstance2 = LoginBox.getInstance();

loginInstance1.doLogin();
loginInstance2.doLogin();
console.log(loginInstance1 === loginInstance2)


// 登录成功
// 已经登录
// true

我们可以看到,这里利用立即执行函数和闭包实现了一个getInstance的静态方法,这让每次getInstance都会获取到同一个LoginBox的实例,在loginInstance1.doLogin()时,loginInstance2的状态也改变了。console.log(loginInstance1 === loginInstance2)也说明了这两个实例的一致性。

实际运用场景

vuex

redux

购物车

结构型

这里创建型设计模式主要介绍如下

  1. 适配器模式
  2. 装饰器模式

适配器模式

适配器模式在我们日常开发中会经常使用到,而我们却不知道自己用了。

举个生活中的例子,当我们的手机充电器是香港的规格,当我们想要在大陆的插座使用时,需要加一个转接口,这就是一种适配器模式。

适配器模式就是当旧的接口格式与使用者要求的不兼容,为了不影响原来原接口的使用,我们在中间加个转换的接口,保证两个接口都可以正常使用。

场景

现在有一个数字LED显示的需求,输入一个10位以内的整数,要求输出一个数组,不满10位的补0。

比如输入11,输出[0,0,0,0,0,0,0,0,1,1]

代码

class NumberAdapter {
	constructor(value) {
		this.value = value;
	}
	toArray() {
		let length = 10 - this.value.toString().length
		let res = new Array(length).fill(0)
		let res2 = this.value.toString().split('').map(num => parseInt(num))
		return res.concat(res2)
	}
}
// 利用闭包

let obj = new NumberAdapter(11)
console.log(obj.toArray())

// [
// 	0, 0, 0, 0, 0,
// 	0, 0, 0, 1, 1
//   ]

这个需求其实非常常见于一些数字大屏的数据显示,由于后端传过来的数据可能并不是我们需要的格式,这时候我们需要给数据加一个适配器让它适用于我们的视图使用。

实际运用场景

旧接口改造

vue中的computed

装饰器模式

装饰器模式与刚刚的适配器模式有很多人会弄混淆,但可以利用生活中的例子来区分。

如果一个手机出厂时不具备防水功能,我们可以改造它,加一个防水的外壳,这样就在不影响手机原有功能的基础上多了一个防水的功能。

装饰器模式也是为对象添加新的功能,但是不改变其原有的结构与功能。

装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。

场景

目前有一个Cat类,内部有一个walk方法,调用walk方法会打印“walking”。

我们希望在不改动该类的情况下,在调用walk方法后再打印出“喵喵喵”。

代码

class Cat{
    walk(){
        console.log('walking')
    }
}

class Decorater {
    constructor(cat){
        this.cat = cat
    }
    walk(){
        this.cat.walk()
        this.talk()
    }
    talk(){
        console.log('喵喵喵')
    }
}

let cat = new Cat()
cat.walk()

let cat2 = new Decorater(cat)
cat2.walk()

// walking
// walking
// 喵喵喵

这里我们用一个Decorater类去给Cat类增强了功能,但不影响cat的原来的结构与功能。

实际运用场景

ES7装饰器

react高阶组件

外观模式

外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式显著的特点是为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易。

举个生活中的例子,比如一个洗衣机有着很多功能,我们会为它设计一个控制面板作为接口统一管理洗衣机的所有可能,而不需要用户去深入到洗衣机内部进行操作,使得操作更容易。

场景

兼容浏览器的事件模型,用外观模式进行封装

代码


function addEvent(dom, type, fn) {
  if (dom.addEventListener) {  // 支持DOM2级事件处理方法的浏览器
    dom.addEventListener(type, fn ,false);
  }else if (dom.attachEvent){  // 不支持DOM2级但支持attachEvent
    dom.attachEvent('on' + type, fn);
  }else{
    dom['on + type'] = fn; // 都不支持的浏览器
  }
};
const myInput = document.getElementById('myinput');
addEvent(myInput, 'click', function () {
  console.log('绑定click事件');
})

实际运用场景

可运用在一些兼容性的场景

行为型

这里行为型设计模式主要介绍如下

  1. 策略模式
  2. 观察者模式
  3. 发布-订阅模式

策略模式

策略模式也是我们在日常开发中可以经常使用上的一种设计模式,这在一些前端框架中会经常看到。

策略模式是指定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

这会有效地提高代码的复用性,易于拓展新的功能,也有效地避免了多重if...else条件语句出现的情况。

场景

日常生活中,计算绩效工资时,会有A,B,C三种不同等级,分别对应着2,1.5,1三种倍数的绩效工资,如何在不使用if...else的情况下完成不同等级的工资计算工作。

代码

// 策略模式

let salayStrategy = {
	A: function (salay) {
		return salay * 2;
	},
    B: function (salay) {
		return salay * 1.5;
	},
    C: function (salay) {
		return salay * 1;
	},
};

const useSalayStrategy = (level,salay) => {
    return salayStrategy[level](salay)
}

console.log(useSalayStrategy('A',1000))
console.log(useSalayStrategy('B',1000))
console.log(useSalayStrategy('C',1000))

可以看到,我们创建了一个salayStrategy对象,里面包含了三种方法;useSalayStrategy是调用这些对象方法的接口函数。在这里我们一个if...else都没有使用,而且拓展性也很不错,整体代码非常直观。

实际运用场景

表单的校验模式

一些简单的条件分支情况

观察者模式

观察者模式是我们前端经常用到的一种设计方式,在我们熟悉的vue前端框架,这更是响应式系统的核心设计原理。

我们可以这样来简单理解观察者模式,比如在上课的时候有老师A,这个老师就是被观察者,而三个学生ABC就是观察者,但是只有学生A和B在认真听课,即与被观察者老师A建立了联系,组成了一个一对多的关系。当老师A发出做题的命令,学生A和B收到命令就开始做题。这就是现实生活中的一种观察者模式。

场景

实现一个简单的观察者模式

代码

// 观察者模式

class Observer{
    update(Observerd){
        console.log(`${Observerd.name}正在${Observerd.state}。`)
    }

}

class Observerd{
    constructor(name){
        this.name = name
        this.state = '走路'
        this.ObserverArray = []
    }
    setObserver(Observer){
        this.ObserverArray.push(Observer)

    }
    setState(state){
        this.state = state
        this.ObserverArray&&this.ObserverArray.forEach(Observer => {
            Observer.update(this)
        })
    }
}

let newObserverd = new Observerd('小明')
let Observer1 = new Observer()
let Observer2 = new Observer()
let Observer3 = new Observer()
newObserverd.setObserver(Observer1)
newObserverd.setObserver(Observer2)
newObserverd.setObserver(Observer3)

newObserverd.setState('喝水')

可以看到,我们创建了两个类,分别是Observer观察者类与Observerd被观察者类。

setObserver是Observerd被观察者类用来收集观察者Observer的一个方法,所有观察者都会被收集到ObserverArray这个数组中,当触发被观察者的setState方法后,遍历ObserverArray中所有Observer的update方法。

实际运用场景

vue响应式系统

发布-订阅模式

发布-订阅模式的运用场景非常多,也是面试高频考察点。很多人会把发布订阅模式与观察者模式弄混淆,确实这两者看似非常相似,但还是有着本质上的差别。

场景

实现一个简单的发布-订阅模式

代码

// 实现发布-订阅模式

class EventEmitter {
	constructor() {
		this.events = {};
	}
	on(type, callback) {
        if (!this.events) this.events = Object.create(null);
        if(this.events[type]){
            this.events[type].push(callback)
        }else{
            this.events[type] = [callback]
        }
    }
	off(type, callback) {
        if(!this.events[type]){
            return
        }
        this.events[type] = this.events[type].filter(fn => fn !== callback)
    }
	once(type, callback) {
        function fn(){
            callback()
            this.off(type,fn)
        }
        this.on(type,fn)
    }
	emit(type, ...rest) {
        this.events[type] && this.events[type].forEach(fn => {
            fn.apply(this,rest)
        });
    }
}

// test
const eventInstance = new EventEmitter();

const handle = (...rest) => {
	console.log(rest);
};

eventInstance.on('click', handle);

eventInstance.emit('click', 1, 2, 3, 4);

eventInstance.off('click', handle);

eventInstance.emit('click', 1, 2);

eventInstance.once('dbClick', () => {
	console.log(123456);
});
eventInstance.emit('dbClick');
eventInstance.emit('dbClick');

我们定义了一个EventEmitter类,并实现了里面的4个方法:

  1. on():注册事件
  2. once():注册只执行一次的事件
  3. off(): 解除事件
  4. emit():触发事件执行

而this.events = {};为存储事件回调的一个容器,以key-value的形式存储,每个事件key对应的是一组回调函数。

与观察者模式的不同

我们可以分别执行这两个模式的demo细细体会,观察者模式中,被观察者和观察者之间是紧密相关的,代码并不解耦,在事件触发时,需要被观察者去通知每个观察者;而发布-订阅模式则不同,所有事件相关的操作统一由调度中心进行处理,发布方与订阅方解耦,两者互不关心。

实际运用场景

vue 自定义组件事件机制

vue的eventbus

node.js的events

小结

本文只是非常简单的介绍了几种前端在开发过程中常用的设计模式。虽然在开发过程中并不一定需要使用设计模式,但是如果在遇到了类似的业务场景时,我们可以先思考一下能不能使用上这些设计模式,看看用了会不会使代码结构变得更好。设计模式不是盲目的使用,我们应该用在合适的地方。很多设计模式都可以组合使用,这些可能只有在你们的coding中慢慢体会了。