本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
前言
yocto-queue作用
针对 先进先出 这种场景,如果是大型数组,我们应该使用链表而不是数组,因为Array#shift()它具有线性时间复杂度 O(n)而Queue#dequeue()具有恒定时间复杂度 O(1), 从而优化该场景下的性能
在javascript中,是没有链表这个数据结构的yocto-queue便是用js对链表的实现
前置知识
什么是链表
链表是物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,由一系列可动态生成的结点(地址)组成
链表与数组的区别
1、链表是链式存储结构,数组是顺序存储结构
2、链表通过指针连接元素与元素,而数组则是把所有元素按顺序进行存储
3、链表的插入和删除元素比较简单,不需要移动元素,且较为容易实现长度的扩充,但是查询元素比较困难数组是查询比较快,但是删除和增加会比较麻烦。
什么是迭代器&生成器
迭代器
在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值
迭代器对象可以通过重复调用 next()显式地迭代。 迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。 在产生终止值之后,对 next()的额外调用应该继续返回{done:true}
function makeRangeIterator() {
// 迭代器1
// value 表示当前值
// done 表示是否是最后一个迭代器
// next方法 返回下一个迭代器
let iterator = {
value: "1",
done: false,
next: function () { return iterator2 } // 返回迭代器2
}
// 迭代器2
const iterator2 = {
value: "2",
done: false,
next: function () { return iterator3 } // 返回迭代器3
}
// 迭代器3
const iterator3 = {
value: "3",
done: false,
next: function () { return iteratorDone } // 返回迭代器结束
}
// 迭代器结束
// 迭代器结束时 done 字段为 true
const iteratorDone = {
done: true,
next: function () { return iteratorDone }
}
return {
next: function () {
const result = iterator
iterator = iterator.next()
return result
}
}
}
使用方式:
let it = makeRangeIterator();
console.log(it.next()) // {value: 0, done: false}
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {"done":true}
生成器
生成器函数使用 function*语法编写,配合yield字段生成迭代器
yield关键字实际返回一个对象,它有两个属性,value和done。value属性是对yield表达式求值的结果,而done是false,表示生成器函数尚未完全完成
我们再使用生成器实现一遍上面的代码
function* makeRangeIterator(end = Infinity) {
yield '1';
yield '2';
yield '3';
}
var a = makeRangeIterator(3)
console.log(a.next()) // {value: 0, done: false}
console.log(a.next()) // {value: 1, done: false}
console.log(a.next()) // {value: 2, done: false}
console.log(a.next()) // {"done":true}
源码
框架
class Node {}
export default class Queue {
#head; // 指向头指针
#tail; // 指向当前节点
#size; // 当前队列元素个数
// 构造函数
constructor () {}
// 向队列中添加一个值
enqueue (value) {}
// 删除队列中的下一个值
dequeue () {}
// 清空队列
clear () {}
// 获取当前链表大小
get size () {}
*[Symbol.iterator] () {}
}
node类
class Node {
value; // 当前节点值
next; // 指向后一个节点
constructor(value) {
this.value = value;
}
}
这里使用到了设计模式中的工厂模式,使用node类实例化出来的对象,都有value和next这两个字段
constructor 构造方法
constructor() {
this.clear();
}
构造函数只是调用了一下清空队列的方法
这里的clear()方法主要起设置初始值的作用,比如上面虽然定义了size字段,但是并未赋值
clear()方法中有一段this.#size = 0正好可以给size赋初始值,提高代码复用
API
enqueue 入栈
enqueue (value) {
const node = new Node(value);
// 判断是否存在头部节点
if (this.#head) {
// 如果存在就新节点追加到当前节点后面
this.#tail.next = node;
this.#tail = node;
} else {
// 如果不存在,则把头部节点和当前节点指向新节点
this.#head = node;
this.#tail = node;
}
this.#size++; // 实时记录链表大小
}
dequeue 出栈
dequeue () {
const current = this.#head;
// 如果头部节点不存在则不执行
if (!current) {
return;
}
// 将头部节点指向后一个节点(先入先出)
this.#head = this.#head.next;
this.#size--; // 实时记录链表大小
return current.value;
}
clear 清空
clear () {
this.#head = undefined;
this.#tail = undefined;
this.#size = 0; // 重置链表大小
}
size 获取大小
get size () {
return this.#size;
}
这里使用了get关键字
主要是为了让实例对象获取链表大小时,可以通过实例对象.size直接获取当前链表大小
Symbol.iterator
*[Symbol.iterator] () {
let current = this.#head;
while (current) {
yield current.value;
current = current.next;
}
}
看到这里有的可能会比较诧异,这段代码有什么用?你说enqueue dequeue 这些还能理解,它们是队列的API函数,但是这Symbol.iterator是个什么鬼?上面使用时也没看到哪里有调用它啊?
那我我们先注释掉Symbol.iterator代码部分,打印试一下
export default class Queue {
// 其余代码...
// *[Symbol.iterator] () {
// 内部代码...
// }
}
const queue = new Queue();
queue.enqueue('🦄');
queue.enqueue('🌈');
queue.enqueue('🦄');
for (let i of queue) {
console.log(i)
}
// TypeError: queue is not iterableat
循环时时浏览器就直接 抛出了异常
然后我们再取消Symbol.iterator注释了,再打印试一下
export default class Queue {
// 其余代码...
// 取消注释 Symbol.iterator
*[Symbol.iterator] () {
// 内部代码...
}
}
const queue = new Queue();
queue.enqueue('🦄');
queue.enqueue('🌈');
queue.enqueue('🦄');
for (let i of queue) {
console.log(i)
}
// 🦄🌈🦄
这里是可以正常打印的
到这里们不难看出Symbol.iterator作用就是为了让实例化出来的对象可以被 for...of 循环
然后我们再来解析源码
*[Symbol.iterator] () {
// 当前节点指向头部链表头部节点
let current = this.#head;
while (current) {
// 使用 yield 生成迭代器
yield current.value;
// 当前节点指向后一个节点
// 如果后面没有节点了,将停止循环
current = current.next;
}
}
总结
通过yocto-queue源码的学习,加深了数据结构中链表的印象,也学习了Symbol.iterator的应用场景,s收获还是比较多吧