乐趣区

关于javascript:框架源码中用来提高扩展性的设计模式

为什么要进步代码扩展性

咱们写的代码都是为了肯定的需要服务的,然而这些需要并不是变化无穷的,当需要变更了,如果咱们代码的扩展性很好,咱们可能只须要简略的增加或者删除模块就行了,如果扩展性不好,可能所有代码都须要重写,那就是一场劫难了,所以进步代码的扩展性是势在必行的。怎么才算有好的扩展性呢?好的扩展性应该具备以下特色:

  1. 需要变更时,代码不须要重写。
  2. 部分代码的批改不会引起大规模的改变。有时候咱们去重构一小块代码,然而发现他跟其余代码都是杂糅在一起的,外面各种耦合,一件事件拆在几个中央做,要想改这一小块必须要改很多其余代码。那阐明这些代码的耦合太高,扩展性不强。
  3. 能够很不便的引入新性能和新模块。

怎么进步代码扩展性?

当然是从优良的代码身上学习了,本文会深刻 AxiosNode.jsVue 等优良框架,从他们源码总结几种设计模式进去,而后再用这些设计模式尝试解决下工作中遇到的问题。本文次要会讲 职责链模式 观察者模式 适配器模式 装璜器模式。上面一起来看下吧:

职责链模式

职责链模式顾名思义就是一个链条,这个链条上串联了很多的职责,一个事件过去,能够被链条上的职责顺次解决。他的益处是链条上的各个职责,只须要关怀本人的事件就行了,不须要晓得本人的上一步是什么,下一步是什么,跟高低的职责都不耦合,这样当高低职责变动了,本人也不受影响,往链条上增加或者缩小职责也十分不便。

实例:Axios 拦截器

用过 Axios 的敌人应该晓得,Axios 的拦截器有 申请拦截器 响应拦截器 ,执行的程序是 申请拦截器 -> 发动申请 -> 响应拦截器,这其实就是一个链条上串起了三个职责。上面咱们来看看这个链条怎么实现:

// 先从用法动手,个别咱们增加拦截器是这样写的 
// instance.interceptors.request.use(fulfilled, rejected)
// 依据这个用法咱们先写一个 Axios 类。function Axios() {
  // 实例上有个 interceptors 对象,外面有 request 和 response 两个属性
  // 这两个属性都是 InterceptorManager 的实例
  this.interceptors = {request: new InterceptorManager(),
    response: new InterceptorManager()};
}

// 而后是实现 InterceptorManager 类
function InterceptorManager() {
  // 实例上有一个数组,存储拦截器办法
  this.handlers = [];}

// InterceptorManager 有一个实例办法 use
InterceptorManager.prototype.use = function(fulfilled, rejected) {
  // 这个办法很简略,把传入的回调放到 handlers 外面就行
  this.handlers.push({
    fulfilled,
    rejected
  })
}

下面的代码其实就实现了拦截器创立和 use 的逻辑,并不简单,那这些拦截器办法都是什么时候执行呢?当然是咱们调用 instance.request 的时候,调用 instance.request 的时候真正执行的就是 申请拦截器 -> 发动申请 -> 响应拦截器 链条,所以咱们还须要来实现下Axios.prototype.request:

Axios.prototype.request = function(config) {
  // chain 外面存的就是咱们要执行的办法链条
  // dispatchRequest 是发动网络申请的办法,本文次要讲设计模式,这个办法就不实现了
  // chain 外面先把发动网络申请的办法放进去,他的地位应该在 chain 的两头
  const chain = [dispatchRequest, undefined];
  
  // chain 后面是申请拦截器的办法, 从 request.handlers 外面取出来放进去
  this.interceptors.request.handlers.forEach(function unshiftRequestInterceptors(interceptor) {chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  // chain 前面是响应拦截器的办法,从 response.handlers 外面取出来放进去
  this.interceptors.response.handlers.forEach(function pushResponseInterceptors(interceptor) {chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  // 通过上述代码的组织,chain 这时候是这样的:// [request.fulfilled, request.rejected, dispatchRequest, undefined, response.fulfilled,  
  // response.rejected]
  // 这其实曾经依照申请拦截器 -> 发动申请 -> 响应拦截器的程序排好了,拿来执行就行
  
  let promise = Promise.resolve(config);   // 先来个空的 promise,好开启 then
  while (chain.length) {
    // 用 promise.then 进行链式调用
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
}

上述代码是从 Axios 源码中精简进去的,能够看出他奇妙的使用了职责链模式,将须要做的工作组织成一个链条,这个链条上的工作互相不影响,拦截器可有可无,而且能够有多个,兼容性十分强。

实例:职责链组织表单验证

看了优良框架对职责链模式的使用,咱们再看看在咱们平时工作中这个模式怎么使用起来。当初假如有这样一个需要是做一个表单验证,这个验证须要前端先对格局等内容进行校验,而后 API 发给后端进行合法性校验。咱们先剖析下这个需要,前端校验是同步的,后端验证是异步的,整个流程是同步异步交错的,为了能兼容这种状况,咱们的每个验证办法的返回值都须要包装成 promise 才行

// 前端验证先写个办法
function frontEndValidator(inputValue) {return Promise.resolve(inputValue);      // 留神返回值是个 promise
}

// 后端验证也写个办法
function backEndValidator(inputValue) {return Promise.resolve(inputValue);      
}

// 写一个验证器
function validator(inputValue) {
  // 仿照 Axios,将各个步骤放入一个数组
  const validators = [frontEndValidator, backEndValidator];
  
  // 后面 Axios 是循环调用 promise.then 来执行的职责链,咱们这里换个形式,用 async 来执行下
  async function runValidate() {
    let result = inputValue;
    while(validators.length) {result = await validators.shift()(result);
    }
    
    return result;
  }
  
  // 执行 runValidate,留神返回值也是一个 promise
  runValidate().then((res) => {console.log(res)});
}

// 上述代码曾经能够执行了,只是咱们没有具体的校验逻辑,输出值会一成不变的返回
validator(123);     // 输入: 123

上述代码咱们用职责链模式组织了多个校验逻辑,这几个校验之间相互之间没有依赖,如果当前须要缩小某个校验,只须要将它从 validators 数组中删除即可,如果要增加就往这个数组增加就行了。这几个校验器之间的耦合度就大大降低了,而且他们封装的是 promise,齐全还能够用到其余模块去,其余模块依据须要组织本人的职责链就行了。

观察者模式

观察者模式还有个名字叫公布订阅模式,这在 JS 的世界里可是赫赫有名,大家或多或少都用到过,最常见的就是事件绑定了,有些面试还会要求面试者手写一个事件核心,其实就是一个观察者模式。观察者模式的长处是能够让事件的产生者和消费者互相不晓得,只须要产生和生产相应的事件就行,特地适宜事件的生产者和消费者不不便间接调用的状况,比方异步中。咱们来手写一个观察者模式看看:

class PubSub {constructor() {
    // 一个对象寄存所有的音讯订阅
    // 每个音讯对应一个数组,数组构造如下
    // {//   "event1": [cb1, cb2]
    // }
    this.events = {}}

  subscribe(event, callback) {if(this.events[event]) {
      // 如果有人订阅过了,这个键曾经存在,就往里面加就好了
      this.events[event].push(callback);
    } else {
      // 没人订阅过,就建一个数组,回调放进去
      this.events[event] = [callback]
    }
  }

  publish(event, ...args) {
    // 取出所有订阅者的回调执行
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      subscribedEvents.forEach(callback => {callback.call(this, ...args);
      });
    }
  }

  unsubscribe(event, callback) {
    // 删除某个订阅,保留其余订阅
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }
}

// 应用的时候
const pubSub = new PubSub();
pubSub.subscribe('event1', () => {});    // 注册事件
pubSub.publish('event1');                // 公布事件

实例:Node.js 的 EventEmitter

观察者模式的一个典型利用就是 Node.js 的 EventEmitter,我有另一篇文章从公布订阅模式动手读懂 Node.js 的 EventEmitter 源码从异步利用的角度具体解说了观察者模式的原理和 Node.js 的 EventEmitter 源码,我这里就不反复书写了,下面的手写代码也是来自这篇文章。

实例:转圈抽奖

一样的,看了优良框架的源码,咱们本人也要试着来用一下,这里的例子是转圈抽奖。想必很多敌人都在网上抽过奖,一个转盘,外面各种奖品,点一下抽奖,而后指针开始旋转,最初会停留到一个奖品那里。咱们这个例子就是要实现这样一个 Demo,然而还有一个要求是每转一圈速度就放慢一点。咱们来剖析下这个需要:

  1. 要转盘抽奖,咱们必定先要把转盘画进去。
  2. 抽奖必定会有个后果,有奖还是没奖,具体是什么奖品,个别这个后果都是 API 返回的,很多实现计划是点击抽奖就发动 API 申请拿到后果了,转圈动画只是个成果而已。
  3. 咱们写一点代码让转盘动起来,须要一个静止成果
  4. 每转一圈咱们须要加快速度,所以还须要管制静止的速度

通过下面的剖析咱们发现一个问题,转盘静止是须要一些工夫的,当他静止完了须要通知管制转盘的模块加快速度进行下一圈的静止,所以静止模块和管制模块须要一个异步通信,这种异步通信就须要咱们的观察者模式来解决了。最终成果如下,因为只是个 DEMO,我就用几个 DIV 块来代替转盘了:

上面是代码:

// 先把之前的公布订阅模式拿过去
class PubSub {constructor() {this.events = {}
  }

  subscribe(event, callback) {if(this.events[event]) {this.events[event].push(callback);
    } else {this.events[event] = [callback]
    }
  }

  publish(event, ...args) {const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      subscribedEvents.forEach(callback => {callback.call(this, ...args);
      });
    }
  }

  unsubscribe(event, callback) {const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }
}

// 实例化一个事件核心
const pubSub = new PubSub();

// 总共有 初始化页面 -> 获取最终后果 -> 静止成果 -> 静止管制 四个模块
// 初始化页面
const domArr = [];
function initHTML(target) {
  // 总共 10 个可选奖品,也就是 10 个 DIV
  for(let i = 0; i < 10; i++) {let div = document.createElement('div');
    div.innerHTML = i;
    div.setAttribute('class', 'item');
    target.appendChild(div);
    domArr.push(div);
  }
}

// 获取最终后果,也就是总共须要转几次,咱们采纳一个随机数加 40(4 圈)
function getFinal() {let _num = Math.random() * 10 + 40;

  return Math.floor(_num, 0);
}

// 静止模块,具体静止办法
function move(moveConfig) {
  // moveConfig = {
  //   times: 10,     // 本圈挪动次数
  //   speed: 50      // 本圈速度
  // }
  let current = 0; // 以后地位
  let lastIndex = 9;   // 上个地位

  const timer = setInterval(() => {
    // 每次挪动给以后元素加上边框, 移除上一个的边框
    if(current !== 0) {lastIndex = current - 1;}

    domArr[lastIndex].setAttribute('class', 'item');
    domArr[current].setAttribute('class', 'item item-on');

    current++;

    if(current === moveConfig.times) {clearInterval(timer);

      // 转完了一圈播送事件
      if(moveConfig.times === 10) {pubSub.publish('finish');
      }
    }
  }, moveConfig.speed);
}

// 静止管制模块,管制每圈的参数
function moveController() {let allTimes = getFinal();
  let circles = Math.floor(allTimes / 10, 0);
  let stopNum = allTimes % circles;
  let speed = 250;  
  let ranCircle = 0;

  move({
    times: 10,
    speed
  });    // 手动开启第一次旋转

  // 监听事件,每次旋转实现主动开启下一次旋转
  pubSub.subscribe('finish', () => {
    let time = 0;
    speed -= 50;
    ranCircle++;

    if(ranCircle <= circles) {time = 10;} else {time = stopNum;}

    move({
      times: time,
      speed,
    })
  });
}

// 绘制页面,开始转动
initHTML(document.getElementById('root'));
moveController();

上述代码的难点就在于静止模块的静止是异步的,须要在每圈静止完了之后告诉静止管制模块进行下一次转动,观察者模式很好的解决了这个问题。本例残缺代码我曾经上传到我的 GitHub 了,能够去拿下来运行下玩玩。

装璜器模式

装璜器模式针对的状况是我有一些老代码,然而这些老代码性能不够,须要增加性能,然而我又不能去改老代码,比方 Vue 2.x 须要监听数组的扭转,给他增加响应式,然而他又不能间接批改Array.prototype。这种状况下,就特地适宜应用装璜者模式,给老办法从新装璜下,变成一个新办法来应用。

根本构造

装璜器模式的构造也很简略,就是先调用一下原来的办法,而后加上更多的操作,就是装璜一下。

var a = {b: function() {}}

function myB() {
  // 先调用以前的办法
  a.b();
  
  // 再加上本人的新操作
  console.log('新操作');
}

实例:Vue 数组的监听

相熟 Vue 响应式原理的敌人都晓得,Vue 2.x 对象的响应式是通过 Object.defineProperty 实现的,然而这个办法不能监听数组的扭转,那数组怎么监听的呢?数组操作个别就是 pushshift 这些办法,这些办法是数组原生的办法,咱们当然不能去改他,那会了装璜器模式,咱们齐全能够在放弃他之前性能的根底上给他扩大性能:

var arrayProto = Array.prototype;    // 先拿到原生数组的原型
var arrObj = Object.create(arrayProto);     // 用原生数组的原型创立一个新对象,省得净化原生数组
var methods = ['push', 'shift'];    // 须要扩大的办法,这里只写了两个,然而不止这两个

// 循环 methods 数组,扩大他们
methods.forEach(function(method) {
  // 用扩大的办法替换 arrObj 上的办法
  arrObj[method] = function() {var result = arrayProto[method].apply(this, arguments);    // 先执行老办法
    dep.notify();     // 这个是 Vue 的办法,用来做响应式
    return result;
  }
});

// 对于用户定义的数组,手动将它的原型指向扩大了的 arrObj
var a = [1, 2, 3];
a.__proto__ = arrObj;

上述代码是从 Vue 源码精简过去的,其实就是一个典型的应用装璜器扩大原有办法的性能的例子,因为 Vue 只扩大了数组办法,如果你不通过这些办法,而是间接通过下标来操作数组,响应式就不起作用了。

实例:扩大已有的事件绑定

老规矩,学习了人家的代码,咱们本人也来试试。这个例子面临的需要是咱们须要对已有的 DOM 点击事件上减少一些操作。

// 咱们以前的点击事件只须要打印 1
dom.onclick = function() {console.log(1);
}

然而咱们当初的需要要求还要输入一个 2,咱们当然能够返回原来的代码将他改掉,然而咱们也能够用装璜者模式给他增加性能:

var oldFunc = dom.onclick;  // 先将老办法拿进去
dom.onclick = function() {   // 从新绑定事件
  oldFunc.apply(this, arguments);  // 先执行老的办法
  
  // 而后增加新的办法
  console.log(2);
}

上述代码就扩大了 dom 的点击事件,然而如果须要批改的 DOM 元素很多,咱们要一个一个的去从新绑定事件,又会有大量类似代码,咱们学设计模式的目标之一就是要防止反复代码,于是咱们能够将专用的绑定操作提取进去,作为一个装璜器:

var decorator = function(dom, fn) {
  var oldFunc = dom.onclick;
  
  if(typeof oldFunc === 'function'){dom.onclick = function() {oldFunc.apply(this, arguments);
      fn();}
  }
}

// 调用装璜器,传入参数就能够扩大了
decorator(document.getElementById('test'), function() {console.log(2);
})

这种形式特地适宜咱们引入的第三方 UI 组件,有些 UI 组件本人封装了很多性能,然而并没有暴露出接口,如果咱们要增加性能,又不能间接批改他的源码,最好的办法就是这样应用装璜器模式来扩大,而且有了装璜工厂之后,咱们还能够疾速批量批改。

适配器模式

适配器想必大家都用过,我家里的老显卡只有 HDMI 接口,然而显示器是 DP 接口,这两个插不上,怎么办呢?答案就是买个适配器,将 DP 接口转换为 HDMI 的就行了。这里的适配器模式原理相似,当咱们面临接口不通用,接口参数不匹配等状况,咱们能够在他里面再包一个办法,这个办法接管咱们当初的名字和参数,外面调用老办法传入以前的参数模式。

根本构造

适配器模式的根本构造就是上面这样,假如咱们要用的打 log 的函数叫 mylog,然而具体方法咱们又想调用现成的window.console.log 实现,那咱们就能够给他包一层。

var mylog = (function(){return window.console.log;})()

如果感觉下面的构造太简略了,依然不晓得怎么使用,咱们上面再通过一个例子来看下。

实例:框架变更了

如果咱们当初面临的一个问题是公司以前始终应用的 A 框架,然而当初决定换成 jQuery 了,这两个框架大部分接口是兼容的,然而局部接口不适配,咱们须要解决这个问题。

// 一个批改 css 的接口
$.css();      // jQuery 叫 css
A.style();    // A 框架叫 style

// 一个绑定事件的接口
$.on();       // jQuery 叫 on
A.bind();     // A 框架叫 bind

当然咱们全局搜寻把应用的中央改掉也行,然而如果应用适配器批改可能更优雅:

// 间接把以前用的 A 替换成 $
window.A = $;

// 适配 A.style
A.style = function() {return $.css.apply(this, arguments);    // 放弃 this 不变
}

// 适配 A.bind
A.bind = function() {return $.on.apply(this, arguments);
}

适配器就是这么简略,接口不一样,包一层改成一样就行了。

实例:参数适配

适配器模式不仅仅能够像下面那样来适配接口不统一的状况,还能够用来适配参数的多样性。如果咱们的一个办法须要接管一个很简单的对象参数,比方 webpack 的配置,可能有很多选项,然而用户可能只用到局部,或者用户可能传入不反对的配置,那咱们须要一个将用户传入的配置适配到标准配置的过程,这个做起来其实也很简略:

// func 办法接管一个很简单的 config
function func(config) {
  var defaultConfig = {
    name: 'hong',
    color: 'red',
    // ......
  };
  
  // 为了将用户的配置适配到标准配置,咱们间接循环 defaultConfig
  // 如果用户传入了配置,就用用户的,如果没传就用默认的
  for(var item in defaultConfig) {defaultConfig[item] = config[item] || defaultConfig[item];
  }
}

总结

  1. 高扩展性的外围其实就是高内聚,低耦合,各个模块都专一在本人的性能,尽量减少对外部的间接依赖。
  2. 职责链模式和观察者模式次要是用来升高模块间耦合的,耦合低了就能够很不便的对他们进行组织,给他们扩大性能,适配器模式和装璜器模式次要是用来在不影响原有代码的根底上进行扩大的。
  3. 如果咱们须要对某个对象进行一系列的操作,这些操作能够组织成一个链条,那咱们能够思考应用职责链模式。链条上的具体任务不须要晓得其余工作的存在,只专一本人的工作,音讯的传递由链条负责。应用了职责链模式,链条上的工作能够很不便的减少,删除或者从新组织成新的链条,就像一个流水线一样。
  4. 如果咱们有两个对象在不确定的工夫点须要异步通信,咱们能够思考应用观察者模式,使用者不须要始终关注其余特定的对象,他只有在音讯核心注册一个音讯,当这个音讯呈现时,音讯核心会负责来告诉他。
  5. 如果咱们曾经拿到了一些旧代码,然而这些旧代码不能满足咱们的需要,咱们又不能随便更改他,咱们能够思考应用装璜器模式来加强他的性能。
  6. 对于旧代码革新或者新模块引入,咱们可能面临接口不通用的状况,这时候咱们能够思考写一个适配器来适配他们。适配器模式同样实用于参数适配的状况。
  7. 还是那句话,设计模式更重视的是思维,不必生吞活剥代码模板。也不要在所有中央硬套设计模式,而是在真正须要的时候才应用他来减少咱们代码的可扩展性。

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。

本文次要素材来自于网易高级前端开发工程师微业余唐磊老师的设计模式课程。

作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges

我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢送关注~

退出移动版