摘要:實現代碼大致如下回退重做操作普通操作,棧記錄,棧清空撤回操作重做操作數據的訂閱數據是以鍵值對存儲的,相應地,訂閱的時候也以鍵名為準。
網頁是用戶與網站對接的入口,當我們允許用戶在網頁上進行一些頻繁的操作時,對用戶而言,誤刪、誤操作是一件令人抓狂的事情,“如果時光可以倒流,這一切可以重來……”。
當然,時光不能倒流,而數據是可以恢復的,比如采用 redux(https://redux.js.org/) 來管理頁面狀態,就可以很愉快地實現撤銷與重做,但是傲嬌的我婉拒了redux的加持,手撕出一個 Javascript 狀態管理工具,鑒于是私有構造函數,怎么命名不重要,就叫他李狗蛋好了,英文名就叫 —— DataSet。
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; }();
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103250.html
摘要:會用其它人的分析結果,并付諸實踐,更偏向于執行,通過錯誤來學習。四語言學習的方法有些人可能通過感受和觀察就能很好的學習了,比如我們所熟知的一些學霸。 小推廣講堂《60分鐘徒手擼出Spring框架》,別只會用,干脆自己擼一個輪子吧 一 前言 1984年, 大衛·庫伯曾在他的著作《體驗學習:體驗——學習發展的源泉》提出了學習圈理論,與他認為經驗學習過程是由四個適應性學習階段構成的環形結構,...
摘要:系統需要支持命令的撤銷。第步計算斷路器的健康度會將成功失敗拒絕超時等信息報告給斷路器,斷路器會維護一組計數器來統計這些數據。第步,當前命令的線程池請求隊列或者信號量被占滿的時候。 斷路由器模式 在分布式架構中,當某個服務單元發生故障之后,通過斷路由器的故障監控(類似熔斷保險絲),向調用方返回一個錯誤響應,而不是長時間的等待。這樣就不會使得線程因調用故障服務被長時間占用不釋放,避免了故障...
摘要:要求通過要求數據變更函數使用裝飾或放在函數中,目的就是讓狀態的變更根據可預測性單向數據流。同一份數據需要響應到多個視圖,且被多個視圖進行變更需要維護全局狀態,并在他們變動時響應到視圖數據流變得復雜,組件本身已經無法駕馭。今天是 520,這是本系列最后一篇文章,主要涵蓋 React 狀態管理的相關方案。 前幾篇文章在掘金首發基本石沉大海, 沒什么閱讀量. 可能是文章篇幅太長了?掘金值太低了? ...
閱讀 2621·2021-11-25 09:43
閱讀 2725·2021-11-04 16:09
閱讀 1634·2021-10-12 10:13
閱讀 881·2021-09-29 09:35
閱讀 880·2021-08-03 14:03
閱讀 1777·2019-08-30 15:55
閱讀 2989·2019-08-28 18:14
閱讀 3489·2019-08-26 13:43