4、Generator / yield
JavaScript Generator
是一个相对较新的概念,它们在ES6(也被称为ES2015)中被引入。它是一种特殊类型的函数,它可以被暂停和恢复运行,并且可以通过 yield
语句向调用方返回数据。结合 Promise
使用,可以实现异步编程。
先来看一个简单的例子:
function* foo () { var index = 0; while (index < 2) { yield index++; } } var bar = foo(); console.log(bar.next()); // { value: 0, done: false } console.log(bar.next()); // { value: 1, done: false } console.log(bar.next()); // { value: undefined, done: true }
当 Generator
函数执行时,会返回一个迭代器对象,该迭代器对象可以通过 next()
方法向下执行生成器函数。在 Generator
函数中,通过 yield
关键字可以将代码的执行权交回给调用者,并将 yield
后的表达式作为参数传递给调用者。这样就实现了协作式多任务处理。
可以看到,generator
函数有一个最大的特点,可以在内部执行的过程中交出程序的控制权,yield
相当于起到了一个暂停的作用;而当一定情况下,外部又将控制权再移交回来。
想象一下,我们用generator
来封装代码,在异步任务处使用yield
关键词,此时generator
会将程序执行权交给其他代码,而在异步任务完成后,调用next
方法来恢复yield
下方代码的执行。以readFile为例,大致流程如下:
// 我们的主任务——显示关键字 // 使用yield暂时中断下方代码执行 // yield后面为promise对象 const showKeyword = function* (filepath) { console.log('开始读取'); let keyword = yield readFile(filepath); console.log(`关键字为${filepath}`); } // generator的流程控制 let gen = showKeyword(); let res = gen.next(); res.value.then(res => gen.next(res));
在主任务部分,原本readFile
异步的部分变成了类似同步的写法,代码变得非常清晰。而在下半部分,则是对于什么时候需要移交回控制权给generator
的流程控制。
然而,我们需要手动控制generator
的流程,如果能够自动执行generator
——在需要的时候自动移交控制权,那么会更加具有实用性。
为此,我们可以使用 co 这个库。它可以是省去我们对于generator
流程控制的代码
const co = reuqire('co'); // 我们的主任务——显示关键字 // 使用yield暂时中断下方代码执行 // yield后面为promise对象 const showKeyword = function* (filepath) { console.log('开始读取'); let keyword = yield readFile(filepath); console.log(`关键字为${filepath}`); } // 使用co co(showKeyword);
其中,yeild
关键字后面需要是functio
, promise
, generator
, array
或object
。可以改写文章一开始的例子:
const co = reuqire('co'); const task = function* (filepath) { let keyword = yield readFile(filepath); let count = yield queryDB(keyword); let data = yield getData(res.length); console.log(data); }); co(task, './sample.txt');
Generator + Promise
Generator
函数与 Promise
结合使用时,可以通过 yield
关键字桥接两者之间的通信。在 Generator
函数中通过 yield
语句返回一个 Promise
对象,在 Promise 对象成功或失败后,再次通过 Generator
函数的 next()
方法恢复生成器的执行。在执行过程中,可以通过 try...catch
捕获错误并进行相应的处理。
通过以上方式,利用 Generator
和 Promise
的协作,可以有效解决 JavaScript 中异步编程的问题,提高代码可读性和可维护性。
我们以一个简单的示例来说明 Generator
如何实现异步编程。假设我们有一个包含异步操作的函数 getData()
,它返回一个 Promise
对象。
function getData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('Data received from server'); }, 2000); }); }
接下来,我们创建一个 Generator
函数 run()
,在该函数中实现异步操作的流程控制。程序执行到 yield
语句时,run()
函数会暂停执行并将控制权交还给调用者。当 Promise 对象成功返回数据后,再次通过 next()
方法恢复 Generator
的执行。在中间过程中,可以通过 try...catch
捕获错误并进行相应的处理。
function* run() { try { const data = yield getData(); console.log(data); } catch (error) { console.error(error); } } const iterator = run(); const promise = iterator.next().value; promise.then(data => { iterator.next(data); }).catch(error => { iterator.throw(error); });
首先,我们需要创建一个 Generator
实例 iterator
,并通过 iterator.next()
方法启动迭代器,开始执行 run()
函数。此时程序执行到 yield
语句,run()
函数暂停执行并将控制权交还给调用者。
之后,我们获取到 promise
对象,通过 promise
对象的 then()
方法注册回调函数,当数据成功返回时则触发回调函数,恢复 Generator
的执行,并将返回值作为参数传递给 yield
表达式。这样,我们就可以拿到异步操作得到的数据,并进行下一步处理。
当然,在中间过程中如果发生任何错误,我们可以通过 promise
对象的 catch()
方法或 iterator.throw()
方法捕获异常并进行相应的处理。
总之,将 Generator
与 Promise
结合使用,利用 yield
语句返回 Promise
对象,再通过 next()、then()、catch()、throw()
等方法实现异步编程的流程控制。
5、async/await
上面的方法虽然都在一定程度上解决了异步编程中回调带来的问题。然而:
- 事件发布/监听方式模糊了异步方法之间的流程关系;
Promise
虽然使得多个嵌套的异步调用能够通过链式的API进行操作,但是过多的then
也增加了代码的冗余,也对阅读代码中各阶段的异步任务产生了一定干扰;- 通过
generator
虽然能提供较好的语法结构,但是毕竟generator
与yield
的语境用在这里多少还有些不太贴切。
Async/await
语法是一种在 JavaScript 中处理异步操作的更现代的方式,它提供了一种更简洁的编写异步代码的方式。这是一个例子:
async function fetchData(url) { let response = await fetch(url); if (!response.ok) { throw new Error('Network response was not ok'); } let data = await response.json(); console.log(data); } fetchData('https://jsonplaceholder.typicode.com/todos/1') .catch(error => { console.error(error); });
在这个例子中,我们定义了一个名为fetchData()
的异步函数,使用 await
关键字来等待 fetch()
调用的响应。如果响应是不确定的,我们就抛出一个错误。否则,我们将响应解析为JSON,并将其记录到控制台。
然后我们用一个URL调用 fetchData()
函数,并捕捉任何可能被抛出的错误。
与回调和承诺相比,Async/await
语法为处理异步操作提供了一种更易读、更少出错的方式,并且可以与现代功能如结构化和对象速记符号一起使用。
三、使用 RxJS 进行响应式编程
RxJS
是 Reactive Extensions for JavaScript
的缩写,是一种基于可观察对象的异步编程实现方式。RxJS
提供了丰富的操作符和函数,让开发者可以方便地处理异步事件、流数据等场景。
来看一个例子:
const { from } = rxjs; const { filter, map } = rxjs.operators; from(fetch('https://jsonplaceholder.typicode.com/todos/1')) .pipe( filter(response => response.ok), map(response => response.json()) ) .subscribe(data => { console.log(data); }, error => { console.error(error); });
在这个例子中,我们使用 RxJS
的 from()
方法,从 fetch()
调用中创建一个可观察变量。然后我们使用 filter()
和 map()
操作符来转换该观察变量,过滤掉非OK响应,并将响应解析为JSON。
然后我们订阅 observable
,将数据记录到控制台或捕捉任何可能被抛出的错误。
使用 RxJS
的响应式编程为处理异步数据流提供了一种强大的方式,既可读又可维护,特别是在处理大型复杂数据集时。
参考资料
- The Evolution of Asynchronous JavaScript
- Master Asynchronous JavaScript in 2023: A Comprehensive Guide
- Understanding Asynchronous JavaScript
- Asynchronous Programming in JavaScript – Guide for Beginners
- The Evolution of Asynchronous Data Fetching in JavaScript
end~