JavaScript設計模式與開發實踐 - 觀察者模式

来源:http://www.cnblogs.com/laixiangran/archive/2016/06/26/5618376.html
-Advertisement-
Play Games

概述 觀察者模式又叫發佈 訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個目標對象(為了方便理解,以下將觀察者對象叫做訂閱者,將目標對象叫做發佈者)。發佈者的狀態發生變化時就會通知所有的訂閱者,使得它們能夠自動更新自己。 觀察者模式的使用場合就 ...


概述

觀察者模式又叫發佈 - 訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個目標對象(為了方便理解,以下將觀察者對象叫做訂閱者,將目標對象叫做發佈者)。發佈者的狀態發生變化時就會通知所有的訂閱者,使得它們能夠自動更新自己。

觀察者模式的使用場合就是:當一個對象的改變需要同時改變其它對象,並且它不知道具體有多少對象需要改變的時候,就應該考慮使用觀察者模式。

觀察者模式的中心思想就是促進鬆散耦合,一為時間上的解耦,二為對象之間的解耦。讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響到另一邊的變化。

實現

(function (window, undefined) {
    var _subscribe = null,
        _publish = null,
        _unsubscribe = null,
        _shift = Array.prototype.shift, // 刪除數組的第一個 元素,並返回這個元素
        _unshift = Array.prototype.unshift, // 在數組的開頭添加一個或者多個元素,並返回數組新的length值
        namespaceCache = {},
        _create = null,
        each = function (ary, fn) {
            var ret = null;
            for (var i = 0, len = ary.length; i < len; i++) {
                var n = ary[i];
                ret = fn.call(n, i, n);
            }
            return ret;
        };

    // 訂閱消息
    _subscribe = function (key, fn, cache) {
        if (!cache[key]) {
            cache[key] = [];
        }
        cache[key].push(fn);
    };

    // 取消訂閱(取消全部或者指定消息)
    _unsubscribe = function (key, cache, fn) {
        if (cache[key]) {
            if (fn) {
                for (var i = cache[key].length; i >= 0; i--) {
                    if (cache[key][i] === fn) {
                        cache[key].splice(i, 1);
                    }
                }
            } else {
                cache[key] = [];
            }
        }
    };

    // 發佈消息
    _publish = function () {
        var cache = _shift.call(arguments),
            key = _shift.call(arguments),
            args = arguments,
            _self = this,
            ret = null,
            stack = cache[key];

        if (!stack || !stack.length) {
            return;
        }

        return each(stack, function () {
            return this.apply(_self, args);
        });
    };

    // 創建命名空間
    _create = function (namespace) {
        var namespace = namespace || "default";
        var cache = {},
            offlineStack = {},    // 離線事件,用於先發佈後訂閱,只執行一次
            ret = {
                subscribe: function (key, fn, last) {
                    _subscribe(key, fn, cache);
                    if (!offlineStack[key]) {
                        offlineStack[key] = null;
                        return;
                    }
                    if (last === "last") { // 指定執行離線隊列的最後一個函數,執行完成之後刪除
                        offlineStack[key].length && offlineStack[key].pop()();  // [].pop => 刪除一個數組中的最後的一個元素,並且返回這個元素
                    } else {
                        each(offlineStack[key], function () {
                            this();
                        });
                    }
                    offlineStack[key] = null;
                },
                one: function (key, fn, last) {
                    _unsubscribe(key, cache);
                    this.subscribe(key, fn, last);
                },
                unsubscribe: function (key, fn) {
                    _unsubscribe(key, cache, fn);
                },
                publish: function () {
                    var fn = null,
                        args = null,
                        key = _shift.call(arguments),
                        _self = this;

                    _unshift.call(arguments, cache, key);
                    args = arguments;
                    fn = function () {
                        return _publish.apply(_self, args);
                    };

                    if (offlineStack && offlineStack[key] === undefined) {
                        offlineStack[key] = [];
                        return offlineStack[key].push(fn);
                    }
                    return fn();
                }
            };

        return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
    };

    window.pubsub = {
        create: _create, // 創建命名空間
        one: function (key, fn, last) { // 訂閱消息,只能單一對象訂閱
            var pubsub = this.create();
            pubsub.one(key, fn, last);
        },
        subscribe: function (key, fn, last) { // 訂閱消息,可多對象同時訂閱
            var pubsub = this.create();
            pubsub.subscribe(key, fn, last);
        },
        unsubscribe: function (key, fn) { // 取消訂閱,(取消全部或指定消息)
            var pubsub = this.create();
            pubsub.unsubscribe(key, fn);
        },
        publish: function () { // 發佈消息
            var pubsub = this.create();
            pubsub.publish.apply(this, arguments);
        }
    };
})(window, undefined);

應用

假如我們正在開發一個商城網站,網站里有header頭部、nav導航、消息列表、購物車等模塊。這幾個模塊的渲染有一個共同的前提條件,就是必須先用ajax非同步請求獲取用戶的登錄信息。

至於ajax請求什麼時候能成功返回用戶信息,這點我們沒有辦法確定。更重要的一點是,我們不知道除了header頭部、nav導航、消息列表、購物車之外,將來還有哪些模塊需要使用這些用戶信息。如果它們和用戶信息模塊產生了強耦合,比如下麵這樣的形式:

login.succ(function (data) {
    header.setAvatar(data.avatar); // 設置header模塊的頭像
    nav.setAvatar(data.avatar); // 設置導航模塊的頭像
    message.refresh(); // 刷新消息列表
    cart.refresh(); // 刷新購物車列表
});

現在登錄模塊是由你負責編寫的,但我們還必須瞭解header模塊里設置頭像的方法叫setAvatar、購物車模塊里刷新的方法叫refresh,這種耦合性會使程式變得僵硬,header模塊不能隨意再改變setAvatar的方法名。這是針對具體實現編程的典型例子,針對具體實現編程是不被贊同的。

等到有一天,項目中又新增了一個收貨地址管理的模塊,這個模塊是由另一個同事所寫的,此時他就必須找到你,讓你登錄之後刷新一下收貨地址列表。於是你又翻開你3個月前寫的登錄模塊,在最後部分加上這行代碼:

login.succ(function (data) {
    header.setAvatar(data.avatar);
    nav.setAvatar(data.avatar);
    message.refresh();
    cart.refresh();
    address.refresh(); // 增加這行代碼
});

我們就會越來越疲於應付這些突如其來的業務要求,不停地重構這些代碼。

用觀察者模式重寫之後,對用戶信息感興趣的業務模塊將自行訂閱登錄成功的消息事件。當登錄成功時,登錄模塊只需要發佈登錄成功的消息,而業務方接受到消息之後,就會開始進行各自的業務處理,登錄模塊並不關心業務方究竟要做什麼,也不想去瞭解它們的內部細節。改善後的代碼如下:

$.ajax('http:// xxx.com?login', function(data) { // 登錄成功
    pubsub.publish('loginSucc', data); // 發佈登錄成功的消息
});

// 各模塊監聽登錄成功的消息:

var header = (function () { // header模塊
    pubsub.subscribe('loginSucc', function(data) {
        header.setAvatar(data.avatar);
    });
    return {
        setAvatar: function(data){
            console.log('設置header模塊的頭像');
        }
    };
})();

var nav = (function () { // nav模塊
    pubsub.subscribe('loginSucc', function(data) {
        nav.setAvatar(data.avatar);
    });
    return {
        setAvatar: function(avatar) {
            console.log('設置nav模塊的頭像');
        }
    };
})();

如上所述,我們隨時可以把setAvatar的方法名改成setTouxiang。如果有一天在登錄完成之後,又增加一個刷新收貨地址列表的行為,那麼只要在收貨地址模塊裡加上監聽消息的方法即可,而這可以讓開發該模塊的同事自己完成,你作為登錄模塊的開發者,永遠不用再關心這些行為了。代碼如下:

var address = (function () { // 地址模塊
    pubsub.subscribe('loginSucc', function(obj) {
        address.refresh(obj);
    });
    return {
        refresh: function(avatar) {
            console.log('刷新收貨地址列表');
        }
    };
})();

優缺點

優點

  1. 支持簡單的廣播通信,自動通知所有已經訂閱過的對象;
  2. 頁面載入後發佈者很容易與訂閱者存在一種動態關聯,增加了靈活性;
  3. 發佈者與訂閱者之間的抽象耦合關係能夠單獨擴展以及重用。

缺點

  1. 創建訂閱者本身要消耗一定的時間和記憶體,而且當你訂閱一個消息後,也許此消息最後都未發生,但這個訂閱者會始終存在於記憶體中;
  2. 雖然可以弱化對象之間的聯繫,但如果過度使用的話,對象和對象之間的必要聯繫也將被深埋在背後,會導致程式難以跟蹤維護和理解。

參考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • RPC(Remote Procedure Call Protocol)遠程過程調用協議,它是一種通過網路,從遠程電腦程式上請求服務,而不必瞭解底層網路技術的協議。說的再直白一點,就是客戶端在不必知道調用細節的前提之下,調用遠程電腦上運行的某個對象,使用起來就像調用本地的對象一樣。目前典型的RPC ...
  • 回到目錄 本文來自於實踐中的不足 在最近開始過程中,遇到了一個問題,之前設計的工作單元UoW只支持Insert,Update,Delete三種操作,即開發人員可以將以上三種操作同時扔進工作單元,由工作單元UoW負責事件的處理,這種設計已經出現很多年了,大叔感覺也是不錯,思路就是在工作單元里添加三個字 ...
  • Vue.js介紹 Vue.js是當下很火的一個JavaScript MVVM庫,它是以數據驅動和組件化的思想構建的。相比於Angular.js,Vue.js提供了更加簡潔、更易於理解的API,使得我們能夠快速地上手並使用Vue.js。 如果你之前已經習慣了用jQuery操作DOM,學習Vue.js時... ...
  • 使用js製作一個簡單的產品放大圖 購物網站的產品頁經常會放有一個產品展示圖區。該圖區有一個功能就是產品圖的放大功能,移動左側的焦點區域,可以放大細節部分觀看,詳情如下圖。實現該功能的方法也非常簡單。 實驗:製作產品焦點放大圖。 所需技能:1、基本的獲取頁面元素的方法; 2、幾個簡單的事件; 3、會使 ...
  • 簡介:jQuery Lightbox圖片放大預覽代碼是一款可以在用戶點擊頁面中的小圖片時,將該圖片的高清版本以Lightbox的方式放大顯示在頁面的中間,提高用戶的體驗度。效果展示 http://hovertree.com/texiao/jqimg/6/效果圖如下: 源碼下載:http://hove ...
  • 1.Ajax:readyState(狀態值)和status(狀態碼)的區別readyState,是指運行AJAX所經歷過的幾種狀態,無論訪問是否成功都將響應的步驟,可以理解成為AJAX運行步驟,使用“ajax.readyState”獲得status,是指無論AJAX訪問是否成功,由HTTP協議根據所 ...
  • 談到閉包,人們常常會把匿名函數和閉包混淆在一起。閉包是指由權訪問另一個函數作用域中的變數的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數,仍以前面的 createComparisonFunction()函數為例 在標識的部分,它訪問了外部的變數 propertyName 即使這個函數被返 ...
  • 第一部分:用CSS實現佈局 讓我們一起來做一個頁面 首先,我們需要一個佈局。 請使用CSS控制3個div,實現如下圖的佈局。 第二部分:用javascript優化佈局 由於我們的用戶群喜歡放大看頁面 於是我們給上一題的佈局做一次優化。 當滑鼠略過某個區塊的時候,該區塊會放大25%, 並且其他的區塊仍 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...