如何实现一个队列或链表,探究yocto-queue源码

132 阅读5分钟

要阅读的源码

  1. 链接: yocto-queue

先来看一下readme

  1. 总体介绍 1661138607118.png

    1. 你应该使用这个包,当你要对一个大数组做频繁的push和shift操作时,shift方法是线性的时间复杂度的,也就是O(n)而Queue#dequeue方法是固定的时间复杂度,也就是O(1),这时使用队列就很有意义了。
    2. 队列就是一个有序的列表,添加元素向队列部尾部添加,移除元素从头部移除,它遵循先进先出的原则,也就是(FIFO)原则。
  2. API: 1661146162786.png

创建一个可迭代的实例queue,你可以用for...of来遍历它,或者用展开语法...,将其转化成一个数组,但是非必要不要去那么做因为这样很慢。

1661146482924.png

  1. .enqueue(value):在队列里新增一个值。
  2. .dequeue():移除队列里的下一个值,返回移除的值或者当队列为空时,返回undefined。
  3. .clear():将队列情况
  4. .size:获取队列的大小

开始调试

clone完毕项目后,用vscode打开项目,下载依赖后,打开package.json文件。

1661147317831.png

点击红色箭头的debug,就可以开始调试了,当然别忘了在index.js上打上断点。

分析:

经过断点调试,差不多就知道整个程序想要做什么了。下面就一个一个来分析

  1. Class Node
class Node {
 value;
 next;

 constructor(value) {
 	this.value = value;
 }
}

一个名称为Node的class,它所创建的实例会成为队列的一个节点的值,value存储着enqueue进去的值,next则存放下一个节点的值,数据结构如下{value:1,next:{value:2,next:undefined}}

  1. 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;
		}
	}
}

  1. yocto包的运行机制

    1. 作者的注释如下

    1661150182336.png

    我的理解:#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 } } }
    

其他:

  1. 私有变量可以通过一下方式访问
    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());
    
  2. 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
    
    }
    

总结:

  1. 队列在处理超大数组时会比较高效。

  2. class的私有变量确保了它不会被作用域外操作的更改。

  3. Symbol.iterator能够为对象定义一个迭代器,数组,Map,set都定义了默认的迭代器。

  4. yield能暂停生成器的执行,它的功能类似于await,可以用到异步操作上。

  5. function* 生成器函数,会返回一个迭代对象。