前端设计模式——单例模式、策略模式、订阅发布模式

102 阅读3分钟
  • 设计模式

    • 单例模式

      • 定义:1、只有一个实例 2、可以全局的访问

      • 主要解决:一个全局使用的类,频繁的创建和销毁

      • 何时使用:当你想控制实例的数目,节省系统化资源的时候

      • 如何实现:判断系统是否已经有这个单例,如果有则返回,没有则创建

      • 单例模式有点:内存中只要一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如首页页面的缓存)

      • 使用场景:1、全局的缓存 2、弹窗

      • 例子:登录的弹窗

        • 1、加载完成的时候,已经创建好这个弹窗了,一开始是隐藏的状态,弹窗出现

          缺点:资源的浪费

          var loginLayer = (function () {
              var div = document.createElement('div')
              div.innerHTML = '我是登录的弹窗'
              div.style.display = 'none'
              document.body.appendChild(div)
              return div
          })()
          document.getElementById('button').onClick = function() {
              loginLayer.style.display = 'block'
          }
          
        • 2、点击的时候创建

          缺点:频繁的创建和销毁

          var ceateLoginLayer = function () {
          	var div = document.createElement('div')
          	div.innerHTML = '我是登录的弹窗'
          	div.style.display = 'none'
          	document.body.appendChild(div)
          	return div
          }
          document.getElementById('button').onClick = function() {
          	var loginLayer = ceateLoginLayer()
          	loginLayer.style.display = 'block'
          }
          
        • 3、单例模式

          • 需要一个标记——闭包
          var ceateLoginLayer = (function() {
          	var div
          	// 如果说这个div不被销毁,一直在内存中的话
          	return function() {
          		if(!div) {
          			div = document.createElement('div')
          			div.innerHTML = '我是登录的弹窗'
          			div.style.display = 'none'
          			document.body.appendChild(div)
          		}
          		// 第二次和第一次,都需要return div
          		return div
          	}
          })
          document.getElementById('button').onClick = function() {
          	// 不能在这里ceateLoginLayer()(),会创建多个div
          	var loginLayer = ceateLoginLayer()
          	loginLayer.style.display = 'block'
          }
          
        • 4、改进:单一职责

          • 抽取形成单例,核心代码——闭包
              var getSingle = function (fn) {
                var result
                return function () {
                  if (!result) {
                    result = fn.apply(this, arguments)
                  }
                  return result
                }
              }
              var createLogin = function () {
                var div = document.createElement('div')
                div.innerHTML = '我是登录的弹窗'
                div.style.display = 'none'
                document.body.appendChild(div)
                return div
              }
              var createSingleLogin = getSingle(createLogin)
              document.getElementById('button').onclick = function () {
                var loginLayer = createSingleLogin()
                loginLayer.style.display = 'block'
              }
          
        • 5、es6实现单例模式,不需要使用闭包

          • es5构造函数

            function Person(name, sex){
            	this.name = name
            	this.sex = sex
            }
            Person.prototype.say = function() {
            	console.log('xxx')
            }
            let person1 = new Person('xx', '女')
            
          • es6类

            class Person{
            	constructor(name, sex){
            		this.name = name
            		this.sex = sex
            		// 实际开发中 更多的是做初始化操作
            	}
            	say() {
            		console.log('xxx')
            	}
            }
            let person1 = new Person('xxx', 'man') // new的时候会自动调用constructor方法
            console.log(person1.say === Person.prototype.say) // true
            
            • static静态方法只能被类本身调用,而不能被实例调用
            class Foo {
            	static classMethod() {
            		return 'hello'
            	}
            }
            let foo = new Foo()
            foo.classMethod() // 报错
            Foo.classMethod() // 可以调用
            
          • es6静态方法实现单例模式

            • static静态方法只能被类本身调用,而不能被实例调用
            class singleMode {
            	constructor(name, creator, product) {
            		this.name = name
            		this.creator = creator
            		this.product = product
            	}
            	static getInstance(name, creator, product) {
            		if(!this.instance) {
            			this.instance = new singleMode(name, creator, product)
            		}
            		return this.instence
            	}
            }
            let singleMode1 = singleMode.getInstance('xxx', '111', 'sss')
            let singleMode2 = singleMode.getInstance('xxx2', '1112', 'sss2')
            console.log(singleMode1 === singleMode2) // true, 返回的都是第一次创建的类
            
    • 策略模式

      • 定义一系列的算法,把它们封装起来,并且它们之间可以互相替换

      • 可信:将算法的使用和算法的实现分离开来

      • 例子:年底奖金发放

        • 绩效S:4倍工资;绩效A:3倍工资;绩效B:2倍工资

          • 1、缺点:if语句过多,代码过于庞杂,缺发弹性

            var calculate = function(level, salary) {
            	if (level == 'S') {
            		return salary * 4
            	}
            	if (level == 'A') {
            		return salary * 3
            	}
            	if (level == 'B') {
            		return salary * 2
            	}
            }
            
          • 2、策略模式

            // 适用于所有语言
            var pS = function(){}
            pS.prototype.calculate = function(salary) {
            	return salary * 4
            }
            var pA = function(){}
            pA.prototype.calculate = function(salary) {
            	return salary * 3
            }
            var pB = function(){}
            pB.prototype.calculate = function(salary) {
            	return salary * 2
            }
            var Bouns = function() {
            	this.salary = null
            	this.strategy = null
            }
            Bouns.prototype.setSalary = function(salary){
            	this.salary= salary
            }
            Bouns.prototype.setStrategy= function(strategy){
            	this.strategy = strategy
            }
            Bouns.prototype.getBouns = function() {
            	return this.strategy.calculate(this.salary)	
            }
            var bonus = new Bouns()
            bonus.setSalary(10000)
            bonus.setStrategy(new pS())
            console.log(bonus.getBouns())
            
            // 改写 js的语言特性
            var strategies = {
            	"S": function(salary) {
            		return salary * 4
            	},
            	"A": function(salary) {
            		return salary * 3
            	},
            	"B": function(salary) {
            		return salary * 2
            	}
            }
            var getBouns = function(level, salary) {
            	return strategies[level](salary)
            }
            
      • 例子:表单验证的封装

        • 一般实现

          <form action='xxx.com' method='post' id='registerForm'>
          	请输入用户名:<input type='text' name='username'>
          	请输入密码:<input type='password' name='password'>
          	请输入电话号码:<input type='text' name='phonenumer'>
          	<button>提交</button>
          </form>
          var registerForm = document.getElementById('registerForm')
          registerForm.onsubmit = function() {
          	if(registerForm.username.value === '') {
          		alert('用户名不能为空')
          		return false
          	}
          	if(registerForm.password.value < 6) {
          		alert('密码不能小于6位')
          		return false
          	}
          	if(!/^1[3|5|8][0-9]{9}$/.test(registerForm.phonenumer.value)) {
          		alert('手机号格式不正确')
          		return false
          	}
          }
          
        • 策略模式封装

          // 策略对象:一系列的算法——业务逻辑
          var strategies = {
          	isNoneEmpty: function(value, errorMsg) {
          		if (value === '') {
          			return errorMsg
          		}
          	},
          	minLength: function(value, length, errorMsg) {
          		if (value.length < length) {
          			return errorMsg
          		}
          	},
          	isMobie: function(value, errorMsg) {
          		if (!\^1[3|5|8][0-9]{9}\.test(value)) {
          			return errorMsg
          		}
          	}
          }
          var registorForm = document.getElementById('registorForm')
          // 假设有一个验证类Validator, new Validator()
          var validateFun = function() {
          	var validator = new Validator()
          	// 添加验证规则
          	validator.add(registorForm.username, 'isNoneEmpty', '用户名不能为空')
          	validator.add(registorForm.password, 'minLength:6', '密码不能小于6位')
          	validator.add(registorForm.phonenumer, 'isMobie', '手机号格式不正确')
          	// 开启验证
          	var errorMsg = validator.start()
          	return errorMsg
          }
          registorForm.onsubmit = function() {
          	var errorMsg = validateFun()
          	if(errorMsg) {
          		alert(errorMsg)
          		return false
          	}
          }
          // 封装策略类
          var Validator = function() {
          	// 保存验证规则的数组
          	this.cache = []
          }
          Validator.prototype.add = function(dom, rule, errorMsg) {
          	var arg = rule.split(':')
          	this.cache.push(function() {
          		var strategy = arg.shift() // 用户选择的鹅验证规则
          		arg.unshift(dom.value)
          		arg.push(errorMsg)
          		// 注意:这里已经执行函数了
          		return strategies[strategy].apply(dom, arg)
          		// 或者return strategies[strategy](...arg )
          	})
          }
          Validator.prototype.start = function(){
          	for(var i = 0, vaFunc; vaFunc = this.cache[i++];){
          		var msg = vaFunc()
          		// 正常需要提示一个之后需要终端循环
          		if(msg) {
          			return msg
          		}
          	}
          }
          
    • 发布订阅模式

      • 发布订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知(先订阅再发布)

      • 作用

        1. 支持简单的广播通信:当对象状态发生改变时,会自动通知已经订阅过的对象
        2. 可以应用在异步编程中,替代回调函数:可以订阅ajax之后的事件,只需要订阅自己需要的部分(那么ajax调用发布之后订阅的就可以拿到消息了)(不需要关心对象在异步运行时候的状态)
        3. 对象之间的松耦合:两个对象之间都互相不了解彼此,但是不影响通信,当有新的订阅者出现的时候,发布的代码无需要改变,同样发布的代码改变,只要之前约定的事件的名称没有改变,也不影响订阅
        4. vue、react之间实现跨组件之间的传值
      • 例子

        • 小红想买一双鞋子,卖光了,联系卖家有货的时候联系他,卖家说要一个星期才有货,可以先收藏店铺,有货的时候通知他,小红收藏了店铺;与此同时,小明,小花等也收藏了店铺,等来货时就会一次通知他们

        • 如何实现

          1. 谁是发布者(比如卖家)
          2. 给发布者添加一个缓存列表,用于存放回调函数来通知订阅者(比如买家收藏了卖家的店铺,就会有一个收藏了该店铺的名单列表)
          3. 发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数
          var shopObj = {} // 定义发布者
          shopObj.list = [] // 缓存列表,存放的是订阅的函数
          // 增加订阅者
          shopObj.listen = function(fn) {
          	shopObj.list.push(fn)
          }
          // 发布消息
          shopObj.trigger = function() {
          	for(var i =0, fn; fn = this.list[i++];) {
          		fn.apply(this, arguments)
          	}
          }
          // 先订阅,再发布
          shopObj.listen(function(color, size){
          	console.log(color)
          	console.log(size)
          })
          shopObj.listen(function(color, size){
          	console.log('再次', color)
          	console.log('再次', size)
          })
          shopObj.trigger('red', 42)
          shopObj.trigger('black', 43)
          // 上面的写法会造成,只要listen,就会循环多次
          
          // 改写
          var shopObj = {} // 定义发布者
          shopObj.list = [] // 缓存列表,存放的是订阅的函数
          // 增加订阅者
          shopObj.listen = function(key, fn) {
          	if(!this.list[key]){
          		this.list[key] = []
          	}
          	this.list[key].push(fn)
          }
          // 发布消息
          shopObj.trigger = function() {
          	// arguments是类数组对象,并没有shift方法
          	var key = Array.prototype.shift.call(arguments)
          	var fns = this.list[key]
          	if (!fns || fns.length === 0) {
          		return
          	}
          	for(var i =0, fn; fn = fns[i++];) {
          		fn.apply(this, arguments)
          		// fn(...arguments)
          	}
          }
          // 上面的无法取消订阅
          
          // 封装事件
          var event = {
          	list: [],
          	listen: function(key, fn) {
          		if(!this.list[key]){
          			this.list[key] = []
          		}
          		this.list[key].push(fn)
          	},
          	trigger: function() {
          		// arguments是类数组对象,并没有shift方法
          		var key = Array.prototype.shift.call(arguments)
          		var fns = this.list[key]
          		if (!fns || fns.length === 0) {
          			return
          		}
          		for(var i =0, fn; fn = fns[i++];) {
          			fn.apply(this, arguments)
          			// fn(...arguments)
          		}
          	}
          }
          // 取消订阅
          event.remove = function(key, fn){
          	var fns = this.list[key]
          	if(!fns) {
          		return false
          	}
          	if (!fn){
          		fn && (fns.length = 0)
          	} else {
          		for(var i = fns.length -1; i >=0; i--){
          			var _fn = fns[i]
          			if(_fn === fn) {
          				fns.splice(i, 1)
          			}
          		}
          	}
          }
          
          // 封装
          var Event = (function(){
          	var list = []
          	var listen = function(key, fn) {
          		if(!this.list[key]){
          			this.list[key] = []
          		}
          		this.list[key].push(fn)
          	}
          	var trigger = function() {
          		// arguments是类数组对象,并没有shift方法
          		var key = Array.prototype.shift.call(arguments)
          		var fns = this.list[key]
          		if (!fns || fns.length === 0) {
          			return
          		}
          		for(var i =0, fn; fn = fns[i++];) {
          			fn.apply(this, arguments)
          			// fn(...arguments)
          		}
          	}
          	var remove = function(key, fn){
          		var fns = this.list[key]
          		if(!fns) {
          			return false
          		}
          		if (!fn){
          			fn && (fns.length = 0)
          		} else {
          			for(var i = fns.length -1; i >=0; i--){
          				var _fn = fns[i]
          				if(_fn === fn) {
          					fns.splice(i, 1)
          				}
          			}
          		}
          	}
          	return {
          		list, listen, trigger, remove
          	}
          })()
          
      • 改进异步操作中的强耦合——例子

        • 一个商城网站,网站有很多模块,模块渲染的公共前提是先用ajax异步请求获取用户的登录信息

          login.succ(function(data){
          	header.setAvatar(data.avatar)
          	nav.setAvatar(data.avatar)
          	messgae.refresh()
          	cart.refresh()
          })
          // 缺点:强耦合
          // header模块里设置头像的方法setAvatar、购物车刷新方法叫refresh,强耦合
          // header模块不能随意改变setAvatar方法名,自身名称也不能被改为header1
          
          • 发布订阅模式实现低耦合

            $.ajax('xxx', function(data){
            	login.trigger('loginSucc', data) // 发布
            })
            // 订阅
            var header = (function(){
            	login.listen('loginSucc', function(data){
            		header.setAvatar(data.avatar)
            	})
            	return {
            		setAvatar: function(dtata) {
            			console.log('设置header头像')
            		}
            	}
            })()
            var nav = (function(){
            	login.listen('loginSucc', function(data){
            		nav.setAvatar(data.avatar)
            	})
            	return {
            		setAvatar: function(dtata) {
            			console.log('设置nav头像')
            		}
            	}
            })()
            
      • vue react 实现跨组件之间的传值

        // 父组件
        <template>
        	<div>
        		<input type="text" v-modal="name" />
        		<button @click="doPub">发布</button>
        		<Son />
        	</div>
        </template>
        <script>
        	import Son from './Son'
        	// 自己封装
        	import pubsub from '@/utils/pubsub.js'
        	export default {
        		name: 'HelloWorld',
        		data: {
        			return {
        				name: ''
        			}
        		},
        		components: {
        			Son
        		},
        		methods: {
        			// 发布
        			doPub(){
        				pubsub.trigger('item', this.name)
        			}
        		}
        	}
        </script>
        
        // 子组件Son
        <template>
        	<div>
        		<h1>{{h1}}</h1>
        	</div>
        </template>
        <script>
        	import pubsub from '@/utils/pubsub.js'
        	export default {
        		name: 'Son',
        		mounted(){
        			// 订阅
        			pubsub.listen('item', data =>{
        				this.h1 = data
        			})
        		}
        	}
        </script>
        
        // 孙子组件
        <template>
        	<div>
        		<h1>{{h1}}</h1>
        	</div>
        </template>
        <script>
        	import pubsub from '@/utils/pubsub.js'
        	export default {
        		name: 'Children',
        		mounted(){
        			// 订阅
        			pubsub.listen('item', data =>{
        				this.h1 = data
        			})
        		}
        	}
        </script>
        
        // 改写Son组件
        <template>
        	<div>
        		<Children />
        	</div>
        </template>
        <script>
        	import Children from './Children'
        	import pubsub from '@/utils/pubsub.js'
        	export default {
        		name: 'Son',
        		// 订阅还是在最上层的HelloWorld组件里
        		components: {
        			Children
        		}
        	}
        </script>
        
        // 封装
        var Event = (function(){
        	var list = []
        	var listen = function(key, fn) {
        		if(!list[key]){
        			list[key] = []
        		}
        		list[key].push(fn)
        	}
        	var trigger = function() {
        		// arguments是类数组对象,并没有shift方法
        		var key = Array.prototype.shift.call(arguments)
        		var fns = list[key]
        		if (!fns || fns.length === 0) {
        			return
        		}
        		for(var i =0, fn; fn = fns[i++];) {
        			fn.apply(this, arguments)
        			// fn(...arguments)
        		}
        	}
        	var remove = function(key, fn){
        		var fns = list[key]
        		if(!fns) {
        			return false
        		}
        		if (!fn){
        			fn && (fns.length = 0)
        		} else {
        			for(var i = fns.length -1; i >=0; i--){
        				var _fn = fns[i]
        				if(_fn === fn) {
        					fns.splice(i, 1)
        				}
        			}
        		}
        	}
        	return {
        		list, listen, trigger, remove
        	}
        })()
        export default Event
        
      • PubSubJs插件 npm install pubsub-js —save-dev