Vuex和Redux都使用的Flux設計模式精簡版實現

殷榮檜發表於2018-12-11

作者:殷榮檜@騰訊

目錄:

1. 為什麼需要Flux設計模式

2. Flux設計模式是怎麼實現的

本文對應Github地址,如果覺得文章還可以,希望您送上寶貴的Star

  先來看一下最終結果,免得你覺得太複雜,跑了。怎麼樣就一個新增,一個刪除,是不是很簡單,那就繼續往下看吧。

Vuex和Redux都使用的Flux設計模式精簡版實現

  老外搞個新東西就喜歡給其取個Cool的名字,什麼Flux,Redux,Meteor。本來英語就不是太好的中國人一看就跑路了,What?老子Javascirpt還沒學好,你又來這這些。名字都看不懂,還學啥。紛紛感慨:老子學不動了,不要再更新了。其實吧,老外就是把公司的個人檔案管理流程應用到了前端資料管理流程中了,然後取了嚇跑了很多人的名字Flux而已,不信我給你實現一個簡單版的Flux架構(程式碼中對照個人檔案管理流程講解Flux流程),結合下面這張圖你再看看是不是。

Vuex和Redux都使用的Flux設計模式精簡版實現

1.為什麼需要Flux設計模式

(1)刀耕火種的年代

  最初因為前端就是幾個頁面和幾個js檔案,前端對於網站中的全域性變數(全域性變數就是在哪個頁面都可以被訪問和修改的變數,如網站的標題)基本上用

 window.title = 'XXX'
複製程式碼

  就可以了。剛上手專案,你全域性搜一下window這個關鍵字也差不多對專案中的全域性變數都有個印象了。然後有一天,線上網站中標題出錯了(比如:‘愛牛網’ 變成了 ‘屠牛網’),老闆一看嚇的不輕,讓你趕緊看看。你就全域性搜一下window.title看一下,原來在D頁面title被一個新來的員工改錯了(新員工在測試環境下改程式碼改著玩的,忘刪了)。然後你把它修改過來就可以了。

(2)MVC時代

  前端發展到中期,應用的場景開始增多,需要一個框架來降低程式碼的耦合了,Backbone類似的MVC框架出現。通過各個Model來存放資料,包括全域性的資料。這個時候全域性變數可以通過View(頁面)來修改Model中的資料,再反饋到其他的View中去,做到資料同步。這個時期基本上是做一個部落格網站,管理系統的體量。

(3) 前端大爆炸時代

  前端發展到現在,所應用的場景就越來越多了。有個同學小扎(馬克·祖克伯)在大學裡想做個叫Facebook的社交應用,就讓前端的朋友幫忙做一做頁面。前端同學很快用MVC模式搞定了Facebook前端架構。但是令小扎同學沒想到的是,這個叫Facebook的應用火了,功能不停的迭代,增加。前端到同學表示每天加班加點累到只能看著自己手中小扎剛發的1000萬美刀的股票罵罵娘。這個時候的前端體量急劇膨脹,上百個View(頁面)出現,對應上百個Model(存放前端資料的專用層),經常會出現多個頁面對同一個Model資料進行操作,比如有個全域性資料剛新增到Model,又來一個頁面通知這個全域性變數要刪除,這個時候其他頁面還不知道這個全域性變數已經刪除了,表示要修改這個資料,再來個頁面又要獲取這個資料,這個時候就導致前端的資料管理異常的混亂,任何一個頁面都有權操作全域性變數。你能想象一個公司的員工檔案資訊中心(類似全域性變數)如何任何員工(頁面)都可以過來隨意的增刪改查,你信不信沒過幾天,你再去檔案庫檢視你的檔案,你的學歷會變成幼兒園。這個檔案資訊中心的資料將會一塌糊塗。這就是Facebook有一斷時間總是會出現你有未讀訊息提示,但是你點進去檢視卻沒有任何訊息的問題。小扎同學因為這個問題被無數的美國網友罵 What the hell.小扎那叫個火大啊,把前端的主管叫過來一頓痛罵(把美國網友送給他的What the hell都送給了這位主管),讓其一個星期內解決此問題(以上罵娘故事純屬虛構,)。這個時候Flux就出現了。這就是為什麼需要Flux設計模式。

2.Flux設計模式是怎麼實現的

  我們直接上程式碼,在程式碼中做了非常詳細的比喻,再對照文章開始的圖,相信你一定能看懂。

(1) Action實現部分

/**
 *(2)Action人事辦事(分設:增加檔案辦事視窗,刪除檔案辦事視窗...)大廳生成器
 *公司員工的檔案資訊屬於重要資訊,非HR工作人員不
 *能夠直接找檔案資訊中心的人私自修改自己或他人檔案,
 *需要到人事大廳視窗辦理,由人事大廳交給人事主管(dispatcher)
 *再由HR人事辦事大廳
 */
let Action = function Action() {
    return {
        create: function(initData) { // 新建員工辦事視窗
            // 辦事大廳登記此項事物後由人事主管(dispatcher)通知手下注冊過的員工去幹(staff_1好像就會幹)
            dispatcher.dispatch({ type: 'create', item: initData });
        },
        add: function(item) { // 新增員工辦事視窗
            // 辦事大廳登記此項事物後由人事主管(dispatcher)通知手下注冊過的員工去幹(staff_1好像就會幹)
            dispatcher.dispatch({ type: 'add', item });
        },
        remove: function(item) { // 刪除員工辦事視窗
            // 辦事大廳登記此項事物後由人事主管(dispatcher)通知手下注冊過的員工去幹(staff_1好像就會幹)
            dispatcher.dispatch({ type: 'remove', item });
        }
    }
};
複製程式碼

(2)Dispatcher 實現部分

/**
 * (1)主管生成器
 * 生成各個部門的主管(各種主管,包括hr主管,人事檔案主管等)
 */
let Dispatcher = function Dispatcher() {
    let _cid = 0; // callbackId
    let _callbacks = [];
    return {
        register: function(callback) {
            _callbacks[_cid] = callback;
            return _cid++;
        },
        unregister: function(_cid) {
            // 從回撥陣列中刪除當前的一項
            _callbacks.splice(_cid, _cid);
        },
        dispatch: function(payload) {
            // 通知所有註冊的使用者執行回撥方法
            _callbacks.forEach(callback => callback(payload));
        }
    }
};	
複製程式碼

(3)Store實現部分

/**
 * (3)檔案資訊中心生成器
 * 所有員工的資訊都存放在檔案資訊中心
 */
let Store = function Store() {
    let _itemList = []; // 員工檔案資訊存放櫃
    let _emit = new Flux.Dispatcher(); // 生成檔案資訊中心主管(姓名_emit)
    // 一個小員工名為staff_1到人事主管dispatcher這注冊,這樣dispatcher主管就有一個員工啦
    dispatcher.register(function staff_1(payload) {
        // 以下是這個小員工的簡歷,表明他可以幹增加,刪除,更新等hr員工該乾的活
        switch (payload.type) {
            case 'create': // 建立初始員工(創始人那一批,類似於阿里巴巴的18羅漢之類的)
                _itemList = [...payload.item];
                break;
            case 'add': // 新增一個新的員工檔案
                _itemList.push(payload.item);
                break;
            case 'remove': // 刪除一個離職員工的檔案
                _itemList = _itemList.filter(item => item.id != payload.item.id);
                break;
            default: // 其他操作
                break;
        }
        _emit.dispatch(); // 檔案資訊中心主管發出通知,讓手下注冊的員工們開始幹活了
    });

    return {
        // 獲取員工所有的檔案資訊
        getList: function() {
            return _itemList;
        },
        // 有新的員工到檔案資訊中心主管(_emit)這注冊報到,這樣就有人給主管幹活啦
        addEmiter: function(callback) {
            return _emit.register(callback);
        },
        // 乾的不爽,從檔案資訊中心主管這離職了
        removeEmiter: function(callbackId) {
            _emit.unregister(callbackId);
        }
    }
}
複製程式碼

(4)最後為頁面中的實現部分

// ==========以下為辦事大廳螢幕顯示情況控制==============
var store = new Flux.Store(); // 生成一個名為store的檔案資訊中心
var action = new Flux.Action(); // 生成一個名為action的人事辦事大廳
// 初始化載入的備忘錄列表
var initStaffs = [
    { id: 0, name: '馬雲' },
    { id: 1, name: '王健林' },
    { id: 2, name: '褚時健' },
];

// 重新把檔案資訊中心的人事檔案渲染到辦事大廳大螢幕上
var reRender = function reRender() {
    appEle = document.querySelector('#app');
    ulEle = document.querySelector('#list');
    let allEleArr = [];
    this.store.getList().forEach((item) => {
        allEleArr.push(`<li>${item.name}  <span id="${item.id}" class="remove" onclick="remove(${item.id})">刪除</span></li>`);
    });
    let allStr = allEleArr.join('');
    ulEle.innerHTML = allStr;
}

var remove = function remove(id) {
    // 整理全離職員工的資訊
    let delItem = '';
    this.store.getList().forEach(item => {
        if (item.id === id) {
            delItem = item;
        }
    });
    // 到人事大廳“刪除”辦事視窗辦理
    this.action.remove(delItem);
}
var add = function add(event) {
    // 自己整理新員工的檔案資訊
    let itemValue = document.querySelector('#thingInput').value;
    let currentList = this.store.getList();
    let lastId = currentList.length ? currentList[currentList.length - 1].id : 0;
    let item = {
            id: lastId + 1,
            name: itemValue
        }
        // 整理好了到人事大廳“新增”辦事視窗辦理新增員工業務
    this.action.add(item);
}

setTimeout(() => {
    // 將初始員工的資訊到人事大廳新建辦事視窗辦理,以便存放到檔案資訊中心的檔案櫃中
    action.create(initStaffs);
    //  檔案資訊中心主管這來了一個新員工註冊了,他的本領都在 reRender 函式中說明了
    store.addEmiter(reRender);
    // 程式碼載入完畢後,點亮辦事大廳的所有員工檔案資訊展示大螢幕
    reRender();
}, 0);
複製程式碼

把所有的程式碼結合起來也就是下面這樣:

=====================js.js檔案=====================
/**
 * author: JackieYin
 * time: 2018-12-8 16:35
 * title: Flux設計模式的簡單實現
 * goal: flux設計的初衷是讓你取資料好取,但要改變
 * 資料需要經過flux的一系列操作才行,這樣可
 * 以保障資料不會被隨意篡改
 */

// Flux物件中存放Flux架構
let Flux = function() {
    /**
     * (1)主管生成器
     * 生成各個部門的主管(各種主管,包括hr主管,人事檔案主管等)
     */
    let Dispatcher = function Dispatcher() {
        let _cid = 0; // callbackId
        let _callbacks = [];
        return {
            register: function(callback) {
                _callbacks[_cid] = callback;
                return _cid++;
            },
            unregister: function(_cid) {
                // 從回撥陣列中刪除當前的一項
                _callbacks.splice(_cid, _cid);
            },
            dispatch: function(payload) {
                // 通知所有註冊的使用者執行回撥方法
                _callbacks.forEach(callback => callback(payload));
            }
        }
    };
    var dispatcher = new Dispatcher(); // // 生成一個HR人事主管(主管姓名:dipatcher)(主管員工的增刪改查)
    /**
     *(2)Action人事辦事(分設:增加檔案辦事視窗,刪除檔案辦事視窗...)大廳生成器
     *公司員工的檔案資訊屬於重要資訊,非HR工作人員不
     *能夠直接找檔案資訊中心的人私自修改自己或他人檔案,
     *需要到人事大廳視窗辦理,由人事大廳交給人事主管(dispatcher)
     *再由HR人事辦事大廳
     */
    let Action = function Action() {
        return {
            create: function(initData) { // 新建員工辦事視窗
                // 辦事大廳登記此項事物後由人事主管(dispatcher)通知手下注冊過的員工去幹(staff_1好像就會幹)
                dispatcher.dispatch({ type: 'create', item: initData });
            },
            add: function(item) { // 新增員工辦事視窗
                // 辦事大廳登記此項事物後由人事主管(dispatcher)通知手下注冊過的員工去幹(staff_1好像就會幹)
                dispatcher.dispatch({ type: 'add', item });
            },
            remove: function(item) { // 刪除員工辦事視窗
                // 辦事大廳登記此項事物後由人事主管(dispatcher)通知手下注冊過的員工去幹(staff_1好像就會幹)
                dispatcher.dispatch({ type: 'remove', item });
            }
        }
    };
    /**
     * (3)檔案資訊中心生成器
     * 所有員工的資訊都存放在檔案資訊中心
     */
    let Store = function Store() {
        let _itemList = []; // 員工檔案資訊存放櫃
        let _emit = new Flux.Dispatcher(); // 生成檔案資訊中心主管(姓名_emit)
        // 一個小員工名為staff_1到人事主管dispatcher這注冊,這樣dispatcher主管就有一個員工啦
        dispatcher.register(function staff_1(payload) {
            // 以下是這個小員工的簡歷,表明他可以幹增加,刪除,更新等hr員工該乾的活
            switch (payload.type) {
                case 'create': // 建立初始員工(創始人那一批,類似於阿里巴巴的18羅漢之類的)
                    _itemList = [...payload.item];
                    break;
                case 'add': // 新增一個新的員工檔案
                    _itemList.push(payload.item);
                    break;
                case 'remove': // 刪除一個離職員工的檔案
                    _itemList = _itemList.filter(item => item.id != payload.item.id);
                    break;
                default: // 其他操作
                    break;
            }
            _emit.dispatch(); // 檔案資訊中心主管發出通知,讓手下注冊的員工們開始幹活了
        });

        return {
            // 獲取員工所有的檔案資訊
            getList: function() {
                return _itemList;
            },
            // 有新的員工到檔案資訊中心主管(_emit)這注冊報到,這樣就有人給主管幹活啦
            addEmiter: function(callback) {
                return _emit.register(callback);
            },
            // 乾的不爽,從檔案資訊中心主管這離職了
            removeEmiter: function(callbackId) {
                _emit.unregister(callbackId);
            }
        }
    }
    return {
        Dispatcher,
        Action,
        Store,
    }
}();

// ==========以下為辦事大廳螢幕顯示情況控制==============
var store = new Flux.Store(); // 生成一個名為store的檔案資訊中心
var action = new Flux.Action(); // 生成一個名為action的人事辦事大廳
// 初始化載入的備忘錄列表
var initStaffs = [
    { id: 0, name: '馬雲' },
    { id: 1, name: '王健林' },
    { id: 2, name: '褚時健' },
];

// 重新把檔案資訊中心的人事檔案渲染到辦事大廳大螢幕上
var reRender = function reRender() {
    appEle = document.querySelector('#app');
    ulEle = document.querySelector('#list');
    let allEleArr = [];
    this.store.getList().forEach((item) => {
        allEleArr.push(`<li>${item.name}  <span id="${item.id}" class="remove" onclick="remove(${item.id})">刪除</span></li>`);
    });
    let allStr = allEleArr.join('');
    ulEle.innerHTML = allStr;
}

var remove = function remove(id) {
    // 整理全離職員工的資訊
    let delItem = '';
    this.store.getList().forEach(item => {
        if (item.id === id) {
            delItem = item;
        }
    });
    // 到人事大廳“刪除”辦事視窗辦理
    this.action.remove(delItem);
}
var add = function add(event) {
    // 自己整理新員工的檔案資訊
    let itemValue = document.querySelector('#thingInput').value;
    let currentList = this.store.getList();
    let lastId = currentList.length ? currentList[currentList.length - 1].id : 0;
    let item = {
            id: lastId + 1,
            name: itemValue
        }
        // 整理好了到人事大廳“新增”辦事視窗辦理新增員工業務
    this.action.add(item);
}

setTimeout(() => {
    // 將初始員工的資訊到人事大廳新建辦事視窗辦理,以便存放到檔案資訊中心的檔案櫃中
    action.create(initStaffs);
    //  檔案資訊中心主管這來了一個新員工註冊了,他的本領都在 reRender 函式中說明了
    store.addEmiter(reRender);
    // 程式碼載入完畢後,點亮辦事大廳的所有員工檔案資訊展示大螢幕
    reRender();
}, 0);

==================index.html 檔案==================
<!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">
    <link rel="stylesheet" href="./css.css">
    <title>Document</title>
</head>
<body>
    <header id="title">
        <h1>Flux樣例演示系統</h1>
    </header>
    <div id="app">
        <input type="text" id="thingInput" placeholder="請輸入新的員工名稱"><span onclick="add()" class="add">新增</span>
        <ul id="list"></ul>
    </div>
</body>
<script src="./js.js"></script>

</html>
複製程式碼

  最後我還是牆裂建議你把我的程式碼clone下來,直接開啟其中的index.html就能執行。然後再仔細看看,如果發現有寫的不對的地方反手就是一個Issue提給我。最後還是那就不要臉的話,如果你覺的講的還可以,Please送上你寶貴的Star。   
   部門正在招新,為騰訊企業產品部,隸屬CSGI事業群。福利不少,薪水很高,就等你來。有興趣請猛戳下方兩個連結。       www.lagou.com/jobs/521039… www.zhipin.com/job_detail/…

參考文章:

Flux架構小白入門筆記

Flux from Scratch

Dissection of Flux architecture or how to write your own

A cartoon guide to Flux

相關文章