资讯专栏INFORMATION COLUMN

JavaScript异步编程解决方案笔记

dmlllll / 2476人阅读

摘要:异步编程解决方案笔记最近读了朴灵老师的深入浅出中异步编程一章,并参考了一些有趣的文章。另外回调函数中的也失去了意义,这会使我们的程序必须依赖于副作用。

JavaScript 异步编程解决方案笔记

最近读了朴灵老师的《深入浅出NodeJS》中《异步编程》一章,并参考了一些有趣的文章。
在此做个笔记,记录并巩固学到的知识。

JavaScript异步编程的两个核心难点

异步I/O、事件驱动使得单线程的JavaScript得以在不阻塞UI的情况下执行网络、文件访问功能,
且使之在后端实现了较高的性能。然而异步风格也引来了一些麻烦,其中比较核心的问题是:

函数嵌套过深

JavaScript的异步调用基于回调函数,当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成
金字塔型结构。这不仅使得代码变难看难懂,更使得调试、重构的过程充满风险。

异常处理

回调嵌套不仅仅是使代码变得杂乱,也使得错误处理更复杂。

异步编程中可能抛出错误的情况有两种:

异步函数错误

由于异步函数是立刻返回的,异步事务中发生的错误是无法通过try-catch来捕捉的,只能采用由调用方提供错误处理回调的方案来解决。
例如Node中常见的function (err, ...) {...}回调函数,就是Node中处理错误的约定:
即将错误作为回调函数的第一个实参返回。
再比如HTML5中FileReader对象的onerror函数,会被用于处理异步读取文件过程中的错误。

回调函数错误

由于回调函数执行时,异步函数的上下文已经不存在了,通过try-catch无法捕捉回调函数内的错误。

可见,异步回调编程风格基本上废掉了try-catch和throw。另外回调函数中的return也失去了意义,这会使我们的程序必须依赖于副作用。
这使得JavaScript的三个语义失效,同时又得引入新的错误处理方案,如果没有像Node那样统一的错误处理约定,问题会变得更加麻烦。

几种解决方案

下面对几种解决方案的讨论主要集中于上面提到的两个核心问题上,当然也会考虑其他方面的因素来评判其优缺点。

Async.js

首先是Node中非常著名的Async.js,这个库能够在Node中展露头角,恐怕也得归功于Node统一的错误处理约定。
而在前端,一开始并没有形成这么统一的约定,因此使用Async.js的话可能需要对现有的库进行封装。

Async.js的其实就是给回调函数的几种常见使用模式加了一层包装。比如我们需要三个前后依赖的异步操作,采用纯回调函数写法如下:

asyncOpA(a, b, (err, result) => {
	if (err) {
		handleErrorA(err);
	}
	asyncOpB(c, result, (err, result) => {
		if (err) {
			handleErrorB(err);
		}
		asyncOpB(d, result, (err, result) => {
			if (err) {
				handlerErrorC(err);
			}
			finalOp(result);
		});
	});
});

如果我们采用async库来做:

async.waterfall([
	(cb) => {
		asyncOpA(a, b, (err, result) => {
			cb(err, c, result);
		});
	},
	(c, lastResult, cb) => {
		asyncOpB(c, lastResult, (err, result) => {
			cb(err, d, result);
		})
	},
	(d, lastResult, cb) => {
		asyncOpC(d, lastResult, (err, result) => {
			cb(err, result);
		});
	}
], (err, finalResult) => {
	if (err) {
		handlerError(err);
	}
	finalOp(finalResult);
});

可以看到,回调函数由原来的横向发展转变为纵向发展,同时错误被统一传递到最后的处理函数中。
其原理是,将函数数组中的后一个函数包装后作为前一个函数的末参数cb传入,同时要求:

每一个函数都应当执行其cb参数;

cb的第一个参数用来传递错误。

我们可以自己写一个async.waterfall的实现:

let async = {
	waterfall: (methods, finalCb = _emptyFunction) => {
		if (!_isArray(methods)) {
			return finalCb(new Error("First argument to waterfall must be an array of functions"));
		}
		if (!methods.length) {
			return finalCb();
		}
		function wrap(n) {
			if (n === methods.length) {
				return finalCb;
			}
			return function (err, ...args) {
				if (err) {
					return finalCb(err);
				}
				methods[n](...args, wrap(n + 1));
			}
		}
		wrap(0)(false);
	}
};

Async.js还有series/parallel/whilst等多种流程控制方法,来实现常见的异步协作。

Async.js的问题是:

在外在上依然没有摆脱回调函数,只是将其从横向发展变为纵向,还是需要程序员熟练异步回调风格。

错误处理上仍然没有利用上try-catch和throw,依赖于“回调函数的第一个参数用来传递错误”这样的一个约定。

Promise方案

ES6的Promise来源于Promise/A+。使用Promise来进行异步流程控制,有几个需要注意的问题,
在We have a problem with promises一文中有很好的总结。

把前面提到的功能用Promise来实现,需要先包装异步函数,使之能返回一个Promise:

function toPromiseStyle(fn) {
	return (...args) => {
		return new Promise((resolve, reject) => {
			fn(...args, (err, result) => {
				if (err) reject(err);
				resolve(result);
			})
		});
	};
}

这个函数可以把符合下述规则的异步函数转换为返回Promise的函数:

回调函数的第一个参数用于传递错误,第二个参数用于传递正常的结果。

接着就可以进行操作了:

let [opA, opB, opC] = [asyncOpA, asyncOpB, asyncOpC].map((fn) => toPromiseStyle(fn));

opA(a, b)
	.then((res) => {
		return opB(c, res);
	})
	.then((res) => {
		return opC(d, res);
	})
	.then((res) => {
		return finalOp(res);
	})
	.catch((err) => {
		handleError(err);
	});

通过Promise,原来明显的异步回调函数风格显得更像同步编程风格,我们只需要使用then方法将结果传递下去即可,同时return也有了相应的意义:
在每一个then的onFullfilled函数(以及onRejected)里的return,都会为下一个then的onFullfilled函数(以及onRejected)的参数设定好值。

如此一来,return、try-catch/throw都可以使用了,但catch是以方法的形式出现,还是不尽如人意。

Generator方案

ES6引入的Generator可以理解为可在运行中转移控制权给其他代码,并在需要的时候返回继续执行的函数。利用Generator可以实现协程的功能。

将Generator与Promise结合,可以进一步将异步代码转化为同步风格:

function* getResult() {
	let res, a, b, c, d;
	try {
		res = yield opA(a, b);
		res = yield opB(c, res);
		res = yield opC(d);
		return res;
	} catch (err) {
		return handleError(err);
	}
}

然而我们还需要一个可以自动运行Generator的函数:

function spawn(genF, ...args) {
	return new Promise((resolve, reject) => {
		let gen = genF(...args);
		
		function next(fn) {
			try {
				let r = fn();
				if (r.done) {
					resolve(r.value);
				}
				Promise.resolve(r.value)
					.then((v) => {
						next(() => {
							return gen.next(v);
						});
					}).catch((err) => {
						next(() => {
							return gen.throw(err);
						})
					});
			} catch (err) {
					reject(err);
			}
		}
		
		next(() => {
			return gen.next(undefined);
		});
	});
}

用这个函数来调用Generator即可:

spawn(getResult)
	.then((res) => {
		finalOp(res);
	})
	.catch((err) => {
		handleFinalOpError(err);
	});

可见try-catch和return实际上已经以其原本面貌回到了代码中,在代码形式上也已经看不到异步风格的痕迹。

类似的功能有co/task.js等库实现。

ES7的async/await

ES7中将会引入async function和await关键字,利用这个功能,我们可以轻松写出同步风格的代码,
同时依然可以利用原有的异步I/O机制。

采用async function,我们可以将之前的代码写成这样:

async function getResult() {
	let res, a, b, c, d;
	try {
		res = await opA(a, b);
		res = await opB(c, res);
		res = await opC(d);
		return res;
	} catch (err) {
		return handleError(err);
	}
}

getResult();

和Generator & Promise方案看起来没有太大区别,只是关键字换了换。
实际上async function就是对Generator方案的一个官方认可,将之作为语言内置功能。

async function的缺点是:

await只能在async function内部使用,因此一旦你写了几个async function,
或者使用了依赖于async function的库,那你很可能会需要更多的async function。

目前处于提案阶段的async function还没有得到任何浏览器或Node.JS/io.js的支持。

Babel转码器也需要打开实验选项,并且对于不支持Generator的浏览器来说,
还需要引进一层厚厚的regenerator runtime,想在前端生产环境得到应用还需要时间。

参考

1. A Study on Solving Callbacks with JavaScript Generators

2. Async Functions

3. 异步操作

4. Promise - JavaScript MDN

5. We have a problem with promises

6. Taming the asynchronous beast with ES7

7. Managing Node.js Callback Hell

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85897.html

相关文章

  • JS笔记

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。异步编程入门的全称是前端经典面试题从输入到页面加载发生了什么这是一篇开发的科普类文章,涉及到优化等多个方面。 TypeScript 入门教程 从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript。 网络基础知识之 HTTP 协议 详细介绍 HTT...

    rottengeek 评论0 收藏0
  • 笔记】 你不知道的JS读书笔记——异步

    摘要:异步请求线程在在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件循环队列中。 基础:浏览器 -- 多进程,每个tab页独立一个浏览器渲染进程(浏览器内核) 每个浏览器渲染进程是多线程的,主要包括:GUI渲染线程 JS引擎线程 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎) JS引擎线程负...

    junnplus 评论0 收藏0
  • 深入理解ES6笔记(十一)Promise与异步编程

    摘要:回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行如果回调过多,会陷入回调地狱基础可以当做是一个占位符,表示异步操作的执行结果。函数可以返回一个,而不必订阅一个事件或者向函数传递一个回调函数。 主要知识点:Promise生命周期、Promise基本操作、Promise链、响应多个Promise以及集成PromiseshowImg(https://segmentfaul...

    RayKr 评论0 收藏0
  • 《深入理解ES6》笔记—— Promise与异步编程(11)

    摘要:为什么要异步编程我们在写前端代码时,经常会对做事件处理操作,比如点击激活焦点失去焦点等再比如我们用请求数据,使用回调函数获取返回值。这些都属于异步编程。回调有多个状态,当响应成功和失败都有不同的回调函数。 为什么要异步编程 我们在写前端代码时,经常会对dom做事件处理操作,比如点击、激活焦点、失去焦点等;再比如我们用ajax请求数据,使用回调函数获取返回值。这些都属于异步编程。 也许你...

    ssshooter 评论0 收藏0

发表评论

0条评论

dmlllll

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<