欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于 Immutable.js 實(shí)現(xiàn)撤銷重做功能的實(shí)例代碼

 更新時(shí)間:2018年03月01日 10:09:48   投稿:mrr  
這篇文章主要介紹了基于 Immutable.js 實(shí)現(xiàn)撤銷重做功能及一些需要注意的地方,需要的朋友可以參考下

瀏覽器的功能越來(lái)越強(qiáng)大,許多原來(lái)由其他客戶端提供的功能漸漸轉(zhuǎn)移到了前端,前端應(yīng)用也越來(lái)越復(fù)雜。許多前端應(yīng)用,尤其是一些在線編輯軟件,運(yùn)行時(shí)需要不斷處理用戶的交互,提供了撤消重做功能來(lái)保證交互的流暢性。不過為一個(gè)應(yīng)用實(shí)現(xiàn)撤銷重做功能并不是一件容易的事情。 Redux官方文檔中 介紹了如何在 redux 應(yīng)用中實(shí)現(xiàn)撤銷重做功能?;?redux 的撤銷功能是一個(gè)自頂向下的方案:引入 redux-undo 之后所有的操作都變?yōu)榱恕缚沙蜂N的」,然后我們不斷修改其配置使得撤銷功能變得越來(lái)越好用(這也是 redux-undo 有那么多配置項(xiàng) 的原因)。

本文將采用自底向上的思路,以一個(gè)簡(jiǎn)易的在線畫圖工具為例子,使用TypeScript Immutable.js 實(shí)現(xiàn)一個(gè)實(shí)用的「撤消重做」功能。大致效果如下圖所示:

第一步:確定哪些狀態(tài)需要?dú)v史記錄,創(chuàng)建自定義的 State 類

并非所有的狀態(tài)都需要?dú)v史記錄。許多狀態(tài)是非?,嵥榈?,尤其是一些與鼠標(biāo)或者鍵盤交互相關(guān)的狀態(tài),例如在畫圖工具中拖拽一個(gè)圖形時(shí)我們需要設(shè)置一個(gè)「正在進(jìn)行拖拽」的標(biāo)記,頁(yè)面會(huì)根據(jù)該標(biāo)記顯示對(duì)應(yīng)的拖拽提示,顯然該拖拽標(biāo)記不應(yīng)該出現(xiàn)在歷史記錄中;而另一些狀態(tài)無(wú)法被撤銷或是不需要被撤銷,例如網(wǎng)頁(yè)窗口大小,向后臺(tái)發(fā)送過的請(qǐng)求列表等。

排除那些不需要?dú)v史記錄的狀態(tài),我們將剩下的狀態(tài)用 Immutable Record 封裝起來(lái),并定義 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 {}

這里我們的例子是一個(gè)簡(jiǎn)易的在線畫圖工具,所以上面的 State 類中包含了三個(gè)字段,items 用來(lái)記錄已經(jīng)繪制的圖形,transform 用來(lái)記錄畫板的平移和縮放狀態(tài),selection 則表示目前選中的圖形的 ID。而畫圖工具中的其他狀態(tài),例如圖形繪制預(yù)覽,自動(dòng)對(duì)齊配置,操作提示文本等,則沒有放在 State 類中。

第二步:定義 Action 基類,并為每種不同的操作創(chuàng)建對(duì)應(yīng)的 Action 子類

與 redux-undo 不同的是,我們?nèi)匀徊捎?命令模式 :定義基類 Action,所有對(duì) State 的操作都被封裝為一個(gè) Action 的實(shí)例;定義若干 Action 的子類,對(duì)應(yīng)于不同類型的操作。

在 TypeScript 中,Action 基類用 Abstract Class 來(lái)定義比較方便。

// 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 對(duì)象的 next 方法用來(lái)計(jì)算「下一個(gè)狀態(tài)」,prev 方法用來(lái)計(jì)算「上一個(gè)狀態(tài)」。getMessage 方法用來(lái)獲取 Action 對(duì)象的簡(jiǎn)短描述。通過 getMessage 方法,我們可以將用戶的操作記錄顯示在頁(yè)面上,讓用戶更方便地了解最近發(fā)生了什么。prepare 方法用來(lái)在 Action 第一次被應(yīng)用之前,使其「準(zhǔn)備好」,AppHistory 的定義在本文后面會(huì)給出。

Action 子類舉例

下面的 AddItemAction 是一個(gè)典型的 Action 子類,用于表達(dá)「添加一個(gè)新的圖形」。

// actions/AddItemAction.ts
export default class AddItemAction extends Action {
 newItem: Item
 prevSelection: number
 constructor(newItem: Item) {
 super()
 this.newItem = newItem
 }
 prepare(history: AppHistory) {
 // 創(chuàng)建新的圖形后會(huì)自動(dòng)選中該圖形,為了使得撤銷該操作時(shí) state.selection 變?yōu)樵瓉?lái)的值
 // 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}` }
}

運(yùn)行時(shí)行為

應(yīng)用運(yùn)行時(shí),用戶交互產(chǎn)生一個(gè) Action 流,每次產(chǎn)生 Action 對(duì)象時(shí),我們調(diào)用該對(duì)象的 next 方法來(lái)計(jì)算后一個(gè)狀態(tài),然后將該 action 保存到一個(gè)列表中以備后用;用戶進(jìn)行撤銷操作時(shí),我們從 action 列表中取出最近一個(gè) Action 并調(diào)用其 prev 方法。應(yīng)用運(yùn)行時(shí),next/prev 方法被調(diào)用的情況大致如下:

// initState 是一開始就給定的應(yīng)用初始狀態(tài)
// 某一時(shí)刻,用戶交互產(chǎn)生了 action1 ...
state1 = action1.next(initState)
// 又一個(gè)時(shí)刻,用戶交互產(chǎn)生了 action2 ...
state2 = action2.next(state1)
// 同樣的,action3也出現(xiàn)了 ...
state3 = action3.next(state2)
// 用戶進(jìn)行撤銷,此時(shí)我們需要調(diào)用最近一個(gè)action的prev方法
state4 = action3.prev(state3)
// 如果再次進(jìn)行撤銷,我們從action列表中取出對(duì)應(yīng)的action,調(diào)用其prev方法
state5 = action2.prev(state4)
// 重做的時(shí)候,取出最近一個(gè)被撤銷的action,調(diào)用其next方法
state6 = action2.next(state5)
Applied-Action

為了方便后面的說明,我們對(duì) Applied-Action 進(jìn)行一個(gè)簡(jiǎn)單的定義:Applied-Action 是指那些操作結(jié)果已經(jīng)反映在當(dāng)前應(yīng)用狀態(tài)中的 action;當(dāng) action 的 next 方法執(zhí)行時(shí),該 action 變?yōu)?applied;當(dāng) prev 方法被執(zhí)行時(shí),該 action 變?yōu)?unapplied。

第三步:創(chuàng)建歷史記錄容器 AppHistory

前面的 State 類用于表示某個(gè)時(shí)刻應(yīng)用的狀態(tài),接下來(lái)我們定義 AppHistory 類用來(lái)表示應(yīng)用的歷史記錄。同樣的,我們?nèi)匀皇褂?Immutable Record 來(lái)定義歷史記錄。其中 state 字段用來(lái)表達(dá)當(dāng)前的應(yīng)用狀態(tài),list 字段用來(lái)存放所有的 action,而 index 字段用來(lái)記錄最近的 applied-action 的下標(biāo)。應(yīng)用的歷史狀態(tài)可以通過 undo/redo 方法計(jì)算得到。apply 方法用來(lái)向 AppHistory 中添加并執(zhí)行具體的 Action。具體代碼如下:

// AppHistory.ts
const emptyAction = Symbol('empty-action')
export const undo = Symbol('undo')
export type undo = typeof undo // TypeScript2.7之后對(duì)symbol的支持大大增強(qiáng)
export const redo = Symbol('redo')
export type redo = typeof redo
const AppHistoryRecord = Record({
 // 當(dāng)前應(yīng)用狀態(tài)
 state: new State(),
 // action 列表
 list: List<Action>(),
 // index 表示最后一個(gè)applied-action在list中的下標(biāo)。-1 表示沒有任何applied-action
 index: -1,
})
export default class AppHistory extends AppHistoryRecord {
 pop() { // 移除最后一項(xiàng)操作記錄
 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),
 })
 }
}

第四步:添加「撤銷重做」功能

假設(shè)應(yīng)用中的其他代碼已經(jīng)將網(wǎng)頁(yè)上的交互轉(zhuǎn)換為了一系列的 Action 對(duì)象,那么給應(yīng)用添上「撤銷重做」功能的大致代碼如下:

type HybridAction = undo | redo | Action
// 如果用Redux來(lái)管理狀態(tài),那么使用下面的reudcer來(lái)管理那些「需要?dú)v史記錄的狀態(tài)」
// 然后將該reducer放在應(yīng)用狀態(tài)樹中合適的位置
function reducer(history: AppHistory, action: HybridAction): AppHistory {
 if (action === undo) {
 return history.undo()
 } else if (action === redo) {
 return history.redo()
 } else { // 常規(guī)的 Action
 // 注意這里需要調(diào)用prepare方法,好讓該action「準(zhǔn)備好」
 return action.prepare(history).apply(action)
 }
}
// 如果是在 Stream/Observable 的環(huán)境下,那么像下面這樣使用 reducer
const action$: Stream<HybridAction> = generatedFromUserInteraction
const appHistory$: Stream<AppHistory> = action$.fold(reducer, new AppHistory())
const state$ = appHistory$.map(h => h.state)
// 如果是用回調(diào)函數(shù)的話,大概像這樣使用reducer
onActionHappen = function (action: HybridAction) {
 const nextHistory = reducer(getLastHistory(), action)
 updateAppHistory(nextHistory)
 updateState(nextHistory.state)
}

第五步:合并 Action,完善用戶交互體驗(yàn)

通過上面這四個(gè)步驟,畫圖工具擁有了撤消重做功能,但是該功能用戶體驗(yàn)并不好。在畫圖工具中拖動(dòng)一個(gè)圖形時(shí),MoveItemAction 的產(chǎn)生頻率和 mousemove 事件的發(fā)生頻率相同,如果我們不對(duì)該情況進(jìn)行處理,MoveItemAction 馬上會(huì)污染整個(gè)歷史記錄。我們需要合并那些頻率過高的 action,使得每個(gè)被記錄下來(lái)的 action 有合理的撤銷粒度。

每個(gè) Action 在被應(yīng)用之前,其 prepare 方法都會(huì)被調(diào)用,我們可以在 prepare 方法中對(duì)歷史記錄進(jìn)行修改。例如,對(duì)于 MoveItemAction,我們判斷上一個(gè) action 是否和當(dāng)前 action 屬于同一次移動(dòng)操作,然后來(lái)決定在應(yīng)用當(dāng)前 action 之前是否移除上一個(gè) action。代碼如下:

// actions/MoveItemAction.ts
export default class MoveItemAction extends Action {
 prevItem: Item
 // 一次圖形拖動(dòng)操作可以由以下三個(gè)變量來(lái)進(jìn)行描述:
 // 拖動(dòng)開始時(shí)鼠標(biāo)的位置(startPos),拖動(dòng)過程中鼠標(biāo)的位置(movingPos),以及拖動(dòng)的圖形的 ID
 constructor(readonly startPos: Point, readonly movingPos: Point, readonly itemId: number) {
 // 上一行中 readonly startPos: Point 相當(dāng)于下面兩步:
 // 1. 在MoveItemAction中定義startPos只讀字段
 // 2. 在構(gòu)造函數(shù)中執(zhí)行 this.startPos = startPos
 super()
 }
 prepare(history: AppHistory) {
 const lastAction = history.getLastAction()
 if (lastAction instanceof MoveItemAction && lastAction.startPos == this.startPos) {
  // 如果上一個(gè)action也是MoveItemAction,且拖動(dòng)操作的鼠標(biāo)起點(diǎn)和當(dāng)前action相同
  // 則我們認(rèn)為這兩個(gè)action在同一次移動(dòng)操作中
  this.prevItem = lastAction.prevItem
  return history.pop() // 調(diào)用pop方法來(lái)移除最近一個(gè)action
 } else {
  // 記錄圖形被移動(dòng)之前的狀態(tài),用于撤銷
  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) {
 // 撤銷的時(shí)候我們直接使用已經(jīng)保存的prevItem即可
 return state.setIn(['items', this.itemId], this.prevItem)
 }
 getMessage() { /* ... */ }
}

從上面的代碼中可以看到,prepare 方法除了使 action 自身準(zhǔn)備好之外,它還可以讓歷史記錄準(zhǔn)備好。不同的 Action 類型有不同的合并規(guī)則,為每種 Action 實(shí)現(xiàn)合理的 prepare 函數(shù)之后,撤消重做功能的用戶體驗(yàn)?zāi)軌虼蟠筇嵘?/p>

一些其他需要注意的地方

撤銷重做功能是非常依賴于不可變性的,一個(gè) Action 對(duì)象在放入 AppHistory.list 之后,其所引用的對(duì)象都應(yīng)該是不可變的。如果 action 所引用的對(duì)象發(fā)生了變化,那么在后續(xù)撤銷時(shí)可能發(fā)生錯(cuò)誤。本方案中,為了方便記錄操作發(fā)生時(shí)的一些必要信息,Action 對(duì)象的 prepare 方法中允許出現(xiàn)原地修改操作,但是 prepare 方法只會(huì)在 action 被放入歷史記錄之前調(diào)用一次,action 一旦進(jìn)入紀(jì)錄列表就是不可變的了。

總結(jié)

以上就是實(shí)現(xiàn)一個(gè)實(shí)用的撤銷重做功能的所有步驟了。不同的前端項(xiàng)目有不同的需求和技術(shù)方案,有可能上面的代碼在你的項(xiàng)目中一行也用不上;不過撤銷重做的思路應(yīng)該是相同的,希望本文能夠給你帶來(lái)一些啟發(fā)。

以上所述是小編給大家介紹的基于 Immutable.js 實(shí)現(xiàn)撤銷重做功能的實(shí)例代碼,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!

相關(guān)文章

  • JavaScript逆向案例之如何破解登錄密碼

    JavaScript逆向案例之如何破解登錄密碼

    這篇文章主要介紹了JavaScript逆向案例之如何破解登錄密碼,文章通過12306的登陸方法展開用戶登錄密碼的參數(shù)破解辦法,感興趣的小伙伴可以參考一下
    2022-06-06
  • Echarts折線圖如何根據(jù)容器寬度自適應(yīng)展示

    Echarts折線圖如何根據(jù)容器寬度自適應(yīng)展示

    我們使用vue做項(xiàng)目的時(shí)候,常常需要做到echarts圖表的自適應(yīng),一般是根據(jù)頁(yè)面的寬度做對(duì)應(yīng)的適應(yīng),下面這篇文章主要給大家介紹了關(guān)于Echarts折線圖如何根據(jù)容器寬度自適應(yīng)展示的相關(guān)資料,需要的朋友可以參考下
    2022-11-11
  • 微信小程序自定義tabBar在uni-app的適配詳解

    微信小程序自定義tabBar在uni-app的適配詳解

    這篇文章主要介紹了微信小程序自定義tabBar在uni-app的適配詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • JS 中 reduce()方法使用小結(jié)

    JS 中 reduce()方法使用小結(jié)

    reduce是一個(gè)對(duì)數(shù)組累積操作的方法,使用時(shí)要加上 return 返回累積操作的數(shù)據(jù),這樣 prev 才能獲取上一次執(zhí)行的結(jié)果,否則是 undefined,這篇文章主要介紹了JS 中 reduce()方法及使用詳解,需要的朋友可以參考下
    2023-12-12
  • 淺談webpack打包生成的bundle.js文件過大的問題

    淺談webpack打包生成的bundle.js文件過大的問題

    下面小編就為大家分享一篇淺談webpack打包生成的bundle.js文件過大的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧
    2018-02-02
  • js判斷兩個(gè)字符串是否相等的兩種方法

    js判斷兩個(gè)字符串是否相等的兩種方法

    昨天用Ajax作驗(yàn)證,在前臺(tái)JS中判斷返回的字符串的值與給定的值是否相等,索性給大家總結(jié)下,這篇文章主要給大家介紹了關(guān)于js判斷兩個(gè)字符串是否相等的兩種方法,需要的朋友可以參考下
    2023-05-05
  • textarea焦點(diǎn)的用法實(shí)現(xiàn)獲取焦點(diǎn)清空失去焦點(diǎn)提示效果

    textarea焦點(diǎn)的用法實(shí)現(xiàn)獲取焦點(diǎn)清空失去焦點(diǎn)提示效果

    這篇文章主要介紹了textarea焦點(diǎn)的用法實(shí)現(xiàn)獲取焦點(diǎn)清空失去焦點(diǎn)提示效果,需要的朋友可以參考下
    2014-05-05
  • Js callBack 返回前一頁(yè)的js方法

    Js callBack 返回前一頁(yè)的js方法

    通過 window.opener 實(shí)現(xiàn)。關(guān)于這個(gè)對(duì)象的用法請(qǐng)查閱相關(guān)資料。
    2008-11-11
  • 一起來(lái)看看JavaScript數(shù)據(jù)類型最詳解

    一起來(lái)看看JavaScript數(shù)據(jù)類型最詳解

    這篇文章主要為大家詳細(xì)介紹了JavaScript數(shù)據(jù)類型,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2022-01-01
  • ionic在開發(fā)ios系統(tǒng)微信時(shí)鍵盤擋住輸入框的解決方法(鍵盤彈出問題)

    ionic在開發(fā)ios系統(tǒng)微信時(shí)鍵盤擋住輸入框的解決方法(鍵盤彈出問題)

    在使用ionic開發(fā)ios系統(tǒng)微信的時(shí)候遇到一個(gè)bug,在填寫表單的時(shí)候鍵盤會(huì)擋住輸入框。下面小編給大家?guī)?lái)了ionic在開發(fā)ios系統(tǒng)微信時(shí)鍵盤擋住輸入框的解決方法(鍵盤彈出問題),非常不錯(cuò),有需要的朋友參考下吧
    2016-09-09

最新評(píng)論