本文會探討一下發布訂閱模式在前端的應用以及雙向綁定的實現原理。前端
軟件編程的設計模式起源於上世紀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模型實現的雙向綁定。