本文会探讨一下发布订阅模式在前端的应用以及双向绑定的实现原理。前端
软件编程的设计模式起源于上世纪90年代:软件编程开发中,会有一些比较经典的问题以及对应方法,能够概括总结出来成为通用的思路和方式,以便在后续软件开发人员借鉴使用,上世纪90年代逐渐出现一些零星的设计模式出现,而比较系统而且有表明性的设计模式则是由Design Patterns: Elements of Reusable Object-Oriented Software一书出版后流行开来。node
和算法与数据结构同样,设计模式也是优秀程序必须学习掌握的重要一项技能,可是没必要刻意追求深刻学习某种设计模式,当工程复杂到必定程度或者面对某些复杂需求,就天然会在实践中使用某种设计模式。算法
发布订阅模式(观察者模式),在某些文章中,例Observer vs Pub-Sub Pattern,会将观察者模式和发布订阅模式说成两种模式,不过笔者认为,其实从核心思想来看,仍是一类模式,主要解决一类问题。编程
发布订阅模式中涉及着信息的获取行为,根据获取信息的一方,能够将发布订阅模式分为“推”模式和“拉”模式;根据发布订阅模式的实现程度能够分为“观察者模式”和“发布订阅模式”。设计模式
概念图以下: bash
首先,从“推模式”开始,先看这样一份代码,能够假设发布者和订阅者分别是报社和订报纸的顾客.前端框架
const Publisher = new Observable; const subscriber = function (news) { } Publisher.subscribe(subscriber) .notify('a new news!') .unsubscribe(Subscriber) .notify('a new news!') 复制代码
在这个模型中,报社处于主导地位,负责较多的功能:markdown
管理订阅的顾客而且有权利中止为顾客投送;数据结构
在新报纸出现后为订阅的顾客投送报纸。app
下面是一份完整实现这样功能的可执行代码(简单版本):
//简单版本 class Observable { constructor() { this.subscribers = [] } subscribe(fn) { const ifExist = this.subscribers.some((existSubscriber) => { return existSubscriber = fn }) if (!ifExist) { this.subscribers.push(fn) } return this; } notify() { this.subscribers.map(fn => { fn.apply(this, arguments) }) return this; } unsubscribe(fn) { this.subscribers = this.subscribers.filter((existSubscriber) => { return existSubscriber !== fn; }) return this; } } const Publisher = new Observable; const subscriber = function (message) { console.log('recieved ' + message) } Publisher.subscribe(subscriber) .notify('a new message!') .unsubscribe(Subscriber) .notify('a new message!') 复制代码
若是顾客只想订阅某个频道的消息,不想每一个消息都被推送到的话怎么处理呢?咱们能够更改一下subscribers
的数据结构来处理。
//带key版本(带频道的版本) class Observable { constructor() { this.subscribers = {} } subscribe(key, fn) { if (!this.subscribers[key]) { this.subscribers[key] = [] } const ifExist = this.subscribers[key].some((existSubscriber) => { return existSubscriber === fn }) if (!ifExist) { this.subscribers[key].push(fn) } return this; } notify() { const key = Array.prototype.shift.call(arguments), fns = this.subscribers[key] if (!fns || fns.length === 0) { return false; } fns.forEach(fn => { fn.apply(this, arguments) }); return this; } unsubscribe(key, fn) { if (!this.subscribers[key]) { return this; } this.subscribers[key] = this.subscribers[key].filter((existSubscriber) => { return existSubscriber !== fn; }) return this; } } const Publisher = new Observable; const subscriberA = function (message) { console.log('subscriberA recieved ' + message) } const subscriberB = function (message) { console.log('subscriberB recieved ' + message) } Publisher.subscribe('sport', subscriberA) .subscribe('weather', subscriberA) .subscribe('weather', subscriberB) .notify('sport', 'a new message about sport!') .notify('weather', 'a new message about weather') .unsubscribe('weather', subscriberA) .notify('weather', 'a new message about weather! ') 复制代码
“推”模式的发布订阅就这样实现了,你们能够思考一下如何实现一个“拉”模式的发布订阅模式。(待修改)
以上的设计实现能够说是发布者和订阅者比较耦合的场景,发布者内部须要实现管理订阅者,以及推送消息等功能,而订阅者严重依赖发布者,若是有多个发布者呢?若是想减小发布者和订阅者之间的耦合性呢?那么就能够引入一个“中介”来达到这样一个目的。(待修改)
发布订阅模式能够在大型程序中用于解耦,可使各个模块开发时没必要担忧其余模块迭代带来的影响,例如,一个博客产品,在登陆成功后可能会有获取评论、获取权限内文章等等这样的功能,在引入发布订阅模式以前,可能面临着在登陆模块中,引入评论模块以及文章模块的一些代码,若是这三个模块分别三个项目组开发,那可能面临着评论模块升级必须有登陆模块的人员配合联调。在引入发布订阅模式后,这类耦合性严重的开发迭代问题将不复存在。
当面临大量频繁数据改变时,经过一次注册监听数百、上千次的数据变化能够减小屡次的事件监听次数。
发布订阅模式在首次建立可观察对象时会带来比较大的开销,因此使用场景比较适合一次建立,屡次使用的场景。同时,在js这样单线程模型中,可使用惰性加载以及预先加载的技术来避免和主进程冲突,影响程序性能。
双向绑定能够将视图和数据绑定在一块儿,减小咱们频繁的视图与数据之间的同步。
首先,为了比较清晰地了解双向绑定的原理,咱们须要实现一个简单版本的双向绑定,目标有两个:
1.当input
和textarea
等变化时,实时改变数据而且响应到用来展现其的span
等标签上。
2.能够经过js来指定须要双向绑定的key
,而且改变其value
。
从HTML开始,代码以下:
<div> Name: <input data-bind="name" type="text"> <span data-bind="name"></span> <br> Email: <input data-bind="email" type="text"> <span data-bind="email"></span> </div> 复制代码
利用data属性标记来帮助定位数据,用这种方式代替Angular
和Vue
中template
的功能,从而简化代码复杂性,便于理解。
在这个简单双向绑定模型中,为了实现两个目标需求,提供的API以下
const mvvm = new MVVM(); mvvm.set("name", "free"); mvvm.set("email"); 复制代码
和全部前端框架同样,第一步,须要实例化咱们的简单框架;而后实例提供一个set
方法,set
方法接受两个参数,key
对应HTML代码中data-bind
的值,是一个必选值,value
对应着双向绑定数据的值,是一个可选值。
在开始实现这个简单框架以前,让咱们仔细想一下这样一个简单框架中有哪些行为会触发数据变化:1)input
和textarea
中的输入行为;2)框架API提供的手动修改绑定数据的行为。
首先,咱们先设定一个scope
的JSON对象用来存储须要双向绑定的key和value,而后在实例化这个框架的时,对须要须要双向绑定的元素添加事件监听,当事件触发时,使用本简单框架中提供的set方法来更新数据。
class MVVM { constructor() { this.scope = {} const elements = Array.from(document.querySelectorAll('[data-bind]')) elements.forEach(element => { if (checkBindElement(element)) { const currentKey = element.getAttribute('data-bind') const listenEvents = ['input'] listenEvents.map(event => { element.addEventListener(event, (e) => { if (this.checkKeyInScope(currentKey)) { this.set(currentKey, element.value) } }) }) } }) } //... } 复制代码
须要注意querySelectorAll
返回的是一个HTML的nodelist并非Array,可使用foreach但不能直接使用map。checkBindElement
是用来检查是不是input
或者textarea
,checkKeyInScope
用来检查是否已经含有该key。
而后咱们须要一个方法,可以响应constructor
中函数监听到的数据变化的行为,而且在数据变化的同时,响应到依赖该数据的UI上。经过在Object.defineProperty
中的set
拦截方法,咱们能够比较容易的实现这一功能。
set(key, value) { if (key) { this.bindKey(key) } if (value) { this.scope[key] = value; } } bindKey(key) { if (!this.checkKeyInScope(key)) { const aimElements = document.querySelectorAll(`[data-bind=${key}]`); if (aimElements.length > 0) { Object.defineProperty(this.scope, key, { set: function (newValue) { aimElements.forEach((element) => { if (checkBindElement(element)) { element.value = newValue } else { element.innerHTML = newValue } }) }, get: function (value) { return value; }, enumerable: true }) } } } 复制代码
最后,总体的实现代码以下:
function checkBindElement(element) { const nodeName = element.nodeName && element.nodeName.toLowerCase(); if (nodeName === 'input' || nodeName === 'textarea') { return true } else { return false } } class MVVM { constructor() { this.scope = {} const elements = Array.from(document.querySelectorAll('[data-bind]')) elements.forEach(element => { if (checkBindElement(element)) { const currentKey = element.getAttribute('data-bind') const listenEvents = ['input'] listenEvents.map(event => { element.addEventListener(event, (e) => { if (this.checkKeyInScope(currentKey)) { this.set(currentKey, element.value) } }) }) } }) } set(key, value) { if (key) { this.bindKey(key) } if (value) { this.scope[key] = value; } } bindKey(key) { if (!this.checkKeyInScope(key)) { const aimElements = document.querySelectorAll(`[data-bind=${key}]`); if (aimElements.length > 0) { Object.defineProperty(this.scope, key, { set: function (newValue) { aimElements.forEach((element) => { if (checkBindElement(element)) { element.value = newValue } else { element.innerHTML = newValue } }) }, get: function (value) { return value; }, enumerable: true }) } } } checkKeyInScope(key) { if (this.scope.hasOwnProperty(key)) { return true } else { return false; } } } var mvvm = new MVVM() mvvm.set("name", "jeremy"); mvvm.set("email"); 复制代码
能够试试运行它,在控制台中调用mvvm实例内的方法来直接改变目标数据哦。
附:其实这个简单方法实现的是相似Vue的基于数据劫持的双向绑定;有兴趣能够本身实现基于脏检查结合原生pubsub模型实现的双向绑定。