徒手擼出Javascript 狀態管理工具 DataSet ,實現數據的訂閱、查詢、撤銷和恢復

網頁是用戶與網站對接的入口,當咱們容許用戶在網頁上進行一些頻繁的操做時,對用戶而言,誤刪、誤操做是一件使人抓狂的事情,「若是時光能夠倒流,這一切能夠重來……」。
固然,時光不能倒流,而數據是能夠恢復的,好比採用 redux(https://redux.js.org/) 來管理頁面狀態,就能夠很愉快地實現撤銷與重作,可是傲嬌的我婉拒了redux的加持,手撕出一個 Javascript 狀態管理工具,鑑因而私有構造函數,怎麼命名不重要,就叫他李狗蛋好了,英文名就叫 —— DataSet。redux

1. 數據的存儲

DataSet並非被設計來存儲大量數據的,所以採用鍵值對的方式存儲也不會有任何問題,甚至連 W3C 支持的 IndexdDB 都懶得用,直接以對象存在內存中便可,遂有:數組

// 存儲具體數據的容器
                    this.dataBase = {};

另外,撤回與重作依賴於歷史數據,所以有必要將每次改動的數據存儲起來,在撤回/重作的時候按照先進後出的規則取出,爲此定義了兩個數組——撤回棧和重作棧,默承認以日後回退100步,固然,步長能夠傳入的參數 undoSize 自定義:安全

// 撤回與重作棧
                    this.undoStack = new Array(options.undoSize || 100);
                    this.redoStack = new Array(options.undoSize || 100);

固然,一開始爲了開發方便,有時候須要查詢數據操做歷史,所以還開闢了日誌存儲的空間,可是目前這些日誌貌似沒有派上過用場,還白白佔用內存拖慢速度,有機會得把它移除掉。函數

2. 數據隔離

咱們知道,Javascipt 變量實際上只是對內存引用的一個句柄,所以當你把對象「存」起來以後,在外部對該對象的改動仍舊是會影響存儲的數據的,所以多數狀況下須要對存入的對象進行深拷貝,因爲須要保存的對象一般只是用來描述狀態,所以不該包含方法,因此是能夠轉爲符串再存儲的,取用數據的時候再把它轉爲對象便可,因此數據的出入分別採用了 JSON.stringify 和JSON.parse 方法。
存數據:工具

this.dataBase[key].value = this.immutable &&
         JSON.stringify(this.dataBase[key].value) ||
         this.dataBase[key].value;

取數據:性能

var result= (!this.mutable) &&
         JSON.parse(dataBase['' + key].value) || 
         dataBase['' + key].value;

鑑於部分狀況下數據能夠不進行隔離,好比存儲AJAX獲取到的數據,爲此我預留了 immutable 參數,這個值爲真的時候存取數據不須要通過字符串的轉換,有助於提升運行效率。網站

3. 撤回、重作棧管理

前面已經說了棧實現的中心思想——先進後出,所以數據發生變化的時候,視狀況對兩個數組進行操做,採用數組的 push 方法存入,用 pop 方法取出便可,每次操做先後執行一下數組的 shift 或者 unshift方法,來保證數組長度的穩定(畢竟這個棧是假的)。實現代碼大體以下:ui

// 回退/重作操做
                    var undoStack = this.undoStack;
                    var redoStack = this.redoStack;
                    var undoLength = undoStack.length;
                    if(!undoFlag){
                        // 普通操做,undo棧記錄,redo棧清空
                        undoStack.shift();
                        undoStack.push(formerData);
                        if(!!redoStack.length){
                            redoStack.splice(0);
                            redoStack.length = undoLength 
                        }
                    } else if(undoFlag === 1){
                        // 撤回操做
                        redoStack.shift();
                        redoStack.push(formerData);
                    } else {
                        // 重作操做
                        undoStack.shift();
                        undoStack.push(formerData);
                    }

4. 數據的訂閱

數據是以鍵值對存儲的,相應地,訂閱的時候也以鍵名爲準。因爲接觸過的諸多代碼都濫用了 jQuery 的 .on 方法,我決定本身實現的全部訂閱都必須是惟一的,所以這裏的每一個鍵名也只能訂閱一次。訂閱的接口以下:this

function subscribe(key, callback) {
                if(typeof key !== 'string'){
                    console.warn('DataSet.prototype.subscribe: required a "key" as a string.');
                    return null;
                }

                if(callback && callback instanceof Function){
                    try{
                        if(this.hasData(key)){
                            this.dataBase[key].subscribe = callback;
                        } else {
                            var newData = {};
                            newData['' + key] = null;
                            this.setData(newData, false);
                            this.dataBase[key].subscribe = callback;
                        }
                    } catch (err) {

                    }
                }

                return null;
            };

這樣就把回調函數與鍵名綁定了,對應數據發生改變的時候,即執行對應的回調函數:prototype

... 數據發生了改動
            // 若是該data被設置訂閱,執行訂閱回調函數
            var subscribe = dataBase[key].subscribe;
            (!BETA_silence) && (subscribe instanceof Function) && (subscribe(newData, ver));

你可能注意到了這裏有個 BETA_silence 參數。這是爲了方法複用而預留的參數,適用於數據已在外部修改的情形,只需在內部同步一下數據便可,觸發訂閱可能引發bug,此時將 silence 設爲true便可。不過我認爲應當儘可能減小方法內部的判斷,所以 silence 添加了 BETA_ 前綴,提醒本身有時間的話仍是另增一個專門的方法。

以上基本歸納 DataSet 的設計思想,剩下的就是更加具體的實現和接口的設計,就再也不細說,下面貼出完整代碼,實現有些倉促,歡迎批評與指正。
代碼:

/**
         * @constructor DataSet 數據集管理
         * @description 對數據的全部修改歷史進行記錄,提供撤回、重作等功能
         * @description 內部採用 JSON.stringify 和 JSON.parse對對象進行引用隔離,所以存在性能問題,不適用於大規模的數據存儲
         * */
        function DataSet(param){
            return this._init(param);
        }

        !function(){
            'use strict''
            /**
             * @method 初始化
             * @param {Object} options 配置項
             * @return {Null}
             * */
            DataSet.prototype._init = function init(options) {
                try{
                    // 存儲具體數據的容器
                    this.dataBase = {};

                    // 日誌存儲
                    this.log = [
                        {
                            action: 'initial',
                            data: JSON.stringify(options).substr(137) + '...',
                            success: true
                        },
                    ];

                    // 撤回與重作棧
                    this.undoStack = new Array(options.undoSize || 100);
                    this.redoStack = new Array(options.undoSize || 100);

                    this.mutable = !!options.mutable;

                    // 初始化的時候能夠傳入原始值
                    if(options.data){
                        this.setData(options.data);
                    }
                } catch(err) {
                    this.log = [
                        {
                            action: 'initial',
                            data: 'error:' + err,
                            success: false
                        },
                    ]  // 操做日誌
                }
                return this;
            };

            /**
             * @method 設置數據
             * @param {Object|JSON} data 數據必須以鍵值對格式傳入,數據只能是純粹的Object或Array,不能有循環引用、不能有方法和Symbol
             * @param {Number|*} [undoFlag] 用來標識對歷史棧的更改, 1-undo 2-redo 0|undefined-just 默認不進行棧操做
             * @param {Boolean} [BETA_silence] 靜默更新,即不觸發訂閱事件,該方法不夠安全,慎用
             * @return {Boolean} 以示成敗
             * */
            DataSet.prototype.setData = function setData(data, undoFlag, BETA_silence) {
                // try{
                    var val = null;
                    try {
                        val = JSON.stringify(data);
                    }catch(err) {
                        console.error('DataSet.prototype.setData: the data cannot be parsed to JSON string!');
                        return false;
                    }
                    var dataBase = this.dataBase;
                    var formerData = {};
                    for(var handle in data) {
                        var key = '' + handle;
                        var immutable = !this.mutable;
                        // 保存到撤回/重作棧
                        var thisData = dataBase[key];
                        var newData = immutable && JSON.parse(JSON.stringify(data[key])) || data[key];
                        if(this.dataBase[key]){
                            formerData[key] = immutable &&
                             JSON.parse(JSON.stringify(this.dataBase[key].value)) ||
                              this.dataBase[key].value;
                              
                            // 撤回時版本號減一,不然加一
                            var ver = thisData.version + ((undoFlag !== 1) && 1 || -1);  
                            dataBase[key].value = newData;
                            dataBase[key].version = ver;

                            // 若是該data被設置訂閱,執行訂閱回調函數
                            var subscribe = dataBase[key].subscribe;
                            (!BETA_silence) &&
                            (subscribe instanceof Function) &&
                            (subscribe(newData, ver));
                        } else {
                            this.dataBase[key] = {
                                origin: newData,
                                version: 0,
                                value: newData,
                            }
                        }
                    }

                    // 回退操做
                    var undoStack = this.undoStack;
                    var redoStack = this.redoStack;
                    var undoLength = undoStack.length;
                    if(!undoFlag){
                        // 普通操做,undo棧記錄,redo棧清空
                        undoStack.shift();
                        undoStack.push(formerData);
                        if(!!redoStack.length){
                            redoStack.splice(0);
                            redoStack.length = undoLength;
                        }
                    } else if(undoFlag === 1){
                        // 撤回操做
                        redoStack.shift();
                        redoStack.push(formerData);
                    } else {
                        // 重作操做
                        undoStack.shift();
                        undoStack.push(formerData);
                    }

                    // 記錄操做日誌
                    this.log.push({
                        action: 'setData',
                        data: val.substr(137) + '...',
                        success: true
                    });

                    return true;
                // } catch (err){
                //     // 記錄失敗日誌
                //     this.log.push({
                //         action: 'setData',
                //         data: 'error:' + err,
                //         success: false
                //     });
                //
                //     throw new Error(err);
                // }
            };

            /**
             * @method 獲取數據
             * @param {String|Array} param
             * @return {Object|*} 返回數據依原始數據而定
             * */
            DataSet.prototype.getData = function getData(param) {
                try{
                    var dataBase = this.dataBase;

                    /**
                     * @function 獲取單個數據
                     * */
                    var getItem = function getItem(key) {
                        var data = undefined;

                        try{
                            data = (!this.mutable) && 
                                JSON.parse(JSON.stringify(dataBase['' + key].value)) ||
                                dataBase['' + key].value;
                        } catch(err){
                        }

                        return data;
                    };

                    var result = [];

                    if(/string|number/.test(typeof param)){
                        result = getItem(param);
                    } else if(param instanceof Array){
                        result = [];
                        for(var cnt = 0; cnt < param.length; cnt++) {
                            if(/string|number/.test(typeof param[cnt])) {
                                result.push(getItem(param[cnt]))
                            }else {
                                console.error('DataSet.prototype.getData: requires param(s) ,which typeof string|Number');
                            }
                        }
                    } else {
                        console.error('DataSet.prototype.getData: requires param(s) ,which typeof string|Number');
                    }

                    this.log.push({
                        action: 'getData',
                        data: JSON.stringify(result || []).substr(137) + '...',
                        success: true
                    });

                    return result;
                } catch(err) {
                    this.log.push({
                        action: 'getData',
                        data: 'error:' + err,
                        success: false
                    });
                    console.error(err);

                    return false;
                }
            };

            /**
             * @method 判斷DataSet中是否有某個鍵
             * @param {String} key
             * @return {Boolean}
             * */
            DataSet.prototype.hasData = function hasData(key) {
                return this.dataBase.hasOwnProperty(key);
            };

            /**
             * @method 撤回操做
             * */
            DataSet.prototype.undo = function undo() {
                var self = this;
                var undoStack = self.undoStack;

                // 獲取上一次的操做
                var curActive = undoStack.pop();
                undoStack.unshift(null);

                // 撤回生效
                if(curActive){
                    self.setData(curActive, 1);
                    return true;
                }
                return null;
            };

            /**
             * @method 重作操做
             * */
            DataSet.prototype.redo = function redo() {
                var self = this;
                var redoStack = self.redoStack;
                redoStack.unshift(null);
                var curActive = redoStack.pop();

                // 重作生效
                if(curActive){
                    this.setData(curActive, 2);
                    return true;
                }
                return null;
            };

            /**
             * @method 訂閱數據
             * @description 注意每一個key只能被訂閱一次,屢次訂閱將只有最後一次生效
             * @param {String} key
             * @param {Function} callback 在訂閱的值發生變化的時候執行,參數爲所訂閱的值
             * @return {Null}
             * */
            DataSet.prototype.subscribe = function subscribe(key, callback) {
                if(typeof key !== 'string'){
                    console.warn('DataSet.prototype.subscribe: required a "key" as a string.');
                    return null;
                }

                if(callback && callback instanceof Function){
                    try{
                        if(this.hasData(key)){
                            this.dataBase[key].subscribe = callback;
                        } else {
                            var newData = JSON.parse('{"' + key + '":null}');
                            this.setData(newData, false);
                            this.dataBase[key].subscribe = callback;
                        }
                    } catch (err) {

                    }
                }

                return null;
            };
            
            return null;
        }();
相關文章
相關標籤/搜索