徒手撸出Javascript 状况管理工具 DataSet ,完成数据的定阅、查询、打消和恢复

网页是用户与网站对接的进口,当我们许可用户在网页上举行一些频仍的操纵时,对用户而言,误删、误操纵是一件使人抓狂的事变,“假如时间能够倒流,这一切能够重来……”。
固然,时间不能倒流,而数据是能够恢复的,比方采纳 redux(https://redux.js.org/) 来治理页面状况,就能够很愉快地完成打消与重做,然则傲娇的我婉拒了redux的加持,手撕出一个 Javascript 状况治理工具,鉴因而私有组织函数,怎样定名不重要,就叫他李狗蛋好了,英文名就叫 —— DataSet。

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要领,来保证数组长度的稳固(毕竟这个栈是假的)。完成代码大抵以下:

                    // 回退/重做操纵
                    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 要领,我决议本身完成的一切定阅都必需是唯一的,因而这里的每一个键名也只能定阅一次。定阅的接口以下:

        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;
            };
            

如许就把回调函数与键名绑定了,对应数据发作转变的时刻,即实行对应的回调函数:

            ... 数据发作了修正
            // 假如该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;
        }();
    原文作者:madRain
    原文地址: https://segmentfault.com/a/1190000018769683
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞