代码质量 与 设计模式应用

一、代码指标

1、健壮性

什么意思呢? 百度:在异常和危险情况下系统生存的能力。
用人的健壮性做个比方:

如果一个人很健壮,那一般的小病小灾打不倒他,如感冒、咳嗽
对比过来,一个程序很健壮,那在出现预期之外的错误(eg:文件不存在),减少找bug的难度和影响程度。

健壮性好的代码有哪些特点

减少找bug的难度
很多同学可能有这么一个问题:发现自己每天写代码 2小时,改bug 6小时, 一天8个小时就这么过去了
降低对程序的影响程度
如果程序出现了问题,不至于这一个问题让我们整个系统跑不起来了

保证健壮性的手段

健壮性不同其他性能,没有多抽象的概念,也没有多深的思维,主要就是个人的习惯

1、参数层面

  • 基础类型判断

    先来看下面一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function add(a, b){
    return a + b;
    }
    // 正常入参 没什么问题
    add(1,2);

    // 少传一个参数时
    add(1) // ==> NaN 这里是不会报错的哦!

    // 这时如果这个返回值是通过 axios.get()传到后台会是什么情况?
    axios.get('url',{ num: add(1) }) // ==> 400 bad request
    // 试想:如果你的代码量比较大时,要定位到这个问题是不是要花很长时间,还要debug之类的
    这就是典型的参数类型引起的问题
    [如何解决?]
    1
    2
    3
    4
    5
    6
    7
    8
    function add(a, b){
    // 1、基础参数类型判断
    if(typeof a === 'number' && typeof b === 'number'){
    return a + b;
    } else {
    throw new Error('a or b is not a number')
    }
    }

  • 参数是配置对象 选项合并
    1
    2
    3
    4
    // 以Vue为例:
    new Vue({});
    // 有哪些必传参数?
    // 如何保证这些必传参数都传了?
    [看Vue是怎么做的?]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 必传参数:  el template
    // 如何保证必传值不为空: 提供一个默认配置,把new时的入参与默认配置做合并
    function Vue(config){
    let _default = {
    el: document,
    template:'<div></div>'
    }
    for(let key in config){
    _default[key] = config[key] || _default[key];
    }
    }

  • 参数是某个类的实例

    这种情况,一般是我们在造轮子的时候会用到

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Class1 是一个类
    function Class1() {

    }

    // 这里有个方法fn 参数要求是Class1的实例
    function fn(obj){

    }
    [看这种情况怎么保证健壮性]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Class1 是一个类
    function Class1(){

    }
    // 这里有个方法fn 参数要求是Class1的实例
    function fn(obj){
    if(obj instanceof Class1){
    // dosomthing
    } else {
    throw new Error('obj 必须是 Class1的实例')
    }
    }

这种情况在实际开发中可能用到的比较少,
但在自己造轮子的时候,这种情况就会非常多

  • js中特有的一个问题

    1
    2
    // function 除了可能是一个方法,还可能代表一个什么东东?
    function a(){}

    [如何保证?]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function vue(){
    if(this instanceof vue){ // 通过instanceof vue 判断this 是不是vue实例
    // 类实例
    } else {
    // 当作一个方法调用
    // return new vue(); // 这样即使当一个方法调用 ,拿到的还是一个vue的实例

    // 像一些框架之类的都会有这种措施 vue是这样
    throw new Error('vue is a constructor and should be called with the `new` keyword ')
    }
    }

    vue(); // 直接调用时,this ---> window
    new vue(); // new 操作符调用时, this 指向vue实例 通过instanceof vue 可以判断

易错代码

代码错了就是错了,什么叫易错代码?

  • 不由自己控制的出错代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 例如: 前端请求某个接口,接口返回的数据结构
    // 预期结构是下面这样的:
    let res = {
    list:[
    {
    attrA: 1
    }
    ]
    }

    // 是这样取值的
    let attrA = res.list[0].attrA;

    // 如果 list 为空或[] 时,我们取值就会报错 Cannot read property 'attrA' of undefined

    [怎么解决呢?]
    一般有两种方法:

    • 模拟一个这样的数据结构
      1
      2
      let item = (res.list || [])[0];
      let attrA = (item || {}).attrA;
    • 用&&操作符 先判断为空的情况
      1
      2
      let item = res.list && res.list[0]  // 使用与操作符 判断为空的情况
      let attrA = item && item.attrA;

    前端易错性代码出现可能会少些,后端就非常多

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 在下载某个文件时,
    // 1、文件存在,正常下载
    // 2、文件不存在,就会出错了
    // 如果不处理出错,后端服务可能就停了,那这样影响就非常严重了,

    // 所以后端经常会看到这样的代码段
    try{
    // 易错代码
    } catch (e){
    // 记录日志
    }

    // 先把出错的信息记录下来,保证程序不会挂掉,
    // 再通过日志分析出错原因

变量权限 这个就有点抽象了

变量应该不应该被篡改,能不能够被篡改
变量应不应该被读取,能不能够被读取,在哪里能被读取
这就是变量的权限

变量权限跟健壮性有什么关系呢?

1
2
3
4
// 还以Vue为例:
// 在使用vue-router的时候,有没有留意这样一个问题:
this.$router = {}; // 给$router赋值,但 this.$router的值不会变


[vue-router是怎么做的呢?]
1
2
3
4
5
6
7
8
9
10
11
// vue-router install 方法
function install(Vue){
let _router = {}; // 局部变量
// 屏蔽了set方法,使你给$router赋值时无效
Object.defineProperty(Vue.prototype, '$router', {
get: function get () {
// return this._routerRoot._router // 源码是这样的
return _router;
}
});
}

2、可读性

看到变量或方法就知道这个变量,这段代码是做什么用的

语义化

定义有意义的名字: 描述某个变量或方法在业务中的作用

1
2
3
4
5
6
7
// 这些都是没有意义, 大量的这种命名,在后期阅读代码时,简直是个灾难
function a(){

}
function b(){

}

如何命名呢?
可以按这个思路来: 在业务中的作用 –> 用中文描述它的作用 –> 翻译成英文
尽量做到见名知义

命名规范

这个不多说了,

比如:js可以这样命名:
  1、类命名:首字母大写
  2、普通方法变量,小驼峰命名
  3、常量: 全大写,
  4、局部变量:_开头

结构清晰

  • 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
    if(true){
    if(true){

    } else if(true){

    } else {

    }
    } else if(true){
    if(true){

    } else if(true){

    } else {

    }
    } else {
    if(true){

    } else if(true){

    } else {

    }
    }
  • 回调函数问题

    需求:一个操作需要调三个接口A,B,C,B依赖A的返回结果,C依赖B的返回结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 用jquery 实现就是这种样式
    $.ajax({
    url:'urlA',
    params:{},
    success: function(resA){
    $.ajax({
    url:'urlB',
    params: resA,
    success: function(resB){
    $.ajax({
    url:'urlC',
    params: resB,
    success: function(resC){
    console.log(resC)
    }
    })
    }
    })
    }
    })

    ES6 之后引用了 promise async 主要就是为了解决回调问题 避免回调地狱

    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
    // promise then 方式解决
    function request(url, params){
    return new Promise((resolve,reject) => {
    $.ajax({
    url:url,
    params: params,
    success:function(res){
    resolve(res);
    },
    error: function(err){
    reject(err)
    }
    })
    })
    }
    // 调用
    request('urlA', {})
    .then(res => request('urlB', res))
    .then(res => request('urlC', res))
    .catch(err => console.log(err));

    // async
    async function getData(url, params){
    return axios.post(url, params);
    }
    //调用
    let resA = await getData('urlA', {});
    let resB = await getData('urlB', resA);
    let resC = await getData('urlC', resB);

    3、可复用性

DRY原则 Don’t Repeat Yourself

写过一遍的操作,就不重复第二遍了
尽量不去写重复代码, 这可能带来一些维护上的负作用,这个就要把握一个度了
要看某段代码有没有提取的必要,比如重复了多少次,使用量大不大

逻辑复用,提取代码

针对局部某个操作,某个功能而言
重复的部分提取成一个公用的方法

创建公用模板

针对全局性的,创建公用模块:
layout | header | footer | common.css | util.js

4、可扩展性

程序上的一个终极难题
对开发人员的架构思维,模块思维要求是非常高的

产品经理不可能不修改他的需求,就像程序员不可能不写bug一样
如果在写代码的时候预先考虑到后期需求可能的变化,那在需求变化时,你就会非常舒服了

  • 模块分明

    积木式编程,随时可以插入、移除某个模块

  • 耦合度低

    划分低耦合的模块,并高效设计模块间的沟通 (架构层面讲)
    比如你要开个饭店,你需要怎么设置你的组织架构?

    厨师模块,服务员模块,收银员模块(老板)

  • 合适的扩展技巧

    应用设计模式

二、设计模式概论

1、创建型设计模式

帮助我们优雅的创建对象

  • 工厂模式

    大量创建对象

    Jquery时代,我们需要大量频繁的操作dom

    1
    2
    // $就是一个工厂,他批量生产jquery对象, 根据你传入的选择器,生成一批jquery对象
    $('.className')

    [如何实现一个工厂模式呢?]
    球类工厂

    需求:我们要生产不同类型的球:basketball footerball tennis(网球)…

    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
    // 球类工厂
    function ballFactory(type){

    switch(type){
    case 'football':
    return new football();
    break;
    case 'basketball':
    return new basketball();
    break;
    case 'tennis':
    return new tennis();
    break;
    default:
    break;
    }

    // 可以在局部
    function football(){}
    function basketball(){}
    function tennis(){}
    }

    // 可以在原型链上
    ball.prototype.football = function(){}
    ball.prototype.basketball = function(){}
    ball.prototype.tennis = function(){}

    [再看看jquery是如何实现的]
    Jquery如何实现工厂模式的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 静态函数 jquery的实现
    function jquery(params){
    return new jquery.fn.init(params);
    }
    jquery.fn = {};
    jquery.fn.init = function (params) {};
    window.$ = jquery;

    $('.className');// 实际是通过new jquery静态属性fn上的init方法

    jquery是通过挂载一个静态属性实现的


  • 建造者模式

    精细化组合一个对象:类似建房子
    盖房子明, 先把砖,门,窗这些材料都准备好,再把这些材料搭在一起,就建成了.

    关键思想 就是先把相关的模块提前独立开发好,再把各个模块拼装集成到一起

    明显的标识就是 传入了一大堆的配置信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //建造者模式: 一般实现方案

    // 关键思想 先把相关的模块放一边开发好,再把各模块集成到一起
    function F1(){
    // 直接绑定
    this.model1 = new Model1({});
    this.model2 = new Model2({});
    this.model3 = new Model3({});
    }

    function Model1(params){}
    function Model2(params){}
    function Model3(params){}

    vue 也是用建造者模式实现的, 但双是跟一般的方案又不太一样
    混入方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 明显的标识就是 传入了一大堆的配置信息
    // 方法2 Vue 是怎么做的
    // 先把各个模块独立开发好,之后再混合进实例里
    function Vue (options) {
    // 健壮性校验
    if (!(this instanceof Vue)
    ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
    }

    // 下面就是各个模块的混入Vue原型上
    initMixin(Vue);
    stateMixin(Vue);
    eventsMixin(Vue);
    lifecycleMixin(Vue);
    renderMixin(Vue);
  • 单例模式

    全局只有一个,对象只能被创建一次

    如何实现 单例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 常规类:靠挂在这个类上面的一个静态属性
    function Class1(params){
    if(Class1.instance){ // 查检有没有实现,有则直接返回该实例,无则创建实例,并增加标识
    return Class1.instance;
    } else {
    this.name ='xxx'
    this.attr = 'bbbb';

    Class1.instance = this;
    }
    }
    // 两个new 是同一个实例
    let class11 = new Class1();
    let class12 = new Class1();
    console.log(class11 == class12)

    再看vuex vue-router
    vuex: 全局只有一个,如果有多个,那页面的状态从哪个取也是问题
    vue-router:全局中有一个,如果有多个的话,页面跳转的时候,选用哪个就是一个问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 以vue-router为例子
    // vue插件时如何保证单例的 以vue-router为例
    // 通过一个参数,变量,静态属性去标识这个类是否被new 过,已经new过的,不再去new
    Vue.use(vueRouter);
    // Vue.use 实际上是调用了插件的install方法

    // vue-router
    {
    var _vue;
    function install(vue){
    if(_vue === vue && install.installed)
    return;
    _vue = vue;
    install.installed = true;// 静态属性
    }
    }

问题:

需求1:实现一个消息提示弹框插件,[用什么模式?]
弹框提示插件
分析: 因为一个页面的消息提示可能会很多,添加成功,添加失败,网络出错
会需要频繁创建对象,所以考虑用工厂模式实现

1
2
3
4
5
6
7
8
9
10
11
12
// 工厂模式实现 调用如下
pop('message')
// OR
pop.confirm('message')

// 建造者模式实现,调用如下
pop('message')
// OR
let popObj = new pop('message')
popObj.show();

// 哪个用起来更方便, 虽然看起来只少了一个new

需求2: 实现一个编辑器插件:有前进,后退,改变字体,大小,颜色功能,[用什么模式?]
编辑器插件
需求: 有前进,后退,改变字体,大小,颜色功能

分析:
1、一般一个页面只有一个,不需要频繁创建
2、编辑器可能还要涉及到复杂的配置,需要精细化创建

建造者模式

1
2
3
4
// html 初始化--> 事件绑定 --> 前进后退模块 -->数据记录模块 --> 字体控制模块 --> 数据渲染模块
// 前进后退: 一般都是数据驱动思维,
[{color:'red', content:'hello'}, {color:'green', content:'hello'}]

2、结构型设计模式

帮助我们更优雅的设计代码结构: 策略,享元

还以一个具体的需求为例:

需求:写一个表单验证工具,给我要验证的input 值 value 变化时,应用对应的规则,自动验证
分析:
1、首先是包含的模块:dom初始化模块 –> 事件绑定模块 –> 验证模块 –> 消息提示模块
2、一个页面上可能有多个需要验证的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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 1、防JQuery实现

// 初始化
function t(dom, msgDom){
return new t.init(dom, msgDom);
}
t.init = function(dom, msgDom){
this.dom = dom;
this.msgDom = msgDom;
this.validateArr = [];
}
// 事件绑定
t.init.prototype.initBind = function(params){
let self = this;
this.dom.onblur = function(){
self.run(this.value)
}
}
// 验证模块
// 变化可能性最大,需要细化
// 考虑验证模块在以后的需求中可能需要扩展,
// 所以可以提前预留好自定义扩展方法,预置一些基础的验证规则,减少代码重复
// 以队列的形式存放验证规则,方便扩展
dt.init.prototype.add = function(fn){
if(typeof fn === 'function'){
this.validateArr.push(fn);
} else if(typeof fn === 'string'){
// 预置的验证规则 下面这样写,可读性差,代码不清楚
if(fn === 'isPhone'){
this.validateArr.push(()=>{/*手机号验证*/})
} else if(fn === 'isNumber'){
this.validateArr.push(()=>{/**是否是数字*/})
} else if(fn === 'isEmail'){

}
}
}
t.init.prototype.run = function(value){
while(this.validateArr.length > 0){
// _result 是约定好的验证结果的数据结构
// {success: true|false, msg:''}
let _result = this.validateArr.shift().run(value);
if(!_result.success){
this.sendMsg(_result.msg);
break; // 一个验证失败就停止验证,减少不必要的循环
}
}
}
// 消息提示模块
t.init.prototype.sendMsg = function(msg){
this.msgDom.innerHtml = msg;
}


// 后期的使用 职责链模式
t('input', 'errorMsg')
.add('isPhone')
.add(() => {/**自定义验证1 */})
.add(() => {/**自定义验证2 */})

[优化后的验证工具]
表单验证工具

需求:写一个表单验证工具,给我要验证的input 值 value 变化时,应用对应的规则,自动验证
分析:
1、首先是包含的模块:dom初始化模块 –> 事件绑定模块 –> 验证模块 –> 消息提示模块
2、一个页面上可能有多个需要验证的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
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
// 1、防JQuery
function t(dom, msgDom){
return new t.init(dom, msgDom);
}
t.init = function(dom, msgDom){
this.dom = dom;
this.msgDom = msgDom;
this.validateArr = [];
}
// 2、思考以后的扩展性,想想模块是否需要细化, 模块越细,扩展越方便
// 验证模块变化最大,细化
// 开启验证模块,验证队列模块
t.init.prototype.initBind = function(params){
let self = this;
this.dom.onblur = function(){
self.run(this.value)
}
}

t.init.prototype.add = function(fn){
if(typeof fn === 'function'){
this.validateArr.push(fn);
} else if(typeof fn === 'string'){
// 预置的验证规则 下面这样写,可读性差,代码不清楚
// 引入设计模式解决
// if(fn === 'isPhone'){
// this.validateArr.push(()=>{/*手机号验证*/})
// } else if(fn === 'isNumber'){
// this.validateArr.push(()=>{/**是否是数字*/})
// }

// 策略模式解决
// 简单if-else可以很好解决
let strage = {
isPhone:function(params){},
isNumber: function(params){},
// 更多验证规则
}
this.validateArr.push(strage[fn]);

// 这里涉及的逻辑比较简单,如果逻辑更复杂一点,可能这一个简单的策略就无法解决
// 比如:由单一的条件变化,变成几个组合条件变化

// 状态模式
// 核心:根据对象不同的状态,让对象展示不同的行为, 相当于加了状态管理的策略模式
}
}

t.init.prototype.run = function(value){
while(this.validateArr.length > 0){
// _result 是约定好的验证结果的数据结构
// {success: true|false, msg:''}
let _result = this.validateArr.shift().run(value);
if(!_result.success){
this.sendMsg(_result.msg);
break;
}
}
}

t.init.prototype.sendMsg = function(msg){
this.msgDom.innerHtml = msg;
}

// 后期的使用 职责链模式
t('input', 'errorMsg')
.add('isPhone')
.add(() => {/**自定义验证1 */})
.add(() => {/**自定义验证2 */})

需求2:一个div 实现 上、下、左、右、左上、左下、….这样移动
moveDiv(‘left’) // 左移
moveDiv(‘left’,’top’) // 左上移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function moveDiv(){
if(arguments.length === 1){
// 可以用策略模式
if(arguments[0] == 'left'){
moveLeft();
} else if (arguments[0] === 'right'){
moveRight();
}
} else if(arguments.length === 2){
// 这里简单的策略模式就不容易了
if(arguments[0] == 'left' && arguments[1] == 'top'){
moveLeft();
moveTop();
}
}
}

[状态模式实现moveDiv]
移动Div

需求2:一个div 实现 上、下、左、右、左上、左下、….这样移动
moveDiv(‘left’) // 左移
moveDiv(‘left’,’top’) // 左上移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 用状态模式实现
// 这时候moveDiv就成了一个类了,不是一个方法了
function moveDiv(params){
this.stateArr = [];//因为存在复合运动的行为,所以需要一个数组去存储数据
}
moveDiv.prototype.run = function(params){
// arguments 类数组 将类数组转成真正数组
// 这里有几种方法把类数组转成数组?
this.stateArr = Array.prototype.slice.call(arguments);
// 策略模式
let strage = {
left: moveLeft,
right:moveRight,
top:moveTop
};
this.stateArr.forEach(state => {
strage[state]();
});
}
let moveObj = new moveDiv();
moveObj.run('left', 'top');

使用设计模式之后,这个模块的代码更简洁更易读了


享元模式

存在类似对象和类似代码块时,用于减少类似代码块
这个享元是提取 相同的内容,还是不同的内容?
享元模式里的享无是 代码中的不同点
把不同的提出来,剩下的就是相同的,这样代码块就由多化一了
与平时提取公共代码不是一个意思

以jquery.extend方法为例
extend实现的功能:
$.extend({a:1}) // 会把对象扩展到$对象上$.a = 1; 为jquery对象扩展方法属性使用
$.extend({a:1},{b:2}) // ==> {a:1,b:2}

1
2
3
4
5
6
7
8
9
10
11
12
// 不用设计模式时,是这样实现的
$.extend = function(){
if(arguments.length === 1){
for(let item in arguments[0]){
this[item] = arguments[0][item]
}
} else if(arguments.length === 2){
for(let item in arguments[1]){
arguments[0][item] = arguments[1][item];
}
}
}

[jquery的extend 享元模式实现]
jquery.extend 享元模式实现

extend实现的功能:
$.extend({a:1}) // 会把对象扩展到$对象上$.a = 1; 为jquery对象扩展方法属性使用
$.extend({a:1},{b:2}) // ==> {a:1,b:2}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 不用设计模式时,是这样实现的
$.extend = function(){
// 享元模式,提取不同点:
// 1、for in 的对象不同,
// 2、接收的对象不同
let source = arguments[0];
let target = this;
if(arguments.length === 2){
source = arguments[1];
target = arguments[0];
}
for(let item in source){
target[item] = source[item]
}

}

总结应用场景: 两个if else 分支中,两段代码块非常相似时,就可以用享元模式了

3、行为型设计模式

模块间的行为模式的总结,帮助组织模块的沟通:装饰者 观察者

装饰者模式

目的:不重写方法的扩展方法
应用场景:当一个方法需要扩展,但又不方便或不能去修改方法时 是公共的方法 或他人的方法 或原生方法 或第三方模块的方法
简单理解 在不去修改原方法的情况下,扩展方法的功能

需求: 删除按钮–> 点击可以删除 —> 但没有提示 —> 好多删除按钮都是这样实现,产品需求是要给出删除提示
分析:在原来的删除功能基础上,扩展出提示功能

1、全部改写 2、找到原来定义,修改原方法,增加提示

[你会选哪种方式实现?]
装饰者模式

目的:不重写方法的扩展方法
应用场景:当一个方法需要扩展,但又不方便或不能去修改方法时 是公共的方法 或他人的方法 或原生方法 或第三方模块的方法
简单理解 在不去修改原方法的情况下,扩展方法的功能

需求: 删除按钮–> 点击可以删除 —> 但没有提示 —> 好多删除按钮都是这样实现,产品需求是要给出删除提示
分析:在原来的删除功能基础上,扩展出提示功能

1、全部改写 2、找到原来定义,修改原方法,增加提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function decorate(dom, fn){
// 健壮性校验
if(typeof dom.onClick === 'function'){
// 装饰者 三步走
// 1、重写原方法,或定义新方法;
// 2、提取老方法,并调用
// 3、加入新方法

let _oldFn = dom.onClick; // 在方法被重写前提取
dom.onClick = function(params){
_oldFn.call(this);
fn.call(this);
}
}
}
// 使用
decorate('btnDel', function(params){
console.log('删除成功')
});

vue2 的双向数据绑定

1
2
3
4
5
6
// Vue的双向绑定  非数组属性
Object.defineProperty(vue,'dataKey',{
get: function(){ },
set: function(){ }
})
// 是对象里的属性变了,会触发, 它没办法用到数组上,那Vue对数组是怎么实现双向绑定的

[vue2 对数组属性实现双向绑定]
Vue2 数组的双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13

// 数组:利用装饰者模式,给数组的 push pop
let arr = ['push', 'pop', 'shift']
let arrProto = Array.prototype;
let arrCopy = Object.create(arrProto); // 为了不污染旧原型链,提前拷贝一份出来
arr.forEach(method => {
arrCopy[method] = function (){
let _ref = arrProto[method].apply(this, arguments);
dep.notify();// 触发更新
}
})
// 把data里的所有数组,都变成这里的这个arrCopy
// 主要是一些技巧

**观察者模式** > 应用场景: > 1、异步模块沟通 >>a 异步模块;b 同步模块; b需要a处理完成后a的消息 观察者 类似事件监听

2、方便加入新的模块, 本来没有想到某个模块会突然要加入的情况

例如:聊天室的沟通:我和你在聊天室的沟通(聊天室就是观察者),这样别人加入更容易
根本没有考虑过你要加入时,你非要加入,实在没办法了,才会引入观察者模式;
虽然使用观察者容易做到某件事儿,但它也确实要花费一些开销

看这样一个抽奖的需求

需求:抽奖转盘,特点是越转越快,
模块分析: 奖品初始化html –> 最终结果选定 –> 转动控制 –> 转动效果

转动控制模块 调用 转动效果模块,
转动效果只负责转动效果; 转完之后,询问控制模块接下来怎么转;
转动效果模块接收消息体{moveTime: 10, speed: 200} 多长时间内转动几个奖品 –> setInterval 异步了,
转动控制与转动效果的沟通问题在于:转动控制不知道什么时候转动效果结束,因为转动速度是不恒定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 先定义好几个模块

// 初始化模块
function htmlInit(){ }

// 抽奖结果模块
function getResult(){ }

// 转动控制模块
function moveControll(){ }

// 动画效果:1,2,3,4,5...10 依次高亮显示
function move(config){
let timer = setInterval(() => {
// 转完一个周期后需要向控制块请求接下来怎么转
// 不用设计模式时,就直接通过callback访问
// 要使用设计模式的话,应该用什么模式?
}, config.speed);
}

[抽奖转盘 观察者模式]
观察者模式

需求:抽奖转盘,特点是越转越快,
模块分析: 奖品初始化html –> 最终结果选定 –> 转动控制 –> 转动效果

转动控制模块 调用 转动效果模块,
转动效果只负责转动效果; 转完之后,询问控制模块接下来怎么转;
转动效果模块接收消息体{moveTime: 10, speed: 200} 多长时间内转动几个奖品 –> setInterval 异步了,
转动控制与转动效果的沟通问题在于:转动控制不知道什么时候转动效果结束,因为转动速度是不恒定的

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
73
74
75
// 观察者三要素:队列,注册,触发
function observer(params){
// 1、队列
this.message = {

}
}
// 2、注册
observer.prototype.regist = function(type, fn){
this.message[key] = fn;
}
// 3、触发
observer.prototype.fire = function(type){
this.message[key]();
}

var observerObj = new observer();

var _domArr = [];
function htmlInt(params){
for(let i = 1; i <= 5; i++){
_domArr.push(document.body.append(`<div>${i}</div>`));
}
}
function getResult(params){
let _num = Math.random() * 10 + 40; // 40是基础动画圈数
return Math.floor(_num, 0);
}
function moveControll(params){
let result = getResult();
let _circle = Math.floor(result/10, 0);// 基础动画圈
let _runCircle = 0; // 已经转了多少圈
let stopNum = result%10; // 最终停留的奖品数
let _speed = 200; // 转速
observerObj.regist('finish', () => {
let _time = 0; // 应该转几个数
_speed -=50; // 转一圈 速度加快50
_runCircle++; // 已转的圈数
if(_runCircle <= _circle){ // 未达到指定的圈数
_time = 10; // 继续转10个数
} else {
_time = stopNum;
}

move({moveTime: _time, speed: _speed});
});
}
// 动画效果:1,2,3,4,5...10 依次高亮显示
function move(config){
let nowIn = 0;
let rmNum = 9; // 移除效果的索引
let timer = setInterval(() => {
// 单独处理第10个跳第1个的情况
if(nowIn == 0){ // 代码相似了,可以优化了
_domArr[9].setAttribute('class', 'item');
_domArr[nowIn].setAttribute('class', 'item item-on');
} else{
_domArr[nowIn-1].setAttribute('class', 'item');
_domArr[nowIn].setAttribute('class', 'item item-on');
}

// 享元模式
// if(nowIn != 9){
// rmNum = nowIn--
// }
// _domArr[rmNum].setAttribute('class', 'item');
// _domArr[nowIn].setAttribute('class', 'item item-on');

nowIn++;
if(nowIn == config.moveTime){
clearInterval(timer);
observerObj.fire('finish');
}
}, config.speed);
}

创建型设计模式 –> 创建对象阶段使用
结构型设计模式 –> 用在一个对象内部代码优化
行为型设计模式 –> 模块之间的沟通交互

三、设计模式解决问题

  • 1、if-else模式
  • 2、减少重复代码
  • 3、更好的扩展方法
  • 4、解耦模块