重学迭代器和生成器
之前在 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