- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第32期,链接32期
要阅读的源码
- 链接: yocto-queue
先来看一下readme
-
总体介绍
- 你应该使用这个包,当你要对一个大数组做频繁的push和shift操作时,shift方法是线性的时间复杂度的,也就是O(n)而Queue#dequeue方法是固定的时间复杂度,也就是O(1),这时使用队列就很有意义了。
- 队列就是一个有序的列表,添加元素向队列部尾部添加,移除元素从头部移除,它遵循先进先出的原则,也就是(FIFO)原则。
-
API:
创建一个可迭代的实例queue,你可以用for...of来遍历它,或者用展开语法...,将其转化成一个数组,但是非必要不要去那么做因为这样很慢。
.enqueue(value):在队列里新增一个值。.dequeue():移除队列里的下一个值,返回移除的值或者当队列为空时,返回undefined。.clear():将队列情况.size:获取队列的大小
开始调试
clone完毕项目后,用vscode打开项目,下载依赖后,打开package.json文件。
点击红色箭头的debug,就可以开始调试了,当然别忘了在index.js上打上断点。
分析:
经过断点调试,差不多就知道整个程序想要做什么了。下面就一个一个来分析
- Class Node
class Node {
value;
next;
constructor(value) {
this.value = value;
}
}
一个名称为Node的class,它所创建的实例会成为队列的一个节点的值,value存储着enqueue进去的值,next则存放下一个节点的值,数据结构如下{value:1,next:{value:2,next:undefined}}
- Class Queue
export default class Queue {
//设置了一些私有实例字段,它不能在类的外部定义,从作用域之外引用 # 名称、内部在未声明
//的情况下引用私有字段、或尝试使用 delete 移除声明的字段都会抛出语法错误。
//它需要预先定义,不能在赋值时定义
#head;
#tail;
#size;
constructor() {
this.clear();
}
// 添加值
enqueue(value) {
//node的实例node:{value:xxx,next:xxx}
const node = new Node(value);
//判断头部是否存在
if (this.#head) {
//设置头部节点和尾部节点的next属性,next属性永远指向下一个节点,
//因为#tail和#head在第一次enqueue后,
//两者都指向同一个node,所以修改#tail的next
//也就是修改#head的next
this.#tail.next = node;
//将尾部节点设置为新的node
this.#tail = node;
} else {
//不存在的话,设置头部节点和尾部节点的值为node
this.#head = node;
this.#tail = node;
}
this.#size++; //队列的大小加+1
}
// 删除先前进来的值
dequeue() {
const current = this.#head; //获取头部节点的值
if (!current) {
//没有则直接返回undefined
return;
}
//将下一个节点的值移动到头部节点,也就是说头部节点相邻的第一个节点
//成为了头部节点
this.#head = this.#head.next;
this.#size--; //队列大小减一
return current.value; //返回已被删除的值
}
// 清除队列
clear() {
this.#head = undefined; //将头部节点设置为undefined
this.#tail = undefined; //将尾部节点设置为undefined
this.#size = 0; //将大小设置为0
}
// getter属性,返回队列的大小
get size() {
return this.#size;
}
// 定义了queue实例的迭代器
*[Symbol.iterator]() {
let current = this.#head;
while (current) {
yield current.value;
current = current.next;
}
}
}
-
yocto包的运行机制
- 作者的注释如下
我的理解:
#head保留着当前node的引用和下一个node的引用,当使enqueue方法塞一个值进去的时候,需要去在#head去查找最后一个node的引用。当enqueue很多值后,#head的数据结构可能会变成这样{ value:1, next:{ value:2, next:{ value:3, next:{ value:4, next:{ ..... } } } } }这样的话,想要
enqueue成功的话,就需要遍历#head,这样的时间复杂度就上升到O(n)了,也就是很慢了,而有了#tail之后,#tail保存着最新的enqueue的值,当需要给queue再增加一个节点时,只需更新#tail的值,就能更新#head的值。因为两者都指向同一个引用,这样它的时间复杂度就降低到O(1)了if (this.#head) { //因为#tail和#head在第一次enqueue后, //两者都指向同一个node,所以修改#tail的next //也就是修改#head的next this.#tail.next = node; //将尾部节点设置为新的node this.#tail = node; }验证如下
let obj = {k:1,c:undefined} let a = obj; let b = obj; console.log('b',b);//b { k: 1, c: undefined } console.log('a',a);//a { k: 1, c: undefined } let obj2 = {k:2,c:undefined} b.c = obj2 console.log('b',b);b { k: 1, c: { k: 2, c: undefined } } console.log('a',a);a { k: 1, c: { k: 2, c: undefined } } b = obj2 console.log('b',b);b { k: 2, c: undefined } console.log(a);{ k: 1, c: { k: 2, c: undefined } } let obj3 = {k:3,c:undefined} b.c = obj3 console.log(b)//{ k: 2, c: { k: 3, c: undefined } } console.log(a);//{ k: 1, c: { k: 2, c: { k: 3, c: undefined } } } console.log(obj);//{ k: 1, c: { k: 2, c: { k: 3, c: undefined } } }
其他:
- 私有变量可以通过一下方式访问
class Rectangle { #height = 0 #width; constructor(height,width){ this.#height = height; this.#width = width; } getArea(){ return this.#height * this.#width } } let rect1 = new Rectangle(10,10) console.log(rect1.getArea()); - Symbol.iterator
// Symbol.iterator为每个对象定义了默认的迭代器, // 该迭代器可以被`for...of`循环使用 // 当需要对一个对象迭代时,它的`@@iterator`方法都会在不传参 // 的情况下被调用,返回的迭代器用于获取要迭代的值 // Array,TypedArray,String,Map,Set拥有默认的迭代方法,Object则没有 // 自定义一个迭代器 var myIterable = {}; myIterable[Symbol.iterator] = function * (){ // yield能暂停生成器执行,并且yield右边表达式生成的值,会提供给生成器的next函数,作为next函数 // 的返回值 yield 1; yield 2; yield 3; } for (const item of myIterable) { console.log('item: ', item);//item:1,item:2,item:3 }
总结:
-
队列在处理超大数组时会比较高效。
-
class的私有变量确保了它不会被作用域外操作的更改。
-
Symbol.iterator能够为对象定义一个迭代器,数组,Map,set都定义了默认的迭代器。
-
yield能暂停生成器的执行,它的功能类似于await,可以用到异步操作上。
-
function* 生成器函数,会返回一个迭代对象。