theme: channing-cyan 携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第N天,点击查看活动详情 。 相信大家在日常学习和工作中都多多少少听说/了解/使用过 设计模式 ,我们都知道,使用恰当的设计模式可以优化我们的代码,那你是否知道对于前端开发哪些 设计模式 是日常工作经常用到或者必须掌握的呢?本文我将带大家一起学习下前端常见的设计模式以及它们的 使用场景 !!!
本文主讲:
适合人群:
前端人员
设计模式小白/想知道如何在项目中使用设计模式
策略模式 策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。从定义不难看出,策略模式是用来解决那些一个功能有多种方案、根据不同条件输出不同结果且条件很多的场景,而这些场景在我们工作中也经常遇到,接下来我将用几个例子来展示策略模式在哪里用以及如何用。
1.绩效考核
假如我们有这么一个需求,需要根据员工的绩效考核给员工发放年终奖(分为A/B/C/D四个等级 分别对应基础奖金的1/2/3/4倍),我们很容易就写出这样的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 const computeBonus (level, basicBonus) = () => { if (level === 'A' ) { return basicBonus * 1 ; } else if (level === 'B' ) { return basicBonus * 2 ; } else if (level === 'C' ) { return basicBonus * 3 ; } else if (level === 'D' ) { return basicBonus * 4 ; } } computeBonus ('A' , 1000 );
我们发现,以上的代码可以轻松实现我们的需求,但是这些代码存在什么问题呢?
computedBonus
方法十分臃肿,包含太多if-else
拓展性差,后续如果想要更改评级或者规则都需要进入该函数内部调整。
复用性差。
那策略模式是怎么解决这些问题的呢?我们都知道,设计模式的核心之一就是将可变的和不可变的部分抽离分装 ,那我们根据这个原则来修改我们的代码,其中可变的就是如何使用这些算法(多少个评级),不变的是算法的内容(评级对应的奖金),下面就是改变后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const strategies = { 'A' : function (basicBonus ) { return basicBonus * 1 ; }, 'B' : function (basicBonus ) { return basicBonus * 2 ; }, 'C' : function (basicBonus ) { return basicBonus * 3 ; }, 'D' : function (basicBonus ) { return basicBonus * 4 ; }, } const computeBonus = (level, basicBonus) { return strategies[level](basicBonus); } computeBouns ('A' , 1000 );
从上面可以看出,我们将每种情况都单独弄成一个策略,然后根据传入评级和奖金计算年终奖,这样我们的computeBonus
方法代码量大大减少,也不用冗杂的if-else
分支,同时,如果我们想要改变规则,只需要在strategies
中添加对应的策略,增加了代码的健壮性
2.表单验证
我们日常的工作中,不可避免地需要做表单相关的业务,毕竟这是前端最初始的职能之一。而表单绕不开表单验证,那接下来我将带大家看看策略模式在表单中如何使用。
需求 : 假设我们有一个登录业务,提交表单前需要做验证,验证规则如下:1.用户名称不能为空,2.密码不能少于6位,3.手机格式要正确。
我们很容易写出以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const verifyForm = (formData ) => { if (formData.userName == '' ) { console .log ('用户名不能为空' ); return false }; if (formData.password .length < 6 ) { console .log ('密码长度不能小于6位' ); return false ; } if (( !/(^1[3|5|8][0-9]{9}$)/ .test (formData.phone )) { console .log ('手机格式错误' ); return false } }
显然,这样也可以完成表单校验的功能,但是这样写同样存在着上面说的问题,接下来,我们看下用策略模式如何改写
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 const strategies = { isEmpty : function (value, error ) { if (value === '' { return error; }) }, minLength : function (value, len, error ) { if (value.length < len { return error; }) }, isPhone : function (value, error ) { if ( !/(^1[3|5|8][0-9]{9}$)/ .test ( value ) ){ return errorMsg; } }; } class Validator { controustor (cache ) { this .cache = cache || []; }; add (dom, rule, error ) { const arr = rule.splt (':' ); this .cache .push (function ( ){ const strategy = arr.shift (); arr.unshift ( dom.value ); arr.push ( errorMsg ); return strategies[ strategy ].apply ( dom, ary ); }); }; start ( ) { for ( let i = 0 , validatorFunc; validatorFunc = this .cache [ i++ ]; ){ var msg = validatorFunc (); if ( msg ){ return msg; } } } } const validataFunc = function ( ){ let validator = new Validator (); validator.add ( registerForm.userName , 'isNonEmpty' , '用户名不能为空' ); validator.add ( registerForm.password , 'minLength:6' , '密码长度不能少于 6 位' ); validator.add ( registerForm.phoneNumber , 'isMobile' , '手机号码格式不正确' ); var errorMsg = validator.start (); return errorMsg; } var registerForm = document .getElementById ( 'registerForm' ); registerForm.onsubmit = function ( ){ var errorMsg = validataFunc (); if ( errorMsg ){ alert ( errorMsg ); return false ; } };
这样,我们就用策略模式将需求改好了,之后如果我们的校验规则改变了,修改起来也是很方便的,比如:
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' ); // 改成:
validator.add( registerForm.userName, 'minLength:10', '用户名长度不能小于 10 位' );
而且,我们也可以给文本框添加多个校验规则,只需要修改下策略对象以及策略方法即可!大大地增强了代码地健壮性。
策略模式的优缺点: 优点:
避免多重条件选择语句(if-else
)
具有可拓展性,可独立抽离封装,避免重复复制粘贴
缺点:
增加很多策略类或者策略对象,但是这其实不算什么大缺点
比起直接编写业务代码需要思考策略对象以及其他细节
代理模式 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。代理模式应该是我们日常用到比较多的设计模式了(我们日常工作中不知不觉就会用到代理模式,可能你没发现而已)。
代理模式分为保护代理(用于控制不同权限的对象对目标对象的访问)和虚拟代理(把开销很大的对象或者操作延迟到真正需要的时候再去创建 类比引入时动态引入)两种,但是前端基本不用到保护代理,或者说很难实现保护代理,所以大部分情况下我们用的都是虚拟代理,接下来我主要也是讲虚拟代理!
举个例子,加入A想要给C送情书,但是A没有直接把情书交给C,而是让B代为传送情书,那么B就是代理,他的职责就是替A做事,这个就是最简单的代理模式,接下来我们还是老样子,边写需求边讲解
1.图片懒加载:
相信大家对于图片懒加载都不陌生吧,他可以在我们加载出目标图片前预加载占位图片,避免空白区域影响体验,那我们很容易就能写出下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const lazyImage = (function ( ) { let imgNode = document .createElement ('img' ); document .body .appendChild (imgNode); let image = new Image ; image.onload = function ( ) { imgNode.src = image.src ; }; return { setSrc : function (src ) { imgNode.src = '....' image.src = src } } })() lazyImage.setSrc ('https://olddog.jpg' );
我们看上面的代码,也可以完成预加载的功能,但是这样的代码存在着什么样的问题呢
违反了单一职责原则,而且耦合度太高,如果后期我们不需要懒加载了,或者需要根据判断条件判断是否懒加载,就不得不去动lazyImage的代码
接下来,我们就用代理模式来改写一下这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const lazyImage = (function ( ) { let imageNode = document .createElement ('img' ); document .body .appendChild (imageNode); return { setSrc : function (src ) { imageNode.src = src; } } })() const proxyImage = (function ( ) { let image = new Image ; image.onload = function ( ) { myImage.setSrc (this .src ); } return { setSrc : function (src ) { myImage.setSrc ('....' ) img.src = src } } })() proxyImage.setSrc ('https://olddog.jpg' );
我们观察用代理模式写的代码,发现我们将预加载的逻辑转移到了代理函数中,这样有啥好处呢
如果后期不需要预加载了,只需要取消代理,即将proxyImage.setSrc(...)
改成lazyImage.setSrc(...)
代理函数的使用方式和原函数一模一样,使用者不需要知道代理的实现细节也能使用
不知道大家有没有发现,代理函数和原函数有一部分相似的逻辑和操作,只是代理函数的功能更多,这其实也是代理模式的特征之一,代理函数在保证实现原函数的基本功能的前提下实现更多功能,这样即使使用者不清楚逻辑也能直接使用,而且后期改动成本很低,只需要改回原函数的使用即可!!
2.缓存代理
设想一下,如果现在要你写一个简单的求积函数,你会怎么写
1 2 3 4 5 6 7 8 const mult = function ( ) { let result = 1 ; for (let i = 0 , len = arguments .length ; i < len; i++) { result *= arguments [i]; } return result } mult (1 , 2 , 3 );
我们来看一下上面的代码有啥缺点,上面的代码虽然实现了求积,但是如果我们mult(1,2,3)
之后再去mult(1,2,3)
,那么系统还是会再计算一遍,这样无疑是性能浪费,那么我们就能用代理模式来改写:
1 2 3 4 5 6 7 8 9 10 11 12 const proxyMult = (function ( ) { let cache = {}; return function ( ) { const args = Array .prototype .join .call ( arguments , ',' ); if (args in cache) { return cache[args] } return cache[args] = mult.apply (this .arguments ) } })(); proxyMult (1 ,2 ,3 );proxyMult (1 , 2 , 3 );
可以看到,我们用代理模式改写后避免了重复运算的浪费,这只是一种情景,还有其他相似情景,比如我们分页请求数据,可以使用相似的思路,避免对同页的数据重复请求,这在工作中非常有用!!
总结:
我们日常工作中还有很多地方用到代理,比如代理合并请求(间断性合并而不是全部合并,减少服务器压力)、惰性加载或创建申请资源等等,而什么时候使用代理其实不需要提前花很多精力去思考,当我们写着写着发现可以抽离使用代理模式的时候再去使用也不迟。由于文章篇幅有限,本文就先讲解策略模式和代理模式,后续将继续更新其他实用的设计模式,喜欢的小伙伴可以点个赞和关注一下,有啥问题可以评论区一起学习交流!
相关文章:
前端设计模式之发布-订阅模式