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中文网其它相关文章!
推荐阅读:
Atas ialah kandungan terperinci js怎么做出撤销重做功能. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Alat AI Hot

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool
Gambar buka pakaian secara percuma

Clothoff.io
Penyingkiran pakaian AI

AI Hentai Generator
Menjana ai hentai secara percuma.

Artikel Panas

Alat panas

Notepad++7.3.1
Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1
Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6
Alat pembangunan web visual

SublimeText3 versi Mac
Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Topik panas



Akan terdapat banyak fungsi penciptaan AI dalam apl Doubao, jadi apakah fungsi apl Doubao? Pengguna boleh menggunakan perisian ini untuk membuat lukisan, berbual dengan AI, menjana artikel untuk pengguna, membantu semua orang mencari lagu, dsb. Pengenalan fungsi apl Doubao ini boleh memberitahu anda kaedah operasi khusus Kandungan khusus ada di bawah, jadi lihat! Apakah fungsi apl Doubao Jawapan: Anda boleh melukis, bersembang, menulis artikel dan mencari lagu. Pengenalan fungsi: 1. Pertanyaan soalan: Anda boleh menggunakan AI untuk mencari jawapan kepada soalan dengan lebih pantas, dan anda boleh bertanya apa-apa jenis soalan. 2. Penjanaan gambar: AI boleh digunakan untuk mencipta gambar yang berbeza untuk semua orang Anda hanya perlu memberitahu semua orang keperluan umum. 3. Sembang AI: boleh mencipta AI yang boleh berbual untuk pengguna,

Kedua-dua telefon bimbit vivox100s dan x100 adalah model yang mewakili dalam barisan produk telefon mudah alih vivo, masing-masing mewakili tahap teknologi canggih vivo dalam tempoh masa yang berbeza. Artikel ini akan menjalankan perbandingan terperinci antara kedua-dua telefon mudah alih ini dari segi perbandingan prestasi dan analisis fungsi untuk membantu pengguna memilih telefon mudah alih yang sesuai dengan mereka dengan lebih baik. Mula-mula, mari kita lihat perbandingan prestasi antara vivox100s dan x100. vivox100s dilengkapi dengan yang terbaru

Dengan perkembangan pesat Internet, konsep media kendiri telah berakar umbi dalam hati orang ramai. Jadi, apakah sebenarnya media kendiri? Apakah ciri dan fungsi utamanya? Seterusnya, kita akan meneroka isu-isu ini satu demi satu. 1. Apakah sebenarnya media kendiri? Kami-media, seperti namanya, bermakna anda adalah media. Ia merujuk kepada pembawa maklumat yang melaluinya individu atau pasukan boleh mencipta, mengedit, menerbitkan dan menyebarkan kandungan secara bebas melalui platform Internet. Berbeza dengan media tradisional, seperti akhbar, televisyen, radio, dan lain-lain, media kendiri lebih interaktif dan diperibadikan, membolehkan semua orang menjadi pengeluar dan penyebar maklumat. 2. Apakah ciri dan fungsi utama media kendiri? 1. Ambang rendah: Peningkatan media kendiri telah menurunkan ambang untuk memasuki industri media Peralatan yang rumit dan pasukan profesional tidak lagi diperlukan.

Memandangkan Xiaohongshu menjadi popular di kalangan golongan muda, semakin ramai orang mula menggunakan platform ini untuk berkongsi pelbagai aspek pengalaman dan pandangan hidup mereka. Cara mengurus berbilang akaun Xiaohongshu dengan berkesan telah menjadi isu utama. Dalam artikel ini, kami akan membincangkan beberapa ciri perisian pengurusan akaun Xiaohongshu dan meneroka cara mengurus akaun Xiaohongshu anda dengan lebih baik. Apabila media sosial berkembang, ramai orang mendapati diri mereka perlu mengurus berbilang akaun sosial. Ini juga merupakan satu cabaran untuk pengguna Xiaohongshu. Sesetengah perisian pengurusan akaun Xiaohongshu boleh membantu pengguna mengurus berbilang akaun dengan lebih mudah, termasuk penerbitan kandungan automatik, penerbitan berjadual, analisis data dan fungsi lain. Melalui alatan ini, pengguna boleh mengurus akaun mereka dengan lebih cekap dan meningkatkan pendedahan dan perhatian akaun mereka. Di samping itu, perisian pengurusan akaun Xiaohongshu mempunyai

Tip PHP: Cepat melaksanakan fungsi kembali ke halaman sebelumnya Dalam pembangunan web, kita sering menghadapi keperluan untuk melaksanakan fungsi kembali ke halaman sebelumnya. Operasi sedemikian boleh meningkatkan pengalaman pengguna dan memudahkan pengguna menavigasi antara halaman web. Dalam PHP, kita boleh mencapai fungsi ini melalui beberapa kod mudah. Artikel ini akan memperkenalkan cara cepat melaksanakan fungsi kembali ke halaman sebelumnya dan memberikan contoh kod PHP tertentu. Dalam PHP, kita boleh menggunakan $_SERVER['HTTP_REFERER'] untuk mendapatkan URL halaman sebelumnya

"Meneroka Discuz: Definisi, Fungsi dan Contoh Kod" Dengan perkembangan pesat Internet, forum komuniti telah menjadi platform penting untuk orang ramai mendapatkan maklumat dan bertukar pendapat. Di antara banyak sistem forum komuniti, Discuz, sebagai perisian forum sumber terbuka yang terkenal di China, digemari oleh majoriti pembangun dan pentadbir laman web. Jadi, apa itu Discuz? Apakah fungsi yang ada padanya, dan bagaimana ia boleh membantu tapak web kami? Artikel ini akan memperkenalkan Discuz secara terperinci dan melampirkan contoh kod khusus untuk membantu pembaca mengetahui lebih lanjut mengenainya.

Penjelasan terperinci tentang fungsi dan fungsi GDM di bawah Linux Dalam sistem pengendalian Linux, GDM (GNOMEDisplayManager) ialah pengurus log masuk grafik yang menyediakan antara muka untuk pengguna log masuk dan log keluar daripada sistem. GDM biasanya merupakan sebahagian daripada persekitaran desktop GNOME, tetapi boleh digunakan oleh persekitaran desktop lain juga. Peranan GDM bukan sahaja untuk menyediakan antara muka log masuk, tetapi juga termasuk pengurusan sesi pengguna, penyelamat skrin, log masuk automatik dan fungsi lain. Fungsi GDM terutamanya merangkumi aspek berikut:

PHP ialah bahasa skrip bahagian pelayan yang digunakan secara meluas dalam pembangunan web Fungsi utamanya adalah untuk menjana kandungan web yang dinamik Apabila digabungkan dengan HTML, ia boleh mencipta halaman web yang kaya dan berwarna-warni. PHP berkuasa Ia boleh melaksanakan pelbagai operasi pangkalan data, operasi fail, pemprosesan borang dan tugas lain, menyediakan interaktiviti dan fungsi yang berkuasa untuk tapak web. Dalam artikel berikut, kami akan meneroka lebih lanjut peranan dan fungsi PHP, dengan contoh kod terperinci. Mula-mula, mari kita lihat penggunaan biasa PHP: penjanaan halaman web dinamik: P
