JavaScript 設計模式解析【3】——行爲型設計模式

系列目錄:javascript

策略模式

策略模式就是定義一系列的算法,把它們一個個封裝起來,而且使它們能夠相互替換。html

策略模式的核心是講算法的使用與算法的實現分離開。java

一個策略模式的程序至少要包含兩部分:node

  • 一組策略類,策略類封裝了具體的算法,而且負責具體的計算過程
  • 環境類,接收客戶的請求,隨後把請求委託給某一個策略類。要作到這點,說明環境類中要維持對某個策略對象的引用。

實現一個策略模式

假設咱們要實現一個根據評分來打分的系統git

// 等級與成績的映射關係 
const levels = {
  S : 100,
  A : 90,
  B : 80,
  C : 70,
  D : 60
}

// 一組策略
let gradeBaseOnLevel = {
  S: () => {
    return `當前成績爲${levels['S']}`
  },
  A: () => {
    return `當前成績爲${levels['A']}`
  },
  B: () => {
    return `當前成績爲${levels['B']}`
  },
  C: () => {
    return `當前成績爲${levels['C']}`
  },
  D: () => {
    return `當前成績爲${levels['D']}`
  },
}

// 調用方法
function getStudentScore(level) {
  return levels[level] ? gradeBaseOnLevel[level]() : 0;
}

console.log(getStudentScore('S')); // 當前成績爲100
console.log(getStudentScore('A')); // 當前成績爲90
console.log(getStudentScore('B')); // 當前成績爲80
console.log(getStudentScore('C')); // 當前成績爲70
console.log(getStudentScore('D')); // 當前成績爲60
複製代碼

優缺點

  • 優勢: 能夠有效地避免多重條件語句,將一系列方法封裝起來也更直觀,利於維護
  • 缺點: 每每策略組會比較多,咱們須要事先知道全部定義好的狀況

迭代器模式

迭代器模式時指模塊提供的一種方法去順序訪問一個集合對象中的各個元素,而又不須要暴露該對象的內部表示。迭代器模式也能夠把迭代過程從業務邏輯中分離出來,使用迭代器模式後,即便不關心對象的內部構造,也能夠按順序訪問其中的每一個元素。github

其實咱們無形中已經使用了很多迭代器模式的功能,例如 JS 中數組的 map, 與 forEach已經內置了迭代器。算法

[1,2,3].forEach(function(item, index, arr) {
    console.log(item, index, arr);
});
複製代碼

同時迭代器分爲兩種:內部迭代器 與 外部迭代器設計模式

  • 內部迭代器

內部迭代器在調用時很是方便,外界不會去關係其內部的實現。在每次調用時,迭代器的規則就已經制定完畢,若是遇到一些不一樣樣的迭代規則,此時的內部迭代器就不是很清晰數組

  • 外部迭代器

外部迭代器會顯式地請求迭代下一個元素(next方法),外部迭代器雖然增長了調用的複雜度,可是加強了迭代器的靈活性,咱們能夠手動地控制迭代過程或者順序。就像Generator函數bash

手寫實現一個迭代器

咱們能夠經過代碼實現一個簡單的迭代器:

// 建立者類
class Creator {
  constructor(list) {
    this.list = list;
  }
  // 建立一個迭代器來進行遍歷
  creatIterator() {
    return new Iterator(this);
  }
}

// 迭代器類
class Iterator {
  constructor(creator) {
    this.list = creator.list;
    this.index = 0;
  }

  // 判斷是否完成遍歷
  isDone() {
    if (this.index >= this.list.length) {
      return true
    }
    return false
  }
  // 向後遍歷操做
  next() {
    return this.list[this.index++]
  }
}

let arr = [1,2,3,4];

let creator = new Creator(arr);
let iterator = creator.creatIterator();
console.log(iterator.list) // [1,2,3,4]
while (!iterator.isDone()) {
  console.log(iterator.next()); 
}

// 執行結果爲 1,2,3,4
複製代碼

ES6中的迭代器

JavaScript 中的有序數據集合包括:

  • Array
  • Map
  • Set
  • String
  • typeArray
  • arguments
  • NodeList

!! 注意 Object 不屬於有序數據集合

以上的有序數據集合都部署Symbol.iterator屬性,屬性值做爲一個函數,執行這個函數,返回一個迭代器,迭代器部署next方法來按順序遍歷訪問子元素

以數組對象爲例:

let array = [1,2,3];

let iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
複製代碼

總結

  • 同時迭代器分爲兩種:內部迭代器 與 外部迭代器, 內部迭代器操做方便,外部迭代器可控性強。
  • 任何部署了[Symbol.iterator]接口的數據均可以使用for of循環。
  • 迭代器模式使目標對象和迭代器對象分離,符合了開放封閉原則。

觀察者模式 (發佈-訂閱模式)

觀察者模式也稱做 發佈—訂閱模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,依賴於它的對象都將獲得通知。在JavaScript開發中,咱們通常用事件模型來替代傳統的發佈—訂閱模式。

總的來講,這種模式的實質就是你能夠對程序中的某個對象的狀態進行觀察,而且在其發生改變時可以獲得通知。

當前已經有了不少實用觀察者模式的例子,例如 DOM事件綁定就是一個很是典型的發佈—訂閱模式,還有 Vue.js 框架中的數據雙向綁定,也利用了觀察者模式

因此通常觀察者模式有兩種角色:

  • 觀察者 (發佈者)
  • 被觀察者 (訂閱者)

下面咱們舉一個具體的例子,假設有三個報紙出版社,報社一,報社二,報社三,有兩個訂報人,分別是:訂閱者1,訂閱者2.此時出版社就是被觀察者,訂報人就是觀察者。

咱們先定義報社類:

// 報社類
class Press {
  constructor(name) {
    this.name = name;
    this.subscribers = []; //此處存放訂閱者名單
  }

  deliver(news) {
    let press = this;
    // 循環訂閱者名單中全部的訂報人,爲他們發佈內容
    press.subscribers.map(item => {
      item.getNews(news, press); // 向每一個訂閱者發送新聞
    })
    // 實現鏈式調用
    return this;
  }
}

複製代碼

接着咱們定義訂報人類

// 訂報人類
class Subscriber {
  constructor(name) {
    this.name = name;
  }

  // 獲取新聞
  getNews(news, press) {
    console.log(`${this.name} 獲取來自 ${press.name} 的新聞: ${news}`)
  }
  // 訂閱方法
  subscribe(press) {
    let sub = this;
    // 避免重複訂閱
    if(press.subscribers.indexOf(sub) === -1) {
      press.subscribers.push(sub);
    }
    // 實現鏈式調用
    return this;
  }

  // 取消訂閱方法
  unsubscribe(press) {
    let sub = this;
    press.subscribers = press.subscribers.filter((item) => item !== sub);
    return this;
  }
}
複製代碼

以後咱們經過實際操做進行演示:

let press1 = new Press('報社一')
let press2 = new Press('報社二')
let press3 = new Press('報社三')

let sub1 = new Subscriber('訂報人一')
let sub2 = new Subscriber('訂報人二')

// 訂報人一訂閱報社1、二
sub1.subscribe(press1).subscribe(press2);
// 訂報人二訂閱報社2、三
sub2.subscribe(press2).subscribe(press3);

// 報社一發出新聞
press1.deliver('今每天氣晴');
// 訂報人一 獲取來自 報社一 的新聞: 今每天氣晴


// 報社二發出新聞
press2.deliver('今晚12點蘋果發佈會');
// 訂報人一 獲取來自 報社二 的新聞: 今晚12點蘋果發佈會
// 訂報人二 獲取來自 報社二 的新聞: 今晚12點蘋果發佈會

// 報社三發出新聞
press3.deliver('報社二即將倒閉,請你們儘快退訂');
// 訂報人二 獲取來自 報社三 的新聞: 報社二即將倒閉,請你們儘快退訂

// 訂報人二退訂
sub2.unsubscribe(press2);

press2.deliver('本報社已倒閉');
// 訂報人一 獲取來自 報社二 的新聞: 本報社已倒閉
複製代碼

上文咱們提到了,Vue.js的雙向綁定的原理是數據劫持和發佈訂閱,咱們能夠本身來實現一個簡單的數據雙向綁定

首先咱們須要有一個頁面結構

<div id="app">
    <h3>數據的雙向綁定</h3>
    <div class="cell">
        <div class="text" v-text="myText"></div>
        <input class="input" type="text" v-model="myText" >
  </div>
</div>
複製代碼

接着咱們建立一個類aVue

class aVue {
  constructor (options) {
    // 傳入的配置參數
    this.options = options;
    // 根元素
    this.$el = document.querySelector(options.el);
    // 數據域
    this.$data = options.data;

    // 保存數據model與view相關的指令,當model改變時,咱們會觸發其中的指令類更新
    this._directives = {};
    // 數據劫持,從新定義數據的 set 和 get 方法
    this._obverse(this.$data);
    // 解析器,解析模板指令,並將每一個指令對應的節點綁定更新函數,添加監聽數據的訂閱者
    // 一旦數據發生變更,收到通知,更新視圖
    this._complie(this.$el);
  }
  // 對數據進行處理,重寫set和get方法
  _obverse(data) {
    let val;
    // 進行遍歷
    for(let key in data) {
      // 判斷是否屬於自身的屬性
      if(data.hasOwnProperty(key)) {
        this._directives[key] = [];
      }

      val = data[key];
      if( typeof val === 'object') {
        // 遞歸遍歷
        this._obverse(val);
      }

      // 初始化當前數據的執行隊列
      let _dir = this._directives[key];

      // 從新定義數據的set 和 get 方法
      Object.defineProperty(this.$data, key, {
        // 可枚舉的
        enumerable: true,
        // 可改的
        configurable: true,
        get: () => val,
        set: (newVal) => {
          if (val !== newVal) {
            val = newVal;
            // 觸發_directives 中綁定的Watcher類更新
            _dir.map(item => {
              item._update();
            })
          }
        }
      })
    }
  }

  // 解析器,綁定節點,添加數據的訂閱者,更新視圖變化
  _complie(el) {
    // 子元素
    let nodes = el.children;
    for(let i = 0; i < nodes.length; i++) {
      let node = nodes[i];
      // 遞歸對全部元素進行遍歷
      if (node.children.length) {
        this._complie(node);
      }

      // 若是有 v-text 指令, 監控 node 的值,並及時更新
      if (node.hasAttribute('v-text')) {
        let attrValue = node.getAttribute('v-text');
        // 將指令對應的執行方法放入指令集
        this._directives[attrValue].push(new Watcher('text', node, this, attrValue, 'innerHTML'))
      }

      // 若是有 v-model 屬性,而且元素是 input 或者 textarea 咱們監聽input事件
      if( node.hasAttribute('v-model') && ( node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
        let _this = this;
        // 添加input事件
        node.addEventListener('input', (function(){
          let attrValue = node.getAttribute('v-model');
          // 初始化複製
          _this._directives[attrValue].push(new Watcher('input', node, _this, attrValue, 'value'));
          return function () {
            // 後面每次都會更新
            _this.$data[attrValue] = node.value;
          }
        })())
      }
    }
  }
}

複製代碼

_observe方法處理傳入的data,從新改寫data的setget方法,保證咱們能夠跟蹤到data的變化。

_compile方法本質是一個解析器,他經過解析模板指令,將每一個指令對應的節點綁定更新函數,並添加監聽數據的訂閱者,數據發生變化時,就去更新視圖變化。

接着咱們定義訂閱者類

class Watcher{
  /*
  * name 指令名稱
  * el 指令對應的DOM元素
  * vm 指令所屬的aVue實例
  * exp 指令對應的值,本例爲"myText"
  * attr 綁定的屬性值,本例爲"innerHTML"
  */
  constructor(name, el, vm, exp, attr) {
    this.name = name;
    this.el = el;
    this.vm = vm;
    this.exp = exp;
    this.attr = attr;


    // 更新操做
    this._update();
  }

  _update() {
    this.el[this.attr] = this.vm.$data[this.exp];
  }
}
複製代碼

_compile中,咱們建立了兩個Watcher實例,不過這兩個對應的_update的操做結果不一樣,對於div.text的操做實際上是div.innerHTML = this.data.myText,對於input的操做至關於input.value = this.data.myText,這樣每次數據進行set操做時,咱們會觸發兩個_update方法,分別更新divinput的內容。

udbfdx.gif

Finally,咱們成功地實現了一個簡單的雙向綁定。

示例Demo源碼可在 Observer 查看

總結

  • 觀察者模式可使代碼解耦合,知足開放封閉原則。
  • 當過多地使用觀察者模式時,若是訂閱消息一直沒有觸發,但訂閱者仍然一直保存在內存中。

命令模式

在軟件系統裏,行爲請求者行爲實現者一般呈現一種緊耦合,但在某些場合,好比要對行爲進行記錄、撤銷/重作、事務等處理時,這種沒法抵禦變化的緊耦合是不合適的。在這種狀況下,若是要將行爲請求者行爲實現者解耦合,咱們須要將一組行爲抽象爲對象,實現兩者之間的鬆耦合,這就是命令模式

咱們須要在命令的發佈者和接受者之間定義一個命令對象,命令對象會暴露一個統一的藉口給命令的發佈者而命令的發佈者不須要去了解接受者是如何執行命令的,作到了命令發佈者和接受者的解耦合。

u06DRP.png

咱們下面的例子中一個頁面有三個按鈕,每一個按鈕有不一樣的功能:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>命令模式</title>
</head>
<body>
    <div>
        <button id="btn1">Button1</button>
        <button id="btn2">Button2</button>
        <button id="btn3">Button3/button>
    </div>
    <script src="./Command.js"></script>
</body>
</html>
複製代碼

接下來咱們定義一個命令發佈者(執行者)的類

class Executor {
    setCommand(btn, command) {
        btn.onclick = function () {
            command.execute();
        }
    }
}  
複製代碼

接着咱們定義一個命令接受者,此例中爲一個菜單

// 定義一個命令接受者
class Menu {
    refresh() {
        console.log('刷新菜單')
    }

    addSubMenu() {
        console.log('增長子菜單')
    }

    deleteMenu() {
        console.log('刪除菜單')
    }
}
複製代碼

以後咱們將Menu的方法執行封裝在單獨的類中

class RefreshMenu {
    constructor(receiver) {
        // 使命令對象與接受者關聯
        this.receiver = receiver
    }
    // 暴露出統一接口給 Excetor
    execute() {
        this.receiver.refresh()
    }
}

// 定義一個接受子菜單的命令對象的類
class AddSubMenu {
    constructor(receiver) {
        // 使命令對象與接受者關聯
        this.receiver = receiver
    }

    // 暴露出統一的接口給 Excetor
    execute() {
        this.receiver.addSubMenu()
    }
}

// 定義一個接受刪除菜單對象的類
class DeleteMenu {
    constructor(receiver) {
        this.receiver  = receiver
    }

    // 暴露出統一的接口給 Excetor
    execute() {
        this.receiver.deleteMenu()
    }
}

複製代碼

以後咱們分別實例華不一樣的對象

// 首先獲取按鈕對象
let btn1 = document.getElementById('btn1')
let btn2 = document.getElementById('btn2')
let btn3 = document.getElementById('btn3')

let menu = new Menu()
let executor = new Executor()

let refreshMenu = new RefreshMenu(menu)

// 給 按鈕1 添加刷新功能
executor.setCommand(btn1, refreshMenu) // 點擊按鈕1 顯示"刷新菜單"

let addSubMenu = new AddSubMenu(menu)
// 給按鈕2添加子菜單功能
executor.setCommand(btn2, addSubMenu)// 點擊按鈕2 顯示"添加子菜單"

let deleteMenu = new DeleteMenu(menu)
// 給按鈕3添加刪除功能
executor.setCommand(btn3, deleteMenu)// 點擊按鈕3 顯示"刪除菜單"

複製代碼

總結

  • 發佈者與接受者實現瞭解耦合,符合單一職責原則。
  • 命令可擴展,對請求能夠進行排隊或日誌記錄,符合開放—封閉原則。
  • 但額外增長了命令對象,存在必定的多餘開銷。

狀態模式

狀態模式容許一個對象在其內部狀態改變時改變行爲,這個對象看上去像改變了類同樣,但其實並無。狀態模式把所研究對象在其內部狀態改變時,其行爲也隨之改變,狀態模式須要對每個系統可能取得的狀態創立一個狀態類的子類。當系統的狀態變化時,系統改變所選的子類。

假設咱們此時有一個電燈,在電燈上有個開關,在燈未亮時按下使燈開啓,在燈已亮時按下則將燈關閉,此時行爲表現時不同的:

class Light {
    constructor() {
        this.state = 'off'; //電燈默認爲關閉狀態
        this.button = null;
    }

    init() {
        let button = document.createElement('button');
        let self = this;
        button.innerHTML = '我是開關';
        this.button = document.body.appendChild(button);
        this.button.onclick = () => {
            self.buttonWasClicked();
        }
    }

    buttonWasClicked() {
        if (this.state === 'off') {
            console.log('開燈');
            this.state = 'on';
        } else {
            console.log('關燈');
            this.state = 'off';
        }
    }
}

let light = new Light();
light.init();
複製代碼

uD8U1A.gif

此時只有兩種狀態,咱們尚且能夠不使用狀態模式,但當狀態較多時,例如,當電燈出現弱光,強光檔位時,以上的代碼就沒法知足需求。

### 使用狀態模式重構

狀態模式的關鍵是把每種狀態都封裝成單獨的類,跟此種狀態有關的行爲都被封裝在這個類的內部,在按鈕被按下時,只須要在上下文中,把這個請求委託給當前狀態對象便可,該狀態對象會負責渲染它自身的行爲。

首先定義三種不一樣狀態的類

// 燈未開啓的狀態
class OffLightState {
    constructor(light) {
        this.light = light;
    }

    buttonWasClicked() {
        console.log('切換到弱光模式');
        this.light.setState(this.light.weakLightState);
    }
}

// 弱光狀態
class WeakLightState {
    constructor(light) {
        this.light = light;
    }

    buttonWasClicked() {
        console.log('切換到強光模式');
        this.light.setState(this.light.strongLightState);
    }
}

// 強光狀態
class StrongLightState {
    constructor(light) {
        this.light = light;
    }

    buttonWasClicked() {
        console.log('關燈');
        this.light.setState(this.light.offLightState);
    }
}
複製代碼

接着咱們改寫 Light 類,在內部經過curState記錄當前狀態

class Light {
    constructor() {
        this.offLightState = new OffLightState(this);
        this.weakLightState = new WeakLightState(this);
        this.strongLightState = new StrongLightState(this);
        this.button = null;
    }

    init() {
        let button = document.createElement('button');
        let self = this;
        button.innerHTML = '我是開關';
        this.button = document.body.appendChild(button);
        this.curState = this.offLightState;
        this.button.onclick = () => {
            self.curState.buttonWasClicked();
        }
    }

    setState(state) {
        this.curState = state;
    }
}
複製代碼

以後實例化對象後,咱們在頁面中查看

let light = new Light();
light.init();
複製代碼

uDGh2d.gif

總結

  • 狀態模式經過定義不一樣的狀態類,根據狀態的改變而改變對象的行爲。
  • 沒必要把大量的邏輯都寫在被操做的對象的類中,很容易增長新的狀態。
  • 符合開放-封閉原則。
相關文章
相關標籤/搜索