前言:瞬息万变的前端,也具备一些长久的通用型技术,例如性能优化和设计模式
设计模式就犹如游戏里面的连招技巧一样,这些连招经验就是设计模式
1.工厂模式 用一个工厂函数,创建一个实例,封装创建的过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function User (name , age, career, work ) { this .name = name this .age = age this .career = career this .work = work } function Factory (name, age, career ) { let work switch (career) { case 'coder' : work = ['写代码' ,'写系分' , '修Bug' ] break case 'product manager' : work = ['订会议室' , '写PRD' , '催更' ] break case 'boss' : work = ['喝茶' , '看报' , '见客户' ] case 'xxx' : ... return new User (name, age, career, work) } Factory ("yunmu" , 18 , "coder" )
应用场景:
jQuery $('div')
创建一个 jQuery 实例
React 的React.createElement()
和 Vue 的 h()
创建一个 vnode
2.单例模式 提供全局唯一的对象,无论获取多少次:
应用场景
Vuex Redux 的 store ,全局唯一的
全局唯一的 dialog modal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class SingleTon { private static instance : SingleTon | null = null private constructor ( ) {} public static getInstance (): SingleTon { if (this .instance == null ) { this .instance = new SingleTon () } return this .instance } fn1 ( ) {} fn2 ( ) {} } const s2 = SingleTon .getInstance ()s2.fn1 () s2.fn2 () const s3 = SingleTon .getInstance ()s2 === s3
实现一个全局唯一的Modal弹框:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 单例模式弹框</title > </head > <style > #modal { height : 200px ; width : 200px ; line-height : 200px ; position : fixed; left : 50% ; top : 50% ; transform : translate (-50% , -50% ); border : 1px solid black; text-align : center; } </style > <body > <button id ='open' > 打开弹框</button > <button id ='close' > 关闭弹框</button > </body > <script > const Modal = (function ( ) { let modal = null return function ( ) { if (!modal) { modal = document .createElement ('div' ) modal.innerHTML = '我是一个全局唯一的Modal' modal.id = 'modal' modal.style .display = 'none' document .body .appendChild (modal) } return modal } })() document .getElementById ('open' ).addEventListener ('click' , function ( ) { const modal = new Modal () modal.style .display = 'block' }) document .getElementById ('close' ).addEventListener ('click' , function ( ) { const modal = new Modal () if (modal) { modal.style .display = 'none' } }) </script > </html >
3.代理模式
使用者不能直接访问真实数据,而是通过一个代理层来访问
ES Proxy 本身就是代理模式,Vue3 基于它来实现响应式
应用场景实践:
保护代理:下面模拟现实中婚介所,它们就有点类似于代理层,顾客只能通过婚介所间接获取对方身份
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 const present = { type : '巧克力' , value : 60 , } const girl = { name : '小美' , aboutMe : '...' (大家自行脑补吧) age : 24 , career : 'teacher' , fakeAvatar : 'xxxx' (新垣结衣的图片地址) avatar : 'xxxx' (自己的照片地址), phone : 123456 , presents : [], bottomValue : 50 , lastPresent : present, } const JuejinLovers = new Proxy (girl, { get : function (girl, key ) { if (baseInfo.indexOf (key)!==-1 && !user.isValidated ) { alert ('您还没有完成验证哦' ) return } if (user.isValidated && privateInfo.indexOf (key)!==-1 && !user.isVIP ) { alert ('只有VIP才可以查看该信息哦' ) return } } set : function (girl, key, val ) { if (key === 'lastPresent' ) { if (val.value < girl.bottomValue ) { alert ('sorry,您的礼物被拒收了' ) return } girl.lastPresent = val girl.presents = [...girl.presents , val] } } })
1 2 3 4 5 6 7 8 9 10 11 12 const father = document .getElementById ('father' )father.addEventListener ('click' , function (e ) { if (e.target .tagName === 'A' ) { e.preventDefault () alert (`我是${e.target.innerText} ` ) } } )
虚拟代理:下图 virtualImage 代替真实 DOM 像图片发送了请求,却未曾渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class PreLoadImage { constructor (imgNode ) { this .imgNode = imgNode } setSrc (imgUrl ) { this .imgNode .src = imgUrl } } class ProxyImage { static LOADING_URL = 'xxxxxx' constructor (targetImage ) { this .targetImage = targetImage } setSrc (targetUrl ) { this .targetImage .setSrc (ProxyImage .LOADING_URL ) const virtualImage = new Image () virtualImage.onload = () => { this .targetImage .setSrc (targetUrl) } virtualImage.src = targetUrl } }
缓存代理:空间换时间、下面通过代理计算的同时进行计算结果的缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 ) } })()
4.迭代器模式
统一遍历的模式
之前可以使用 jQuery 的 each 方法遍历不同的集合对象
ES6 之后 使用 迭代器(Iterator),任何结构只要具备 Symbol.iterator 属性,就可以被for…of循环和迭代器的next方法遍历
for…of…的背后正是对next方法的反复调用
自定义Iterator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function iteratorGenerator (list ) { var idx = 0 var len = list.length return { next : function ( ) { var done = idx >= len var value = !done ? list[idx++] : undefined return { done : done, value : value } } } } var iterator = iteratorGenerator (['1号选手' , '2号选手' , '3号选手' ])iterator.next () iterator.next () iterator.next ()
5.适配器模式
对外统一(入参、调用方式、出参)
axios靠一套 API 不仅能在浏览器端调用,而且在 Node 环境同样适用,靠的就是灵活得适配器使用
类似耳机转换头,把一个接口转换为另一个接口
假如我写了这样一个请求库,基于 fetch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 export default class HttpUtils { static get (url ) { return new Promise ((resolve, reject ) => { fetch (url) .then (response => response.json ()) .then (result => { resolve (result) }) .catch (error => { reject (error) }) }) } static post (url, data ) { return new Promise ((resolve, reject ) => { fetch (url, { method : 'POST' , headers : { Accept : 'application/json' , 'Content-Type' : 'application/x-www-form-urlencoded' }, body : this .changeData (data) }) .then (response => response.json ()) .then (result => { resolve (result) }) .catch (error => { reject (error) }) }) } 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 } } const URL = "xxxxx" const params = { ... } const postResponse = await HttpUtils .post (URL ,params) || {} const getResponse = await HttpUtils .get (URL ) || {}
这时候如果老板叫我迁移老项目的请求到我这个请求,这下芭比Q了,因为不仅接口名不同,入参方式也不一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 function Ajax (type, url, data, success, failed ){ 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 ); } xhr.send (); } else if (type == 'POST' ){ xhr.open ('POST' , url, true ); xhr.setRequestHeader ("Content-type" , "application/x-www-form-urlencoded" ); xhr.send (data); } xhr.onreadystatechange = function ( ){ if (xhr.readyState == 4 ){ if (xhr.status == 200 ){ success (xhr.responseText ); } else { if (failed){ failed (xhr.status ); } } } } } Ajax ('get' , url地址, post入参, function (data ){ }, function (error ){ })
这时候我们就可以编写一个适配器函数AjaxAdapter ,并用适配器去承接旧接口的参数,无缝衔接!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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) || {} } result.statusCode === 1 && success ? success (result) : failed (result.statusCode ) } catch (error) { if (failed){ failed (error.statusCode ); } } } async function Ajax (type, url, data, success, failed ) { await AjaxAdapter (type, url, data, success, failed) }
6.装饰器模式
ES 和 TS 的 Decorator 语法就是装饰器模式。可以为 class 和 method 增加新的功能
以下代码可以在 ts playground 中运行
1 2 3 4 5 6 7 8 9 10 11 function logDec (target ) { target.flag = true } @logDec class Log { } console .log (Log .flag )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function log (target, name, descriptor ) { const oldValue = descriptor.value descriptor.value = function (param ) { console .log (`Calling ${name} with` , param) return oldValue.call (this , param) }; return descriptor } class Seller { @log public buy (num ) { console .log ('do buy' , num) } } const s = new Seller ()s.buy (100 )
ngular nest.js 都已广泛使用装饰器。这种编程模式叫做AOP 面向切面编程 :关注业务逻辑,抽离工具功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { Controller , Get , Post } from '@nestjs/common' ;@Controller ('cats' )export class CatsController { @Post () create (): string { return 'This action adds a new cat' ; } @Get () findAll (): string { return 'This action returns all cats' ; } }
7.原型模式
原型模式不仅是一种设计模式,它还是一种编程范式
在 JavaScript 里,Object.create
方法就是原型模式的天然实现,得到相当于继承的对象
在 JavaScript 中,我们使用原型模式,是为了得到与构造函数(类)相对应的类型的实例、实现数据/方法的共享
什么是原型和原型链,什么是深浅拷贝,如何实现,我以前的文章均有涉及,此处就不啰嗦了
8.策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换
比如此时要做一个差异化询价,即同一个商品,我通过在后台给它设置不同的价格类型,可以让它展示不同的价格
当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折
当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折
当价格类型为“返场价”时,满 200 - 50,不叠加
当价格类型为“尝鲜价”时,直接打 5 折
到这里就很容易去写出 if else 的逻辑去操作价格,这样会随着时间越难越维护,如果使用策略模式的话
抽取四种询价逻辑:
prePrice - 处理预热价
onSalePrice - 处理大促价
backPrice - 处理返场价
freshPrice - 处理尝鲜价
askPrice - 分发询价逻辑
此时就只需要询价逻辑的分发 ——> 询价逻辑的执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 function prePrice (originPrice ) { if (originPrice >= 100 ) { return originPrice - 20 } return originPrice * 0.9 } function onSalePrice (originPrice ) { if (originPrice >= 100 ) { return originPrice - 30 } return originPrice * 0.8 } function backPrice (originPrice ) { if (originPrice >= 200 ) { return originPrice - 50 } return originPrice } function freshPrice (originPrice ) { return originPrice * 0.5 } function askPrice (tag, originPrice ) { if (tag === 'pre' ) { return prePrice (originPrice) } if (tag === 'onSale' ) { return onSalePrice (originPrice) } if (tag === 'back' ) { return backPrice (originPrice) } if (tag === 'fresh' ) { return freshPrice (originPrice) } }
但是我们可以使用更为优雅的对象映射 来提高可维护性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 ; }, };
使用其中某个询价算法的时候:通过标签名去定位就好了:
1 2 3 4 function askPrice (tag, originPrice ) { return priceProcessor[tag](originPrice) }
如果此时新增一个新人价,只需要给 priceProcessor 新增一个映射关系:
1 2 3 4 5 6 priceProcessor.newUser = function (originPrice ) { if (originPrice >= 100 ) { return originPrice - 50 ; } return originPrice; }
9.状态模式
状态模式和策略模式长得像并且解决的问题也没有本质去呗
此时要进行咖啡机的代码逻辑编写
美式咖啡态(american):只吐黑咖啡
普通拿铁态(latte):黑咖啡加点奶
香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力
这时候又很可能写出 if else 逻辑,但是当我们使用策略模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const stateToProcessor = { american ( ) { console .log ('我只吐黑咖啡' ); }, latte ( ) { this .american (); console .log ('加点奶' ); }, vanillaLatte ( ) { this .latte (); console .log ('再加香草糖浆' ); }, mocha ( ) { this .latte (); console .log ('再加巧克力' ); } } class CoffeeMaker { constructor ( ) { this .state = 'init' ; } changeState (state ) { this .state = state; if (!stateToProcessor[state]) { return ; } stateToProcessor[state](); } } const mk = new CoffeeMaker ();mk.changeState ('latte' );
上面也用到了对象映射的方法,但是不同于策略模式的简答询价,changeState函数有时需要拿到咖啡机这个主体的信息,咖啡机和它的状态处理函数建立关联
我们这时把状态-行为映射对象作为主体类对应实例的一个属性添加进去就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class CoffeeMaker { constructor ( ) { this .state = 'init' ; this .leftMilk = '500ml' ; } stateToProcessor = { that : this , american ( ) { console .log ('咖啡机现在的牛奶存储量是:' , this .that .leftMilk ) console .log ('我只吐黑咖啡' ); }, latte ( ) { this .american () console .log ('加点奶' ); }, vanillaLatte ( ) { this .latte (); console .log ('再加香草糖浆' ); }, mocha ( ) { this .latte (); console .log ('再加巧克力' ); } } changeState (state ) { this .state = state; if (!this .stateToProcessor [state]) { return ; } this .stateToProcessor [state](); } } const mk = new CoffeeMaker ();mk.changeState ('latte' );
10.观察者模式和发布订阅模式 观察者模式
即常说的绑定事件。一个主题,一个观察者,主题变化之后触发观察者执行
1 2 btn.addEventListener ('click' , () => { ... })
发布订阅模式 即常说的自定义事件,一个 event
对象,可以绑定事件,可以触发事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 event.on ("event-key" , () => { }); event.on ("event-key" , () => { }); event.emit ("event-key" ); event.off ("event-key" , fn1); event.off ("event-key" , fn2);
两者区别
观察者模式
Subject 和 Observer 直接绑定,中间无媒介
如 addEventListener
绑定事件
发布订阅模式
Publisher 和 Observer 相互不认识,中间有媒介
如 eventBus
自定义事件
11.MVC 和 MVVM MVC 原理
View 传送指令到 Controller
Controller 完成业务逻辑后,要求 Model 改变状态
Model 将新的数据发送到 View,用户得到反馈
MVVM 直接对标 Vue 即可
View 即 Vue template
Model 即 Vue data
VM 即 Vue 其他核心功能,负责 View 和 Model 通讯