国产99久久精品_欧美日本韩国一区二区_激情小说综合网_欧美一级二级视频_午夜av电影_日本久久精品视频

最新文章專題視頻專題問答1問答10問答100問答1000問答2000關鍵字專題1關鍵字專題50關鍵字專題500關鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關鍵字專題關鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
當前位置: 首頁 - 科技 - 知識百科 - 正文

js怎么做出撤銷重做功能

來源:懂視網 責編:小采 時間:2020-11-27 19:59:19
文檔

js怎么做出撤銷重做功能

js怎么做出撤銷重做功能:這次給大家帶來js怎么做出撤銷重做功能,js做出撤銷重做功能的注意事項有哪些,下面就是實戰案例,一起來看一下。瀏覽器的功能越來越強大,許多原來由其他客戶端提供的功能漸漸轉移到了前端,前端應用也越來越復雜。許多前端應用,尤其是一些在線編輯軟件,運
推薦度:
導讀js怎么做出撤銷重做功能:這次給大家帶來js怎么做出撤銷重做功能,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 一旦進入紀錄列表就是不可變的了。

相信看了本文案例你已經掌握了方法,更多精彩請關注Gxl網其它相關文章!

推薦閱讀:

上傳圖片時本地先預覽如何實現

JS實現todolist詳解

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

文檔

js怎么做出撤銷重做功能

js怎么做出撤銷重做功能:這次給大家帶來js怎么做出撤銷重做功能,js做出撤銷重做功能的注意事項有哪些,下面就是實戰案例,一起來看一下。瀏覽器的功能越來越強大,許多原來由其他客戶端提供的功能漸漸轉移到了前端,前端應用也越來越復雜。許多前端應用,尤其是一些在線編輯軟件,運
推薦度:
  • 熱門焦點

最新推薦

猜你喜歡

熱門推薦

專題
Top
主站蜘蛛池模板: 亚洲欧美日韩激情在线观看 | 亚洲精美视频 | 国产精品视频久久久 | 国产日韩高清一区二区三区 | 精品久久久久久综合日本 | 日韩精品亚洲电影天堂 | 精品国产自在在线在线观看 | 欧美色综合图区 | 97久久精品午夜一区二区 | 欧美在线日韩在线 | 欧美aⅴ在线 | 日韩欧美一区二区三区在线 | 久久久久777777人人人视频 | 久久精品成人一区二区三区 | 欧美精品v日韩精品v国产精品 | 午夜视频网 | 国产精品一区欧美日韩制服 | 国产在线精品一区二区夜色 | 九九久久香港经典三级精品 | 亚洲欧美日韩高清一区二区一 | 欧美亚洲日本国产 | 日韩极品视频 | 91精品久久久久久久久网影视 | 欧美 日韩 国产 成人 在线观看 | 日韩免费观看 | 福利视频欧美一区二区三区 | 日韩欧美中文在线 | 九九51精品国产免费看 | 日韩专区第一页 | 国产精品系列在线一区 | 一级毛片免费视频观看 | 国产精品伦视频观看免费 | 国产精品视频第一页 | 性久久久久久久 | 91.久久| 久久精品视频一区二区三区 | 在线国产视频 | 九九精品99久久久香蕉 | 亚洲欧美日韩高清综合678 | 欧美色图亚洲天堂 | 久久久久久久99久久久毒国产 |