重学迭代器和生成器
之前在 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
方法的。
换言之,需要满足两个需求:
- 实现一个
next()
方法 - 实现
[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
之外,还有两个可以选的 return
和 throw
,两个处理方式是针对 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
中使用 break
和 throw
最后触发的都是 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