摘要:實(shí)現(xiàn)代碼大致如下回退重做操作普通操作,棧記錄,棧清空撤回操作重做操作數(shù)據(jù)的訂閱數(shù)據(jù)是以鍵值對(duì)存儲(chǔ)的,相應(yīng)地,訂閱的時(shí)候也以鍵名為準(zhǔn)。
網(wǎng)頁是用戶與網(wǎng)站對(duì)接的入口,當(dāng)我們?cè)试S用戶在網(wǎng)頁上進(jìn)行一些頻繁的操作時(shí),對(duì)用戶而言,誤刪、誤操作是一件令人抓狂的事情,“如果時(shí)光可以倒流,這一切可以重來……”。
當(dāng)然,時(shí)光不能倒流,而數(shù)據(jù)是可以恢復(fù)的,比如采用 redux(https://redux.js.org/) 來管理頁面狀態(tài),就可以很愉快地實(shí)現(xiàn)撤銷與重做,但是傲嬌的我婉拒了redux的加持,手撕出一個(gè) Javascript 狀態(tài)管理工具,鑒于是私有構(gòu)造函數(shù),怎么命名不重要,就叫他李狗蛋好了,英文名就叫 —— DataSet。
DataSet并不是被設(shè)計(jì)來存儲(chǔ)大量數(shù)據(jù)的,因此采用鍵值對(duì)的方式存儲(chǔ)也不會(huì)有任何問題,甚至連 W3C 支持的 IndexdDB 都懶得用,直接以對(duì)象存在內(nèi)存中即可,遂有:
// 存儲(chǔ)具體數(shù)據(jù)的容器 this.dataBase = {};
另外,撤回與重做依賴于歷史數(shù)據(jù),因此有必要將每次改動(dòng)的數(shù)據(jù)存儲(chǔ)起來,在撤回/重做的時(shí)候按照先進(jìn)后出的規(guī)則取出,為此定義了兩個(gè)數(shù)組——撤回棧和重做棧,默認(rèn)可以往后回退100步,當(dāng)然,步長(zhǎng)可以傳入的參數(shù) undoSize 自定義:
// 撤回與重做棧 this.undoStack = new Array(options.undoSize || 100); this.redoStack = new Array(options.undoSize || 100);
當(dāng)然,一開始為了開發(fā)方便,有時(shí)候需要查詢數(shù)據(jù)操作歷史,因此還開辟了日志存儲(chǔ)的空間,但是目前這些日志貌似沒有派上過用場(chǎng),還白白占用內(nèi)存拖慢速度,有機(jī)會(huì)得把它移除掉。
2. 數(shù)據(jù)隔離我們知道,Javascipt 變量實(shí)際上只是對(duì)內(nèi)存引用的一個(gè)句柄,因此當(dāng)你把對(duì)象“存”起來之后,在外部對(duì)該對(duì)象的改動(dòng)仍舊是會(huì)影響存儲(chǔ)的數(shù)據(jù)的,因此多數(shù)情況下需要對(duì)存入的對(duì)象進(jìn)行深拷貝,由于需要保存的對(duì)象通常只是用來描述狀態(tài),因此不應(yīng)包含方法,所以是可以轉(zhuǎn)為符串再存儲(chǔ)的,取用數(shù)據(jù)的時(shí)候再把它轉(zhuǎn)為對(duì)象即可,所以數(shù)據(jù)的出入分別采用了 JSON.stringify 和JSON.parse 方法。
存數(shù)據(jù):
this.dataBase[key].value = this.immutable && JSON.stringify(this.dataBase[key].value) || this.dataBase[key].value;
取數(shù)據(jù):
var result= (!this.mutable) && JSON.parse(dataBase["" + key].value) || dataBase["" + key].value;
鑒于部分情況下數(shù)據(jù)可以不進(jìn)行隔離,比如存儲(chǔ)AJAX獲取到的數(shù)據(jù),為此我預(yù)留了 immutable 參數(shù),這個(gè)值為真的時(shí)候存取數(shù)據(jù)不需要經(jīng)過字符串的轉(zhuǎn)換,有助于提高運(yùn)行效率。
3. 撤回、重做棧管理前面已經(jīng)說了棧實(shí)現(xiàn)的中心思想——先進(jìn)后出,因此數(shù)據(jù)發(fā)生變化的時(shí)候,視情況對(duì)兩個(gè)數(shù)組進(jìn)行操作,采用數(shù)組的 push 方法存入,用 pop 方法取出即可,每次操作前后執(zhí)行一下數(shù)組的 shift 或者 unshift方法,來保證數(shù)組長(zhǎng)度的穩(wěn)定(畢竟這個(gè)棧是假的)。實(shí)現(xiàn)代碼大致如下:
// 回退/重做操作 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. 數(shù)據(jù)的訂閱
數(shù)據(jù)是以鍵值對(duì)存儲(chǔ)的,相應(yīng)地,訂閱的時(shí)候也以鍵名為準(zhǔn)。由于接觸過的諸多代碼都濫用了 jQuery 的 .on 方法,我決定自己實(shí)現(xiàn)的所有訂閱都必須是唯一的,因此這里的每個(gè)鍵名也只能訂閱一次。訂閱的接口如下:
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; };
這樣就把回調(diào)函數(shù)與鍵名綁定了,對(duì)應(yīng)數(shù)據(jù)發(fā)生改變的時(shí)候,即執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù):
... 數(shù)據(jù)發(fā)生了改動(dòng) // 如果該data被設(shè)置訂閱,執(zhí)行訂閱回調(diào)函數(shù) var subscribe = dataBase[key].subscribe; (!BETA_silence) && (subscribe instanceof Function) && (subscribe(newData, ver));
你可能注意到了這里有個(gè) BETA_silence 參數(shù)。這是為了方法復(fù)用而預(yù)留的參數(shù),適用于數(shù)據(jù)已在外部修改的情形,只需在內(nèi)部同步一下數(shù)據(jù)即可,觸發(fā)訂閱可能引起bug,此時(shí)將 silence 設(shè)為true即可。不過我認(rèn)為應(yīng)當(dāng)盡量減少方法內(nèi)部的判斷,因此 silence 添加了 BETA_ 前綴,提醒自己有時(shí)間的話還是另增一個(gè)專門的方法。
以上基本概括 DataSet 的設(shè)計(jì)思想,剩下的就是更加具體的實(shí)現(xiàn)和接口的設(shè)計(jì),就不再細(xì)說,下面貼出完整代碼,實(shí)現(xiàn)有些倉(cāng)促,歡迎批評(píng)與指正。
代碼:
/** * @constructor DataSet 數(shù)據(jù)集管理 * @description 對(duì)數(shù)據(jù)的所有修改歷史進(jìn)行記錄,提供撤回、重做等功能 * @description 內(nèi)部采用 JSON.stringify 和 JSON.parse對(duì)對(duì)象進(jìn)行引用隔離,因此存在性能問題,不適用于大規(guī)模的數(shù)據(jù)存儲(chǔ) * */ function DataSet(param){ return this._init(param); } !function(){ "use strict"" /** * @method 初始化 * @param {Object} options 配置項(xiàng) * @return {Null} * */ DataSet.prototype._init = function init(options) { try{ // 存儲(chǔ)具體數(shù)據(jù)的容器 this.dataBase = {}; // 日志存儲(chǔ) 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; // 初始化的時(shí)候可以傳入原始值 if(options.data){ this.setData(options.data); } } catch(err) { this.log = [ { action: "initial", data: "error:" + err, success: false }, ] // 操作日志 } return this; }; /** * @method 設(shè)置數(shù)據(jù) * @param {Object|JSON} data 數(shù)據(jù)必須以鍵值對(duì)格式傳入,數(shù)據(jù)只能是純粹的Object或Array,不能有循環(huán)引用、不能有方法和Symbol * @param {Number|*} [undoFlag] 用來標(biāo)識(shí)對(duì)歷史棧的更改, 1-undo 2-redo 0|undefined-just 默認(rèn)不進(jìn)行棧操作 * @param {Boolean} [BETA_silence] 靜默更新,即不觸發(fā)訂閱事件,該方法不夠安全,慎用 * @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; // 撤回時(shí)版本號(hào)減一,否則加一 var ver = thisData.version + ((undoFlag !== 1) && 1 || -1); dataBase[key].value = newData; dataBase[key].version = ver; // 如果該data被設(shè)置訂閱,執(zhí)行訂閱回調(diào)函數(shù) 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 獲取數(shù)據(jù) * @param {String|Array} param * @return {Object|*} 返回?cái)?shù)據(jù)依原始數(shù)據(jù)而定 * */ DataSet.prototype.getData = function getData(param) { try{ var dataBase = this.dataBase; /** * @function 獲取單個(gè)數(shù)據(jù) * */ 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中是否有某個(gè)鍵 * @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 訂閱數(shù)據(jù) * @description 注意每個(gè)key只能被訂閱一次,多次訂閱將只有最后一次生效 * @param {String} key * @param {Function} callback 在訂閱的值發(fā)生變化的時(shí)候執(zhí)行,參數(shù)為所訂閱的值 * @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; }();
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/103250.html
摘要:會(huì)用其它人的分析結(jié)果,并付諸實(shí)踐,更偏向于執(zhí)行,通過錯(cuò)誤來學(xué)習(xí)。四語言學(xué)習(xí)的方法有些人可能通過感受和觀察就能很好的學(xué)習(xí)了,比如我們所熟知的一些學(xué)霸。 小推廣講堂《60分鐘徒手?jǐn)]出Spring框架》,別只會(huì)用,干脆自己擼一個(gè)輪子吧 一 前言 1984年, 大衛(wèi)·庫(kù)伯曾在他的著作《體驗(yàn)學(xué)習(xí):體驗(yàn)——學(xué)習(xí)發(fā)展的源泉》提出了學(xué)習(xí)圈理論,與他認(rèn)為經(jīng)驗(yàn)學(xué)習(xí)過程是由四個(gè)適應(yīng)性學(xué)習(xí)階段構(gòu)成的環(huán)形結(jié)構(gòu),...
摘要:系統(tǒng)需要支持命令的撤銷。第步計(jì)算斷路器的健康度會(huì)將成功失敗拒絕超時(shí)等信息報(bào)告給斷路器,斷路器會(huì)維護(hù)一組計(jì)數(shù)器來統(tǒng)計(jì)這些數(shù)據(jù)。第步,當(dāng)前命令的線程池請(qǐng)求隊(duì)列或者信號(hào)量被占滿的時(shí)候。 斷路由器模式 在分布式架構(gòu)中,當(dāng)某個(gè)服務(wù)單元發(fā)生故障之后,通過斷路由器的故障監(jiān)控(類似熔斷保險(xiǎn)絲),向調(diào)用方返回一個(gè)錯(cuò)誤響應(yīng),而不是長(zhǎng)時(shí)間的等待。這樣就不會(huì)使得線程因調(diào)用故障服務(wù)被長(zhǎng)時(shí)間占用不釋放,避免了故障...
摘要:要求通過要求數(shù)據(jù)變更函數(shù)使用裝飾或放在函數(shù)中,目的就是讓狀態(tài)的變更根據(jù)可預(yù)測(cè)性單向數(shù)據(jù)流。同一份數(shù)據(jù)需要響應(yīng)到多個(gè)視圖,且被多個(gè)視圖進(jìn)行變更需要維護(hù)全局狀態(tài),并在他們變動(dòng)時(shí)響應(yīng)到視圖數(shù)據(jù)流變得復(fù)雜,組件本身已經(jīng)無法駕馭。今天是 520,這是本系列最后一篇文章,主要涵蓋 React 狀態(tài)管理的相關(guān)方案。 前幾篇文章在掘金首發(fā)基本石沉大海, 沒什么閱讀量. 可能是文章篇幅太長(zhǎng)了?掘金值太低了? ...
摘要:,命令模式,將行為請(qǐng)求者和行為實(shí)現(xiàn)者解耦,將行為抽象為對(duì)象。解釋器模式,迭代器模式,將集合對(duì)象的存儲(chǔ)數(shù)據(jù)和遍歷數(shù)據(jù)職責(zé)分離。即將遍歷的責(zé)任交給迭代器返回的迭代器,迭代器。 設(shè)計(jì)模式總結(jié) 創(chuàng)建型:除了直接new來實(shí)例化對(duì)象外,提供了多種隱藏創(chuàng)建邏輯的生成對(duì)象的方法 結(jié)構(gòu)型:通過對(duì)象和類的組合,得到新的結(jié)構(gòu)和功能 行為型:解決對(duì)象之間的通行和功能職責(zé)分配 詳細(xì)分類 工廠 簡(jiǎn)單工廠...
閱讀 2658·2021-11-25 09:43
閱讀 2759·2021-11-04 16:09
閱讀 1695·2021-10-12 10:13
閱讀 907·2021-09-29 09:35
閱讀 908·2021-08-03 14:03
閱讀 1798·2019-08-30 15:55
閱讀 3023·2019-08-28 18:14
閱讀 3603·2019-08-26 13:43