js怎麼做出撤銷重做功能
這次帶給大家js怎麼做出撤銷重做功能,js做出撤銷重做功能的注意事項有哪些,下面就是實戰案例,一起來看一下。
瀏覽器的功能越來越強大,許多原來由其他客戶端提供的功能漸漸轉移到了前端,前端應用也越來越複雜。許多前端應用,尤其是一些線上編輯軟體,運行時需要不斷處理用戶的交互,提供了撤消重做功能來確保交互的流暢性。不過為一個應用程式實作撤銷重做功能並不是一件容易的事。 Redux官方文件中 介紹如何在 redux 應用程式中實作撤銷重做功能。基於redux 的撤銷功能是一個自頂向下的方案:引入redux-undo
之後所有的操作都變成了「可撤銷的」,然後我們不斷修改其配置使得撤銷功能變得越來越好用(這也是
redux-undo
有這麼多配置項目 的原因)。
本文將採用自底向上的思路,以一個簡易的線上畫圖工具為例子,使用TypeScript 、 Immutable.js 實作一個實用的「撤銷重做」功能。大致效果如下圖所示:
第一步:決定哪些狀態需要歷史記錄,建立自訂的State 類別
#並非所有的狀態都需要歷史記錄。許多狀態是非常瑣碎的,尤其是一些與滑鼠或鍵盤互動相關的狀態,例如在畫圖工具中拖曳一個圖形時我們需要設定一個「正在進行拖曳」的標記,頁面會根據該標記顯示對應的拖曳提示,顯然該拖曳標記不應該出現在歷史記錄中;而另一些狀態無法被撤銷或是不需要被撤銷,例如網頁視窗大小,向後台發送過的請求清單等。
排除那些不需要歷史記錄的狀態,我們將剩下的狀態用Immutable Record 封裝起來,並定義State 類別:
// State.ts import { Record, List, Set } from 'immutable' const StateRecord = Record({ items: List<Item> transform: d3.ZoomTransform selection: number }) // 用类封装,便于书写 TypeScript,注意这里最好使用Immutable 4.0 以上的版本 export default class State extends StateRecord {}
這裡我們的例子是一個簡易的線上畫圖工具,所以上面的State 類別中包含了三個字段,items 用來記錄已經繪製的圖形,transform 用來記錄畫板的平移和縮放狀態,selection 則表示目前選取的圖形的ID。而畫圖工具中的其他狀態,例如圖形繪製預覽,自動對齊配置,操作提示文字等,則沒有放在 State 類別中。
第二步:定義Action 基類,並為每種不同的操作建立對應的Action 子類
與redux-undo 不同的是,我們仍然採用指令模式 :定義基底類Action,所有對State 的運算都被封裝為一個Action 的實例;定義若干Action 的子類,對應於不同類型的動作。
在 TypeScript 中,Action 基底類別用 Abstract Class 定義比較方便。
// actions/index.ts export default abstract class Action { abstract next(state: State): State abstract prev(state: State): State prepare(appHistory: AppHistory): AppHistory { return appHistory } getMessage() { return this.constructor.name } }
Action 物件的 next 方法用來計算「下一個狀態」,prev 方法用來計算「上一個狀態」。 getMessage 方法用來取得 Action 物件的簡短描述。透過 getMessage 方法,我們可以將使用者的操作記錄顯示在頁面上,讓使用者更方便地了解最近發生了什麼。 prepare 方法用來在 Action 第一次被應用之前,使其「準備好」,AppHistory 的定義在本文後面會給出。
Action 子類舉例
下面的 AddItemAction 是一個典型的 Action 子類,用於表達「新增一個新的圖形」。
// actions/AddItemAction.ts export default class AddItemAction extends Action { newItem: Item prevSelection: number constructor(newItem: Item) { super() this.newItem = newItem } prepare(history: AppHistory) { // 创建新的图形后会自动选中该图形,为了使得撤销该操作时 state.selection 变为原来的值 // prepare 方法中读取了「添加图形之前 selection 的值」并保存到 this.prevSelection this.prevSelection = history.state.selection return history } next(state: State) { return state .setIn(['items', this.newItem.id], this.newItem) .set('selection', this.newItemId) } prev(state: State) { return state .deleteIn(['items', this.newItem.id]) .set('selection', this.prevSelection) } getMessage() { return `Add item ${this.newItem.id}` } }
執行階段行為
應用程式執行時,使用者互動產生一個Action 流,每次產生Action 物件時,我們呼叫該對象的next 方法來計算後一個狀態,然後將該action 儲存到一個清單中以備後用;使用者進行撤銷操作時,我們從action 清單中取出最近一個Action 並呼叫其prev 方法。當應用程式執行時,next/prev 方法被呼叫的情況大致如下:
// initState 是一开始就给定的应用初始状态 // 某一时刻,用户交互产生了 action1 ... state1 = action1.next(initState) // 又一个时刻,用户交互产生了 action2 ... state2 = action2.next(state1) // 同样的,action3也出现了 ... state3 = action3.next(state2) // 用户进行撤销,此时我们需要调用最近一个action的prev方法 state4 = action3.prev(state3) // 如果再次进行撤销,我们从action列表中取出对应的action,调用其prev方法 state5 = action2.prev(state4) // 重做的时候,取出最近一个被撤销的action,调用其next方法 state6 = action2.next(state5) Applied-Action
為了方便後面的說明,我們對Applied-Action 進行一個簡單的定義:Applied-Action 是指那些操作結果已經反映在目前應用狀態中的action;當action 的next 方法執行時,該action 變成applied;當prev 方法被執行時,該action 變成unapplied。
第三個步驟:建立歷史記錄容器 AppHistory
前面的 State 类用于表示某个时刻应用的状态,接下来我们定义 AppHistory 类用来表示应用的历史记录。同样的,我们仍然使用 Immutable Record 来定义历史记录。其中 state 字段用来表达当前的应用状态,list 字段用来存放所有的 action,而 index 字段用来记录最近的 applied-action 的下标。应用的历史状态可以通过 undo/redo 方法计算得到。apply 方法用来向 AppHistory 中添加并执行具体的 Action。具体代码如下:
// AppHistory.ts const emptyAction = Symbol('empty-action') export const undo = Symbol('undo') export type undo = typeof undo // TypeScript2.7之后对symbol的支持大大增强 export const redo = Symbol('redo') export type redo = typeof redo const AppHistoryRecord = Record({ // 当前应用状态 state: new State(), // action 列表 list: List<Action>(), // index 表示最后一个applied-action在list中的下标。-1 表示没有任何applied-action index: -1, }) export default class AppHistory extends AppHistoryRecord { pop() { // 移除最后一项操作记录 return this .update('list', list => list.splice(this.index, 1)) .update('index', x => x - 1) } getLastAction() { return this.index === -1 ? emptyAction : this.list.get(this.index) } getNextAction() { return this.list.get(this.index + 1, emptyAction) } apply(action: Action) { if (action === emptyAction) return this return this.merge({ list: this.list.setSize(this.index + 1).push(action), index: this.index + 1, state: action.next(this.state), }) } redo() { const action = this.getNextAction() if (action === emptyAction) return this return this.merge({ list: this.list, index: this.index + 1, state: action.next(this.state), }) } undo() { const action = this.getLastAction() if (action === emptyAction) return this return this.merge({ list: this.list, index: this.index - 1, state: action.prev(this.state), }) } }
第四步:添加「撤销重做」功能
假设应用中的其他代码已经将网页上的交互转换为了一系列的 Action 对象,那么给应用添上「撤销重做」功能的大致代码如下:
type HybridAction = undo | redo | Action // 如果用Redux来管理状态,那么使用下面的reudcer来管理那些「需要历史记录的状态」 // 然后将该reducer放在应用状态树中合适的位置 function reducer(history: AppHistory, action: HybridAction): AppHistory { if (action === undo) { return history.undo() } else if (action === redo) { return history.redo() } else { // 常规的 Action // 注意这里需要调用prepare方法,好让该action「准备好」 return action.prepare(history).apply(action) } } // 如果是在 Stream/Observable 的环境下,那么像下面这样使用 reducer const action$: Stream<HybridAction> = generatedFromUserInteraction const appHistory$: Stream<AppHistory> = action$.fold(reducer, new AppHistory()) const state$ = appHistory$.map(h => h.state) // 如果是用回调函数的话,大概像这样使用reducer onActionHappen = function (action: HybridAction) { const nextHistory = reducer(getLastHistory(), action) updateAppHistory(nextHistory) updateState(nextHistory.state) }
第五步:合并 Action,完善用户交互体验
通过上面这四个步骤,画图工具拥有了撤消重做功能,但是该功能用户体验并不好。在画图工具中拖动一个图形时,MoveItemAction 的产生频率和 mousemove 事件的发生频率相同,如果我们不对该情况进行处理,MoveItemAction 马上会污染整个历史记录。我们需要合并那些频率过高的 action,使得每个被记录下来的 action 有合理的撤销粒度。
每个 Action 在被应用之前,其 prepare 方法都会被调用,我们可以在 prepare 方法中对历史记录进行修改。例如,对于 MoveItemAction,我们判断上一个 action 是否和当前 action 属于同一次移动操作,然后来决定在应用当前 action 之前是否移除上一个 action。代码如下:
// actions/MoveItemAction.ts export default class MoveItemAction extends Action { prevItem: Item // 一次图形拖动操作可以由以下三个变量来进行描述: // 拖动开始时鼠标的位置(startPos),拖动过程中鼠标的位置(movingPos),以及拖动的图形的 ID constructor(readonly startPos: Point, readonly movingPos: Point, readonly itemId: number) { // 上一行中 readonly startPos: Point 相当于下面两步: // 1. 在MoveItemAction中定义startPos只读字段 // 2. 在构造函数中执行 this.startPos = startPos super() } prepare(history: AppHistory) { const lastAction = history.getLastAction() if (lastAction instanceof MoveItemAction && lastAction.startPos == this.startPos) { // 如果上一个action也是MoveItemAction,且拖动操作的鼠标起点和当前action相同 // 则我们认为这两个action在同一次移动操作中 this.prevItem = lastAction.prevItem return history.pop() // 调用pop方法来移除最近一个action } else { // 记录图形被移动之前的状态,用于撤销 this.prevItem = history.state.items.get(this.itemId) return history } } next(state: State): State { const dx = this.movingPos.x - this.startPos.x const dy = this.movingPos.y - this.startPos.y const moved = this.prevItem.move(dx, dy) return state.setIn(['items', this.itemId], moved) } prev(state: State) { // 撤销的时候我们直接使用已经保存的prevItem即可 return state.setIn(['items', this.itemId], this.prevItem) } getMessage() { /* ... */ } }
从上面的代码中可以看到,prepare 方法除了使 action 自身准备好之外,它还可以让历史记录准备好。不同的 Action 类型有不同的合并规则,为每种 Action 实现合理的 prepare 函数之后,撤消重做功能的用户体验能够大大提升。
一些其他需要注意的地方
撤销重做功能是非常依赖于不可变性的,一个 Action 对象在放入 AppHistory.list 之后,其所引用的对象都应该是不可变的。如果 action 所引用的对象发生了变化,那么在后续撤销时可能发生错误。本方案中,为了方便记录操作发生时的一些必要信息,Action 对象的 prepare 方法中允许出现原地修改操作,但是 prepare 方法只会在 action 被放入历史记录之前调用一次,action 一旦进入纪录列表就是不可变的了。
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
以上是js怎麼做出撤銷重做功能的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

豆包app裡會有很多ai創作的功能,那麼豆包app有什麼功能呢?使用者可以透過這個軟體來創作繪畫,和ai進行聊天,也能夠為用戶生成文章,幫助大家搜尋歌曲等。這篇豆包app功能介紹就能夠告訴大家具體的操作方法,以下就是具體內容,趕快看看吧!豆包app有什麼功能答:可以畫畫、聊天、寫文、找歌。功能介紹:1、問題查詢:可以透過ai來更快的找到問題的答案,什麼樣的問題都是可以詢問。 2.圖片生成:可以有ai來為大家創造不同的圖片,只需要告訴大家大概的要求。 3.ai聊天:能夠為用戶創建一個可以聊天的ai,

vivox100s和x100手機都是vivo手機產品線中的代表機型,它們分別代表了vivo在不同時間段內的高端技術水平,因此這兩款手機在設計、性能和功能上均有一定區別。本文將從效能比較和功能解析兩個面向對這兩款手機進行詳細比較,幫助消費者更好地選擇適合自己的手機。首先,我們來看vivox100s和x100在效能上的比較。 vivox100s搭載了最新的

隨著網路的快速發展,自媒體這個概念已經深入人心。那麼,自媒體到底是什麼呢?它有哪些主要特點和功能呢?接下來,我們將一一探討這些問題。一、自媒體到底是什麼?自媒體,顧名思義,就是自己就是媒體。它是指透過網路平台,個人或團隊可以自主創建、編輯、發布和傳播內容的資訊載體。不同於傳統媒體,如報紙、電視、電台等,自媒體具有更強的互動性和個人化,讓每個人都能成為訊息的生產者和傳播者。二、自媒體的主要特色和功能有哪些? 1.低門檻:自媒體的崛起降低了進入媒體產業的門檻,不再需要繁瑣的設備和專業的團隊,一部手

隨著小紅書在年輕人中的流行,越來越多的人開始利用這個平台分享各方面的經驗和生活見解。如何有效管理多個小紅書帳號成為關鍵問題。在本文中,我們將討論一些小紅書帳號管理軟體的功能,並探討如何更好地經營小紅書帳號。隨著社群媒體的發展,許多人發現自己需要管理多個社群帳號。對於小紅書用戶來說,這也是一個挑戰。一些小紅書帳號管理軟體可以幫助使用者更輕鬆地管理多個帳號,包括自動發佈內容、定時發布、資料分析等功能。透過這些工具,使用者可以更有效率地管理他們的帳號,提高帳號的曝光率和關注。另一、小紅書帳號管理軟體有

PHP技巧:快速實現回到上一頁功能在網頁開發中,常常會遇到需要實作返回上一頁的功能。這樣的操作可以提高使用者體驗,讓使用者更方便地在網頁之間進行導航。在PHP中,我們可以透過一些簡單的程式碼來實現這項功能。本文將介紹如何快速實現返回上一頁功能,並提供具體的PHP程式碼範例。在PHP中,我們可以使用$_SERVER['HTTP_REFERER']來取得上一頁的URL

《探索Discuz:定義、功能及程式碼範例》隨著網路的快速發展,社群論壇已成為人們獲取資訊、交流觀點的重要平台。在眾多的社群論壇系統中,Discuz作為國內較知名的一種開源論壇軟體,備受廣大網站開發者和管理員的青睞。那麼,什麼是Discuz?它又有哪些功能,能為我們的網站提供怎樣的幫助呢?本文將對Discuz進行詳細介紹,並附上具體的程式碼範例,幫助讀者更

Linux下GDM的功能和作用詳解在Linux作業系統中,GDM(GNOMEDisplayManager)是一種圖形化登入管理器,它提供了使用者在系統中登入和登出的介面。 GDM通常是GNOME桌面環境的一部分,但也可以被其他桌面環境所使用。 GDM的功能不僅是提供一個登入介面,還包括使用者會話管理、螢幕保護、自動登入等功能。 GDM的功能主要包括以下幾個面向:

PHP是一種廣泛應用於Web開發的伺服器端腳本語言,它主要的功能是產生動態網頁內容,與HTML結合使用,可以創造出豐富多彩的網頁。 PHP的功能強大,它可以執行各種資料庫操作、檔案操作、表單處理等任務,為網站提供強大的互動性和功能性。在接下來的文章中,我們將進一步探究PHP的作用與功能,並配以詳細的程式碼範例。首先,我們來看看PHP的常見用途:動態網頁生成:P
