大家好,我是若川。持续组织了8个月源码共读活动,感兴趣的可以 点此加我微信ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有
江西|湖南|湖北
籍前端群,可加我微信进群。这是来自源码共读群中大二的小伙伴投稿,写的非常好,图文并茂,关键还写了好多篇笔记了。原文链接可点击文末阅读原文查看。
Part11. 前言
1.1 这个库,是干啥的
如果你项目中要用到一个非常大的数组,并且你经常需要使用这两个操作:
Array.push()
在末端添加一个元素.Array.shift()
在取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一
如果学过数据结构,就会敏锐地发现,诶这两个操作,不就是在模拟队列吗
queue 队列是一个有序的元素列表,其中一个元素插入到队列的末尾,然后从队列的前面移除。队列的工作原理是**先进先出(FIFO)**。
JS 没有queue
这个数据结构,用数组模拟就好了,真方便!
nonono,回到开头,当数据量较小的时候,似乎没什么影响,但如果数据量较大,性能就会严重下降
这是因为在底层实现中,数组是顺序存储的,当你shift
的时候,会先取出队列首端的一个元素,整个队列往前移——整个操作的事件时间复杂度是**O(n)**
如果你的项目正如上面我所说的情况,那么你很可能就需要这个包 yocto-queue,它能让你的shift
操作时间复杂度降为O(1)
。(在这库里面shift
用的是dequeue
方法)
1.2 你能学到
ES6 中的
class
链表和数组的区别,时间复杂度
JS 实现链表的方法
学习 Symbol.iterator 的使用场景
调试源码
Part22. 准备
2.1 了解API
import Queue from 'yocto-queue';const queue = new Queue();queue.enqueue('🦄');
queue.enqueue('🌈');console.log(queue.size);
//=> 2console.log(...queue);
//=> '🦄 🌈'console.log(queue.dequeue());
//=> '🦄'console.log(queue.dequeue());
//=> '🌈'
queue = new Queue()
The instance is an Iterable, which means you can iterate over the queue front to back with a “for…of” loop, or use spreading to convert the queue to an array. Don't do this unless you really need to though, since it's slow.
该实例是可枚举的,也就是说 你可以用for...of
来遍历,并且可以用扩展运算符将其变为数组,但是尽量不要这样做,这样性能很差
.enqueue(value)
添加一个元素到队尾
.dequeue()
删去队头,并返回被删除的值 || 或者是 undefined
(队列本来就已经为空的情况)
.clear()
清空队列
.size
返回队列的大小
Part33 看看 源码
3.1 环境准备
# 克隆官方仓库
git clone https://github.com/sindresorhus/yocto-queue.git
cd .\yocto-queue\
npm install
code .
3.3 调试源码
查看 package.json文件来确定主入口为 index.js
demo
新建文件夹examples
,存放 demo index.js
// yocto-queue/examples/index.js
import Queue from "../index.js";const queue = new Queue(); //此处打断点
queue.enqueue("⛵");
queue.enqueue("🌊");console.log(queue.dequeue());console.log(queue.size);
for (let q of queue) {console.log(q);
}
queue.clear();
node examples/index.js
或者直接F5
也可以即可开始调试源码,其实这个代码复杂度不手动调试也可以的,但是通过调试可以让你很明确地看到哪一步代码用到了哪里的东西
3.4 理解源码
源码
Queue
中,#head
和#tail
可以视作虚拟结点,只是分别用来指向头和尾结点的。每次遍历的时候先找到头结点(#head
指向的结点),然后通过每个结点的next
指针往后走。即使只有头结点也能组成该链表——慢慢遍历总能到最后面,但是显然这样效率就低了,所以还有一个专门的尾指针#tail
,方便尾部插入结点
源码总览:
class Node {value;next;constructor(value) {this.value = value;}
}export default class Queue {#head;#tail;#size;constructor() {this.clear();}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() {const current = this.#head;if (!current) {return;}this.#head = this.#head.next;this.#size--;return current.value;}clear() {this.#head = undefined;this.#tail = undefined;this.#size = 0;}get size() {return this.#size;}* [Symbol.iterator]() {let current = this.#head;while (current) {yield current.value;current = current.next;}}
}
分步解析
enqueue
queue.enqueue("⛵");
时,会创造Queue
中第一个实例node
,第一个结点自然头和尾都指向他自己
if (this.#head) {//...
} else {this.#head = node;this.#tail = node;
}
queue.enqueue("🌊");
随后我们添加第二个结点
if (this.#head) {this.#tail.next = node;this.#tail = node;
} else {//...
}
实际上我们可以发现,这就是尾插法
dequeue
console.log(queue.dequeue());
dequeue() {const current = this.#head; //获取当前if (!current) {return;}this.#head = this.#head.next;this.#size--;return current.value;
}
size
console.log(queue.size);
get size() {return this.#size;
}
这里用到了 class 中 getters
⭐for...of
这里是本文的一个重点
这里实现了Queue
这个对象可以通过for...of
来进行遍历,即让它可以迭代。
想要让对象可迭代,需要添加一个Symbol.iterator
方法,这个方法专门用来使对象可迭代的内建symbol
。
通过调试我们也可以知道,当进入for...of
,他就会进入Symbol.iterator
这个方法,(如果没找到,就会报错,像数组那些对象都是有内置该方法的),该方法必须返回一个迭代器—— 一个有next
方法的对象。
像这样使用:
let range = {from: 1,to: 5,[Symbol.iterator]() { // 在 for..of 循环开始时被调用一次return {current: this.from,last: this.to,next() { // 每次迭代时都会被调用,来获取下一个值if (this.current <= this.last) {return { done: false, value: this.current++ };} else {return { done: true };}}};}
};for(let value of range) {alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}
而源码中并不是这样的,而是这样:
* [Symbol.iterator]() {let current = this.#head; //通过current记录当前迭代进程while (current) { //循环取值,直到没有yield current.value; //取值,并返回current = current.next;//通过next往下一个走}
}
这是因为这里并不仅是使用了Symbol.iterator
⭐生成器
生成器是 ES6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种能力具有深远的影响,比如使用生成器可以自定义迭代器和实现协程。
在函数前面加一个星号*
,则表示它是一个生成器。调用生成器函数会产生一个生成器对象,其一开始处于暂停状态,该对象也实现了Iterator
接口,通过调next()
使其转为开始或者恢复执行状态。生成器函数在遇到yield
关键字前会正常执行,遇到该关键字后,执行会停止,函数作用域的状态被保留 —— 有点像函数的中间返回语句,它能让函数返回一个值出去,但是函数仍能继续执行。随后通过在生成器对象上调用next
方法恢复执行。
实际上,很少在生成器对象上显式调用next()
,而是将其作为可迭代对象——
function* generatorFn(){yield 1;yield 2;yield 3;
}
for(let i of generatorFn()){console.log(i)
}
//1
//2
//3
让我们回到源码中
for (let q of queue) {console.log(q);
}
结合上面对Symbol.iterator
的理解
当进入
for...of
,他就会进入Symbol.iterator
这个方法
也就是说 这样调用时,实际上就是
for (let q of queue[Symbol.iterator]()) {console.log(q);
}
让[Symbol.iterator]
这个函数变为了生成器函数,并将其作为可迭代对象!大大地减少了代码量~
clear
clear() {this.#head = undefined;this.#tail = undefined;this.#size = 0;
}
很简单,直接将头指针和尾指针指向的值改为undefined
,size
也设置为0,剩下的就靠JS自身的垃圾回收机制了,本文就不涉及了。
Part44. 学习资源
数组
class
Symbol.iterator
垃圾回收机制
红宝书 7.3 生成器
Part55. 总结 & 收获
复习了 ES6 中的
class
以及相关语法链表和数组的区别,时间复杂度,通过指针的空间 来省下按顺序遍历的时间——一种空间换时间的性能优化策略
JS 实现链表的方法,有了
class
这个语法后,和其他语言差不多了Node
结点,存当前value
以及与用于相邻结点相连的指针
复习
Symbol.iterator
的使用场景 以及 生成器这个平时可能用的较少的知识点
🌊如果有所帮助,欢迎点赞关注,一起进步⛵
················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结。
同时,最近组织了源码共读活动,帮助4000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。
扫码加我微信 ruochuan02、拉你进源码共读群
今日话题
目前建有江西|湖南|湖北 籍 前端群,想进群的可以加我微信 ruochuan12 进群。分享、收藏、点赞、在看我的文章就是对我最大的支持~