• content
    {:toc}

前端开发中,如果遇到复杂的交互逻辑,数据结构的知识将帮助你理清思路,抽象逻辑,完成稳定可靠的逻辑代码。

本文就讲讲我在开发弹窗时加入的队列数据结构,也许有人疑问弹窗不是很简单吗,还需要引入队列?其实在复杂交互中,特别是互动类的界面中,很容易就会有超过 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种情况:

  1. 同一时间段只有一个 Dialog 被触发
  2. 同一时间段有2个 Dialog 同时被触发
  3. 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 完成时,出队,并递归调用展示,直到队列内容为空。这里不展开说明了。

小结

当然上面的需求不使用队列也可以实现,但是队列数据结构的意义在于可以让整个实现更加规范化、抽象化且易于维护。

熟练掌握数据结构的知识,可以让开发的过程中思路更加清晰,代码抽象化程度更高,更加合理的组织代码,提高开发效率。当遇到棘手的问题时,可以多思考一些数据结构中的知识点,说不定可以达到事半功倍的效果呢!