【若川视野 x 源码共读】第32期| yocto-queue:链表数据结构的Array

78 阅读4分钟

1. 前言

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 的使用场景
  • 调试源码

2. 准备

2.1 了解API

import Queue from 'yocto-queue';

const queue = new Queue();

queue.enqueue('🦄');
queue.enqueue('🌈');

console.log(queue.size);
//=> 2

console.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

返回队列的大小

3 看看 源码

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也可以即可开始调试源码,其实这个代码复杂度不手动调试也可以的,但是通过调试可以让你很明确地看到哪一步代码用到了哪里的东西

源码解析

// 定义一个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;
        }
    }
}

参考文章链接

前舟 juejin.cn/post/709165… molychn juejin.cn/post/709538…

4. 学习资源

5. 总结 & 收获

  • 复习了 ES6 中的 class以及相关语法

  • 链表和数组的区别,时间复杂度,通过指针的空间 来省下按顺序遍历的时间——一种空间换时间的性能优化策略

  • JS 实现链表的方法,有了class这个语法后,和其他语言差不多了

    • Node结点,存当前value以及与用于相邻结点相连的指针
  • 复习 Symbol.iterator 的使用场景 以及 生成器这个平时可能用的较少的知识点

🌊如果有所帮助,欢迎点赞关注,一起进步⛵