先记住这个答案
生成器对象除了 next(),还有 return() 和 throw() 两个方法,都能从外部改变它的执行。it.return(value) 让生成器在当前暂停的 yield 处像执行了 return value 一样结束,如果外面包着 try...finally,finally 会先执行,最后返回 { value, done: true }。it.throw(err) 让暂停的 yield 表达式变成抛出 err,生成器内部有 try...catch 就能接住并继续执行下去;没接住的话,错误会从 it.throw() 这一行抛到外面,生成器也跟着结束。
return(v)在暂停处执行return v,会先走finallythrow(e)在暂停处抛错,内部能catch就继续- 没被
catch的错误从it.throw()这一行抛出来 for...of里break会自动调用return()
先抛一个错误,再停掉它
把两个方法放在一个例子里,顺序看执行结果。
throw 被内部接住,return 触发 finallyJavaScriptfunction* worker() {
try {
yield '准备好了'
} catch (err) {
yield `接住了:${err.message}` // 接住后还能继续 yield
} finally {
console.log('清理资源')
}
return '正常结束'
}
const it = worker()
console.log(it.next().value)
console.log(it.throw(new Error('网络断了')).value)
console.log(JSON.stringify(it.return('被外部停止')))
console.log(JSON.stringify(it.next())) // 已经结束查看输出与解释
准备好了
接住了:网络断了
清理资源
{"value":"被外部停止","done":true}
{"done":true}第一次 next 跑到 yield '准备好了' 停住。it.throw() 相当于让这个 yield 抛出错误,被 catch 接住,生成器没有结束,继续跑到 catch 里的 yield 又停住,所以 throw() 的返回值就是这次 yield 的值。
it.return('被外部停止') 在 catch 里的 yield 处插入了 return,要离开 try 结构就必须经过 finally,所以先打印「清理资源」,然后返回 done: true。函数体末尾的 return '正常结束' 永远不会执行。最后一行输出只有 done,是因为结束后 next() 返回的 value 是 undefined,被 JSON.stringify 省略了。
没被接住的错误去了哪
如果生成器暂停的位置外面没有 try...catch,it.throw(err) 注入的错误就会从生成器里冒出来,从调用 it.throw() 的地方抛出,调用方需要自己 try...catch。这时生成器已经结束,之后 next() 只会返回 done: true。
- 生成器还没开始时调用
- 如果创建完生成器一次
next()都没调用,就直接调it.throw(err),函数体还没进入try,内部的catch根本不会生效,错误会直接抛给调用方。it.return(v)同理,直接返回{ value: v, done: true },finally也不会执行。 finally里再yield会怎样finally里如果写了yield,调用it.return(v)时会先停在这个yield上,返回的是done: false,还要再调一次next()才真正结束并拿到v。这会让「停止」变成两步,清理代码里别写yield。
容易答错的地方
- 以为
return()会跳过finally return()走的是正常的退出流程,和函数体里写return一样要经过finally。这正是它的价值:外部提前停止时,生成器里打开的资源依然能被释放。- 以为
throw()一定会让生成器结束 - 内部接住了错误,生成器就会继续运行,
throw()的返回值就是下一个yield的结果。只有错误没被接住时,生成器才会结束。
面试官还会怎么问?
for...of 中途 break,生成器会怎样?
for...of 在 break、return 或循环体抛错提前退出时,会自动调用迭代器的 return()。所以生成器的 finally 会执行,可以在这里关闭连接、释放锁。循环正常跑完则不会调用 return(),因为生成器自己已经结束了。
return() 能取消生成器里正在进行的网络请求吗?
不能。return() 只是让生成器在暂停点结束,已经发出去的请求不受影响。要取消请求,得在 finally 里调用对应的取消手段,比如 controller.abort()。
redux-saga 里是怎么用这两个方法的?
saga 中间件会在 yield 出去的 Promise 失败时调用 it.throw(err),所以 saga 里可以用普通的 try...catch 处理异步错误。任务被取消时则调用 it.return(),finally 里可以用 cancelled() 判断是不是被取消的。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。