本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
源码地址
前提概要
如果你使用的数据结构是Queue(即使JS里对于数据结构都是通过Array/Object模拟出来的),那么在遵循FIFO的原则下,当你在数据量巨大的数据中进行操作时,Array#shift操作的时间复杂度达到了O(n)。而如果要在JS中实现FIFO的优化,利用Object模拟实现链表反而比Array来得更为高效。
源码解析
// 定义一个Node节点类用于每次新增数据时创建节点
class Node {
value;
next;
constructor(value) {
this.value = value;
}
}
export default class Queue {
// 创建私有属性头指针,尾指针和大小
#head;
#tail;
#size;
// 每次实例化时执行clear方法清除指针指向与size清零
constructor() {
this.clear();
}
enqueue(value) {
// 对于入队操作的数据将其实例化为一个Node节点
const node = new Node(value);
// 头指针判断,首次有数据进入队列时头尾指针都指向该节点
// 否则只需要将尾指针指向目标节点
// 需要注意的是确保链表的连接性,因此需要先赋值#tail.next,再赋值#tail
if (this.#head) {
this.#tail.next = node;
this.#tail = node;
} else {
this.#head = node;
this.#tail = node;
}
// 每次入队时增加其大小
this.#size++;
}
dequeue() {
// 出队操作永远是获取链表中的头指针数据
const current = this.#head;
// 如果current无值则说明该队列为空
if (!current) {
return;
}
// 将头指针后移
this.#head = this.#head.next;
this.#size--;
return current.value;
}
// 清空头尾指针的引用与size的值即清空了队列的数据,其余的交给垃圾回收机制
clear() {
this.#head = undefined;
this.#tail = undefined;
this.#size = 0;
}
get size() {
return this.#size;
}
// 这一步在于为了实现该队列可迭代,也就是可被for...of遍历以及使用(...)扩展转换为数组
* [Symbol.iterator]() {
let current = this.#head;
while (current) {
yield current.value;
current = current.next;
}
}
}
读源码时我们在读什么?
Javascript因为其数据类型的薄弱,对于各种数据结构都是以数组和对象的方式去模拟,以至于当我们使用JS去某些大数据场景去操作数据时并不能很清晰地看出时间复杂度,毕竟在我们了解到数组时,它所携带的API几乎已经囊括了我们所需要的业务需求。
而在此次的源码学习中我们需要知道如何去模拟链表,这在之后学习算法时会用到,而symbol实现迭代的方法,也是一个常用技巧。
let arr1 = new Array(100000).fill(0);
console.time('Array');
while(arr1.length) {
arr1.shift();
}
console.timeEnd('Array'); // 1.121s
console.time('Queue in');
let arr2 = new Queue();
while(arr2.size < 100000) {
arr2.enqueue(0);
}
console.timeEnd('Queue in'); // 46.138ms
console.time('Queue out');
while(arr2.size > 0) {
arr2.dequeue();
}
console.timeEnd('Queue out'); // 4.872ms