前端开发中,如果遇到复杂的交互逻辑,数据结构的知识将帮助你理清思路,抽象逻辑,完成稳定可靠的逻辑代码。
本文就讲讲我在开发弹窗时加入的队列数据结构,也许有人疑问弹窗不是很简单吗,还需要引入队列?其实在复杂交互中,特别是互动类的界面中,很容易就会有超过 10 个弹窗对话框,万一同时被触发时,逻辑就会混乱,我们希望一个接一个的方式弹出,这里就需要队列了。
什么是队列
队列(Queue) 是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在尾部进行插入操作(入队 enqueue),在头部进行删除操作(出队 dequeue)。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。上图清晰的描述了队列的特性。
JavaScript 实现队列
一个队列数据结构要包含以下 api
使用 JavaScript/TypeScript 数组可以模拟出这些 api,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class Queue { private dataStore: any[]
constructor() { this.dataStore = [] }
public enqueue(e: any): void { this.dataStore.push(e) }
public dequeue() { this.dataStore.shift() }
public front() { return this.dataStore[0] }
public back() { return this.dataStore[this.dataStore.length - 1] }
public isEmpty(): boolean { if (this.dataStore.length === 0) { return true } return false }
public toString() { return this.dataStore.join(',') } }
export default Queue
|
可以写些测试用例来验证这个队列的功能,保证我们的队列运行正常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import Queue from './Queues'
describe('Queue', () => { const q = new Queue() q.enqueue(1123) q.enqueue('chuanshi') q.enqueue('666') test('queue', () => { expect(q.toString()).toBe('1123,chuanshi,666') q.dequeue() expect(q.toString()).toBe('chuanshi,666') expect(q.front()).toBe('chuanshi') expect(q.back()).toBe('666') expect(q.isEmpty()).toBe(false) q.dequeue() q.dequeue() expect(q.isEmpty()).toBe(true) }) })
|
这样我们就得到了一个队列的数据结构。
对话框弹窗(Dialog)与队列的结合
弹窗被触发唤起会有以下3种情况:
- 同一时间段只有一个 Dialog 被触发
- 同一时间段有2个 Dialog 同时被触发
- Dialog 正在展示时,又触发了另一个 Dialog
为了满足以上3种情况,需要在主线程和弹窗展示之间加一个队列控制逻辑,它们的时序图如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| title 队列弹窗逻辑之1个弹窗
right footer by Chuanshi 20190712
skinparam activityBackgroundColor #efefef skinparam activityBorderColor #454545 skinparam activityArrowColor #666666 skinparam style strictuml
participant DialogA order 10 participant Main order 1 participant QueueCtrl order 2
activate Main
Main -> QueueCtrl: showDialogA activate QueueCtrl QueueCtrl -> QueueCtrl: enqueue eventA
QueueCtrl -> DialogA: show activate DialogA DialogA -> DialogA: showing DialogA -> QueueCtrl: close deactivate DialogA QueueCtrl -> QueueCtrl: dequeue eventA
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| title 队列弹窗逻辑之2个弹窗同时被触发
right footer by Chuanshi 20190712
skinparam activityBackgroundColor #efefef skinparam activityBorderColor #454545 skinparam activityArrowColor #666666 skinparam style strictuml
participant DialogA order 20 participant DialogB order 30 participant Main order 1 participant QueueCtrl order 2
activate Main
Main -> QueueCtrl: showDialogA activate QueueCtrl QueueCtrl -> QueueCtrl: enqueue eventA Main -> QueueCtrl: showDialogB QueueCtrl -> QueueCtrl: enqueue eventB
QueueCtrl -> DialogA: show activate DialogA DialogA -> DialogA: showing DialogA -> QueueCtrl: close deactivate DialogA QueueCtrl -> QueueCtrl: dequeue eventA
QueueCtrl -> DialogB: show activate DialogB DialogB -> DialogB: showing DialogB -> QueueCtrl: close deactivate DialogB QueueCtrl -> QueueCtrl: dequeue eventB
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| title 队列弹窗逻辑之1个正在展示时又触发另一个弹窗
right footer by Chuanshi 20190712
skinparam activityBackgroundColor #efefef skinparam activityBorderColor #454545 skinparam activityArrowColor #666666 skinparam style strictuml
participant DialogA order 20 participant DialogB order 30 participant Main order 1 participant QueueCtrl order 2
activate Main
Main -> QueueCtrl: showDialogA activate QueueCtrl QueueCtrl -> QueueCtrl: enqueue eventA QueueCtrl -> DialogA: show activate DialogA DialogA -> DialogA: showing Main -> QueueCtrl: showDialogB QueueCtrl -> QueueCtrl: enqueue eventB DialogA -> QueueCtrl: close deactivate DialogA QueueCtrl -> QueueCtrl: dequeue eventA QueueCtrl -> DialogB: show activate DialogB DialogB -> DialogB: showing DialogB -> QueueCtrl: close deactivate DialogB QueueCtrl -> QueueCtrl: dequeue eventB
|
上述时序图清晰的表达了弹窗触发的各种情况,那么起到关键作用的队列控制(QueueCtrl)部分应该如何编写呢?其实也很简单,它的逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| skinparam activityBackgroundColor #efefef skinparam activityBorderColor #454545 skinparam activityArrowColor #666666 skinparam style strictuml
title 队列弹窗逻辑活动图
right footer by Chuanshi 20190712
start fork :元素入队; fork again while (队列为空?) is (false) :队首弹窗触发; :队首弹窗关闭; :队首元素出队; endwhile (true) endfork stop
|
当空队列的第一个元素入队后,上图的右侧循环部分开始启动,同时依然可以有元素入队,直到右侧循环逻辑将队列所有元素出队后,整个活动停止。
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import Queue from './Queues'
const queue = new Queue()
const push = (eventName: globalEventName) => { if (queue.isEmpty()) { queue.enqueue(eventName) openDialog() } else { queue.enqueue(eventName) } }
const openDialog = () => { document.dispatchEvent(new Event(queue.front()))
document.addEventListener(`${queue.front()}Close`, () => { queue.dequeue() if (!queue.isEmpty()) { openDialog() } }) }
export default { push, }
|
只需要调用 push()
就可以达到我们的目的,可以看到使用队列这种数据结构,不到20行代码,非常简洁优雅的解决了这个问题!
Toast 与队列的结合
与 Dialog 类似,为了避免多次触发导致的 Toast 堆叠,把每一个要弹出的 Toast 内容入队,每个 Toast 完成时,出队,并递归调用展示,直到队列内容为空。这里不展开说明了。
小结
当然上面的需求不使用队列也可以实现,但是队列数据结构的意义在于可以让整个实现更加规范化、抽象化且易于维护。
熟练掌握数据结构的知识,可以让开发的过程中思路更加清晰,代码抽象化程度更高,更加合理的组织代码,提高开发效率。当遇到棘手的问题时,可以多思考一些数据结构中的知识点,说不定可以达到事半功倍的效果呢!