重学迭代器和生成器

news/2024/11/7 16:58:47/

重学迭代器和生成器

之前在 JavaScript 高级程序设计第 7 章 迭代器和生成器 学习笔记 其实包含过 iterator 和 generator 的学习笔记,不过依旧温故而知新,有了一些实际上手的经验后重新再回滚一边会有比较深刻的理解,而不是只是 cv 书上的内容。

这里丢一个 generator 实现无限拉取的效果,图在这里,代码在最后:

在这里插入图片描述

大抵效果是先加载一部分的文章/视频内容,数量可以由后端控制,如之前复刻 yt 的时候,好像有从 API 中注意到拉取视频的数量其实是由后端控制的:

在这里插入图片描述

如果是自己实现的话,思路大抵是这样的:用户在与前端有交互后(比如说点击 load more,或者用滚轮继续往下拉,通过 loading spin 进行更多拉取),通过 generator 获取下一部分的信息后,渲染到页面上。

protocols

这里说到的 protocol 有三(四)个:

  • iterator protocol

    要满足 iterator protocol,那么就必须要实现对象上的 next() 方法

    next() 返回的对象类型为:

    interface IteratorReturnResult<TReturn> {done: true;value: TReturn;
    }interface IteratorYieldResult<TYield> {done?: false;value: TYield;
    }type IteratorResult<T, TReturn = any> =| IteratorYieldResult<T>| IteratorReturnResult<TReturn>;
    
  • iterable protocol

    iterable 必须实现 @@iterator 方法

    @@iterator 的返回值如下:

    interface Iterator<T, TReturn = any, TNext = undefined> {// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.next(...args: [] | [TNext]): IteratorResult<T, TReturn>;return?(value?: TReturn): IteratorResult<T, TReturn>;throw?(e?: any): IteratorResult<T, TReturn>;
    }
    

    换言之,iterable protocol 的实现是必须基于 iterator protocol 上实现的

  • aync iterator protocol & async iterable protocol

    其实现方法大体与上面的没什么区别,不过需要实现的是 @@asyncIterator 方法而非@@iterator 方法

iterator

如果使用过其他的编程语言,应该会 iterator 不会太陌生。对可以使用 for...of 的对象来说,其 prototype chain 上必然有一个对象是实现了 @@iterator 方法的。

换言之,需要满足两个需求:

  1. 实现一个 next() 方法
  2. 实现 [Symbol.iterator] 方法

基础用法如下:

const arr = [1, 2, 3, 4, 5, 6];
const iterator = arr[Symbol.iterator]();
let res = iterator.next();console.log(res);res = iterator.next();console.log(res);

在这里插入图片描述

可以看到,比起循环来说,iterator 的一个好处在意可以通过程序去暂停和继续迭代的过程。比如说一个使用案例可能是视频的片段播放。现在很少有视频是整个下载下来的,基本上都是播放到某个锚点的时候去抓下一段视频。这个时候就可以通过 iterator 去进行执行。又或者需求可能是要创建一个无限循环的 iterator,这点如果要使用 loop,那就只能用 while (true)for(;;) 去执行,但是这样逻辑也就只能添加到循环体内,对于后期的维护非常困难。

其实现的方法有如下:

class Counter {// 设定上限 和 下限constructor(limit) {this.counter = 1;this.limit = limit;}// 满足 即迭代的自我识别能力// 实现 迭代需要执行的方法// 满足 迭代器协议的实现方法—— next()next() {if (this.counter <= this.limit) {return { done: false, value: this.counter++ };} else {return { done: true, value: undefined };}}// 实现 可迭代协议 第2点// 即 Symbol.iterator 的实现[Symbol.iterator]() {return this;}
}let counter = new Counter(3);// for of 会调用迭代器方法
for (let i of counter) {console.log(i);// 1// 2// 3
}

这个方法的问题就在于,当迭代器走到尽头后,再次调用迭代器不会的结果也是 { done: true, value: undefined }。为了解决这个问题,其中一个实现方法是使用 closure:

class Counter {constructor(limit) {this.limit = limit;}[Symbol.iterator]() {let count = 1,limit = this.limit;return {// 通过闭包,每次调用 迭代器 时会生成一个新的计时器next() {if (count <= limit) {return { done: false, value: count++ };} else {return { done: true, value: undefined };}},};}
}

这样,每次调用 counter.[Symbol.iterator]() 都会产生一个新的 count,并且该方法也可以被复用。

iterator 的终止和报错

重新回顾一下 iterator 的返回值:

interface Iterator<T, TReturn = any, TNext = undefined> {// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.next(...args: [] | [TNext]): IteratorResult<T, TReturn>;return?(value?: TReturn): IteratorResult<T, TReturn>;throw?(e?: any): IteratorResult<T, TReturn>;
}

除了必须要实现的 next 之外,还有两个可以选的 returnthrow,两个处理方式是针对 iterator 的中断实现的操作。依旧用上面的 Conter 为例:

class Counter {constructor(limit) {this.limit = limit;}[Symbol.iterator]() {let count = 1,limit = this.limit;console.log(count);return {// 通过闭包,每次调用 迭代器 时会生成一个新的计时器next() {if (count <= limit) {return { done: false, value: count++ };} else {return { done: true, value: undefined };}},return(value) {console.log('Finished iterator early');return {done: true,value,};},throw(e) {console.log('error thrown', e);return {done: true,value: e,};},};}
}

调用方法如下:

const counter = new Counter(5);for (const val of counter) {console.log(val);if (val > 2) break;
}try {for (const val of counter) {if (val > 2) throw new Error('terminated');}
} catch (e) {}const iter = counter[Symbol.iterator]();
iter.throw('Error occurred');

在这里插入图片描述

需要注意的是,在 for...of 中使用 breakthrow 最后触发的都是 return 而非 throw

注 ⚠️: 因为 return 是可选的,因此不是所有的 iterator 都可以被关闭,如 Array 的就不可以。

generator

generator 是一种特殊的 iterator,它所实现的方法是实现一个 带有 * 的非箭头函数:function* funcName() {},另外,* 两侧不受空格影响,因此 function * funcName(){}, function *funcName(){} 都是合法语法。

因为 generator 本身就是一种另类的 iterator,所以使用方法上来说是一致的:

function* generator() {}const g = generator();console.log(g === g[Symbol.iterator]()); // true

在这里插入图片描述

以及定义:

interface Generator<T = unknown, TReturn = any, TNext = unknown>extends Iterator<T, TReturn, TNext> {next(...args: [] | [TNext]): IteratorResult<T, TReturn>;return(value: TReturn): IteratorResult<T, TReturn>;throw(e: any): IteratorResult<T, TReturn>;[Symbol.iterator](): Generator<T, TReturn, TNext>;
}

关键词 yield

虽然 generator extends 了 iterator,不过在实际开发场景中,很少会手动实现 next,而是使用 yield 去进行控制。具体流程为:

  • JS 执行 generator 中的代码
  • JS 遇到 yield 关键字后停止执行,但是相关联的作用于会被保留
  • 开发调用 g.next() 后,JS 返回 yield 后的值
  • 重复循环操作
  • 当没有可以 yield 的值后,generator 的返回值被改为 {value: undefined, done: true},并且会维持在这个状态

依旧以上面使用的 Counter 为例,对比一下 generator 的实现:

class Counter {constructor(limit) {this.limit = limit;}*generator() {let count = 1;try {while (count < this.limit) {yield count++;}} catch (e) {yield 'Error occurred';} finally {yield 'Generator done';}}
}const counter = new Counter(5);
let iter = counter.generator();
for (const value of iter) {console.log(value);
}

可以看到,generator 的实现稍微简单一些,但是,只是简单的 loop 所有的返回值,会出现结尾多一个 finally 中处理的值:

在这里插入图片描述

这里可能就会要求开发手动进行一些的判断,保证“错误”的值不会被显示出来。

可以接受参数

与普通的 iterator 不同,generator 其实是可以接受参数的,如:

  *generator() {let count = 1;let nextCounter;try {while (count < this.limit) {nextCounter =yield `current counter: ${count++}, nextCounter is: ${nextCounter}`;}} catch (e) {console.log(e);yield 'Error occurred';} finally {yield 'Generator done';}}while (true) {const { value, done } = iter.next(anotherCounter++);console.log(anotherCounter);console.log(value);if (done) break;
}

在这里插入图片描述

yield 可以接受从 next 中传进来的参数,这也让 generator 的使用更加的灵活。

yield 一个可迭代对象

这个写法也是这次复习的时候才看到的,前面真的囫囵吞枣,没看的特别仔细就直接跳过去了:

function* generator() {yield* [1, 2, 3, 4, 5];// 等同于// yield 1// yield 2// yield 3
}const g = generator();for (const val of g) {console.log(val);
}

在这里插入图片描述

开始的案例

这里主要实现的是 asyncIterator,HTML 部分主要就是一点点的 CSS 和 button,这里不多赘述。

JS 如下:

class Posts {wait(delay) {return new Promise((resolve) => {setTimeout(resolve, delay);});}// 实现 asyncIterator// 这里虽然用不到,不过实现了 asyncIterator 应该也可以使用 for await...of 的语法async *fetchPosts() {let id = 1;// while (true) 为必须条件,否则 generator 在没有可以 yield 的东西后就会被关闭while (true) {await this.wait(500);const post = (await fetch(`https://dummyjson.com/posts/${id}`)).json();yield post;id++;}}
}const posts = new Posts();
const iter = posts.fetchPosts();
const postsList = document.getElementById('posts');// UI 相关
const createPost = ({ id, body, title }) => {const postItem = document.createElement('li');postItem.id = id;const article = document.createElement('article');const titleEl = document.createElement('header');const paragraph = document.createElement('p');titleEl.innerHTML = title;paragraph.innerHTML = body;article.appendChild(titleEl);article.appendChild(paragraph);postItem.appendChild(article);postsList.appendChild(postItem);
};// 先拉取几个post做demo
(async () => {for (let i = 0; i < 4; i++) {const res = await iter.next();createPost(res.value);}
})();const fetchBtn = document.getElementById('fetch');
// 点击触发拉取事件
fetchBtn.addEventListener('click', async () => {const res = await iter.next();createPost(res.value);
});

保证 generator 一直是开着的状态对于无限拉取还是很重要的,否则 generator 关闭后就是这个状态:

在这里插入图片描述

这个情况下继续调用 generator.next() 并不会报错,只是返回值永远都是 {value: undefined, done: true}。因此在实际使用 generator 进行开发的时候,也是需要对返回值——特别是 done——进行一个判断。

reference

  • Use-Cases For JavaScript Generators

  • Redux Toolkit + React + TS + Tailwind CSS 复刻 YouTube 学习心得

  • 重学 Symbol


http://www.ppmy.cn/news/97933.html

相关文章

docker版jxTMS使用指南:python服务之jxLocalStateMachine

本文讲解4.0版jxTMS中python服务的jxLocalStateMachine模块&#xff0c;整个系列的文章请查看&#xff1a;docker版jxTMS使用指南&#xff1a;4.0版升级内容 docker版本的使用&#xff0c;请参考&#xff1a;docker版jxTMS使用指南 jxLocalStateMachine提供了一个简单可靠的有…

Oracle数据库从入门到精通系列之十二:段

Oracle数据库从入门到精通系列之十二:段 一、段1.聚簇2.表3.表分区4.索引5.索引分区6.LOB分区、LOB子分区、LOB索引、LOB段7.嵌套表8.回滚段和Type2 undo段二、总结表、索引、段之间关系三、基于创建表实例研究表、索引、段之间关系一、段 Oracle数据库中的段(segment)是占用…

进程间通信之信号

进程间通信之信号 1. 信号2. 信号由谁产生?3. 有哪些信号4. 信号的安装5. 信号的发送1) 使用kill函数2)使用alarm函数3) 使用raise6.发送多个信号7. 信号集1. 信号 什么是信号? 信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有信号…

【算法】单调栈问题

文章目录 题目思路分析代码实现 题目 给定一个不含有重复值的数组arr&#xff0c;找到每一个i位置左边和右边离i位置最近且值比arr[i]小的位置&#xff0c;返回所有位置相应的消息。 比如arr{3&#xff0c;4&#xff0c;1&#xff0c;5&#xff0c;6&#xff0c;2&#xff0c;…

排序算法的时间复杂度、空间复杂度对比总结

参考&#xff1a;八大排序算法的稳定性和时间复杂度

【C语言】二分查找(含图解)

文章目录 1. 二分查找思想2. 代码实现2.1 未封装函数2.2 封装函数&#xff08;使用while循环&#xff09;2.3 封装函数&#xff08;使用递归&#xff09; 1. 二分查找思想 二分法&#xff1a;二分查找算法是一种在有序数组中查找某一特定元素的搜索算法&#xff0c;其思想就是…

.NET面向AI编程——SK框架(SemanticKernel)的简易入门实践

前言&#xff1a;随着ChatGPT开始在各个领域遍地开花&#xff0c;有关的应用也开始层出不穷。随着微软开源SK框架&#xff0c;无疑是给.NET开发者带来面向AI应用的新机遇。以下内容&#xff0c;通过创建一个简单的prompt开始&#xff0c;开发一个使用SK框架进行开发的入门教程&…

Typescript 5.0 发布:快速概览

探索最令人兴奋的功能和更新 作为一种不断发展的编程语言&#xff0c;TypeScript 带来了大量的改进和新功能。在本文中&#xff0c;我们将深入探讨 TypeScript 的最新版本 5.0&#xff0c;并探索其最值得关注的更新。 1. 装饰器 TypeScript 5.0 引入了一个重新设计的装饰器系…