在本文中,我们将介绍JavaScript以及如何在不使用任何第三方库的情况下编写结构良好,易于理解的异步代码并不困难。
JavaScript有两种处理异步任务的主要方法 - 回调和Promises。通常,Promises被认为比回调更容易使用和维护。但是,实际上,即使是Promises也不会让你开心。异步代码可能仍然很难阅读和理解。因此,像 co这样的第三方库提供了编写类似同步的异步代码的方法。
我个人更喜欢世界上的所有东西都像redux-saga一样清晰美丽。但并不是每个人都很幸运能够使用React和Redux来使用传奇。在本文中,我将展示在现代JavaScript中,不使用任何第三方库编写结构良好且易于理解的异步代码并不困难。
回调方法
让我们从一个例子开始吧。假设我们有一个可以从流中读取一些数据的对象,该对象使用事件发射器来通知每个对这些事件感兴趣的人。该事件是start, data,stop ,使事情有点复杂,pause。
因此,我们希望start 在收听事件时捕获我们想要 开始获取和存储数据的data 事件。在 stop 活动中我们需要执行一些数据处理。在 pause 事件中,我们停止等待下一个data 事件并等待start 继续获取和存储数据。
这是代码:
let data = '';
const handleStart = () => {
streamReader.removeAllListeners('pause', handlePause);
streamReader.on('data', (chunk, err) => {
if (err) {
console.error(err);
streamReader.removeAllListeners('data');
streamReader.removeAllListeners('pause');
return;
}
data += chunk;
})
}
const handleStop = () => {
streamReader.removeAllListeners('data');
streamReader.removeAllListeners('pause');
streamReader.removeAllListeners('stop');
processData(data, (err, result) => {
if (err) {
console.error(err);
return;
}
storeResult(result, () => {
console.log('Stored')
})
});
}
const handlePause = () => {
streamReader.removeAllListeners('data');
streamReader.on('start', handleStart);
}
streamReader.once('start', handleStart);
streamReader.on('stop', handleStop)
streamReader.on('pause', handlePause);
这里我们有一堆事件监听器和事件处理程序来实现上述流程。还有一些函数被调用processData , storeData 它们执行一些异步操作并在完成时调用回调。
这段代码有什么问题?嗯......我想,这是一场彻头彻尾的噩梦。首先,有一个全局变量 data,这是不可能摆脱的。另外,我在上面提到了一个流程,但代码中没有流程。很难理解动作的顺序,因此调试起来非常困难。人们并没有把它称为“回调地狱”。
出路
JavaScript中异步回调的好处在于,如果您不想使用它,则不必使用它们。任何回调都可能变成Promise。最简单的例子如下所示:
const processDataPromise = new Promise((resolve, reject) => {
processData(data, (err, result) => {
if (err) reject(err);
resolve(result);
});
})
或者更通用的解决方案:
function promisify(f, context, isEvent) {
const ctx = context || this;
return function () {
return new Promise((resolve, reject) => {
f.call(ctx, ...arguments, (...args) => {
const err = arguments ? args.find((a) => a instanceof Error) : null;
if (err) {
reject(err);
} else {
if (isEvent) {
resolve({
type: arguments[0],
cbArgs: [...args],
});
} else {
resolve([...args]);
}
}
})
});
}
}
一般的解决方案当然不会立即明确,所以让我解释一下。
该函数promisify 将异步函数作为第一个参数,并返回一个函数,该函数除了回调之外,采用与原始参数相同的所有参数。调用此返回函数时,它将返回Promise。原始函数在Promise内部调用,在原始函数调用回调时解析。如果原始函数具有上下文(context 参数promisify),则在Promise中调用时它将绑定到它。如果原始函数只是一个普通的异步函数,我们用回调的参数解析Promise。如果它是一个事件监听器(isEvent = true),我们返回事件类型和回调参数。如果回调被调用和错误,则Promise被拒绝。
应用程序promisify 看起来像这样:
const processDataPromise = promisify(processData);
const storeResultPromise = promisify(storeResult);
const onEventPromise = promisify(emitter.once, emitter, true);
Promise可以这样使用:
processDataPromise(data).then(([err, processedData]) => {
/* do something with the data*/
}
但还有更好的方法。
传奇般的天堂
我们确实需要一种更好的方法,因为将上述流程压缩到Promise链中几乎是不可能的。
更好的方法是JavaScript async 函数,这是同一流程的另一个实现:
async function readStream(streamReader, initialData) {
const processDataPromise = promisify(processData);
const storeResultPromise = promisify(storeResult);
const onEventPromise = promisify(emitter.once, emitter, true);
await onEventPromise('start');
let data = initialData || '';
while (true) {
try {
const event = await Promise.race([
onEventPromise('data'),
onEventPromise('stop'),
onEventPromise('pause'),
]);
const {type} = event;
if (type === 'data') {
const [chunk] = event.cbArgs;
data += chunk;
}
if (type === 'pause') {
func(streamReader, data);
break;
}
if (type === 'stop') {
const [err, processedData] = await processDataPromise(data);
await storeResultPromise(processedData);
return processedData;
}
} catch (err) {
handleError(err);
return;
}
}
}
嗯,这个实现有什么好处?首先,它看起来更漂亮。其次,更重要的是,此代码中有一个流程。它几乎就像流程图一样,您可以逐步跟踪每个循环和每个分支的整个序列。它看起来像我在开始时提到的一个传奇,但是没有必要知道有关redux-saga的任何事情来编写这样的代码。更重要的是,它是您可以使用的代码。
本文暂时没有评论,来添加一个吧(●'◡'●)