我们知道Javascript是单线程的,同一时间只能执行一个任务,这就意味着即使一个任务耗时很长,下一个任务也只能等下去,整个程序就像卡死了一样。为了解决这个问题,Javascript将任务分为同步任务与异步任务:同步任务会阻塞程序,只有一个任务执行完才会开始下一个任务;异步任务则是非阻塞的,异步任务没有完成的时候下一个任务就可以开始执行,常见的异步任务有setTimeout,Ajax操作等。
由于异步不会阻塞程序,因此从设计上说更有优势。然而异步会使得程序的流程控制变得麻烦,如何编写出优雅的异步代码一直困扰着人们,下面介绍几种常见的异步编程方法。
callback
这是大家最熟悉的异步编程方式了,例如我们在setTimeout里面就会指定一个回调函数,在时间到的时候执行;又或者在发起Ajax请求的时候也会指定一个回调函数,在请求返回的时候执行。
|
|
- 优点
- 简单、容易理解
- 非常容易部署到自己的库/函数中
- 缺点
- 容易导致嵌套过深,造成所谓回调地狱,导致代码高度耦合,可读性差
- 每次只能传递一个回调函数,有很大局限
- 不容易进行错误处理,一是因为异步函数是立即返回的,异步事物中发生的错误无法通过try-catch捕获;二是回调函数执行时异步函数的上下文已经不存在了
generator
generator是es6新提出的一个东西,可以用于异步编程。不过目前看来这只是一个过渡方案,了解即可,未来还是属于下面会提到的promise。
generator跟普通函数很像,区别有两点:
- 声明的时候需要在
function
后面加一个*
。就像这样:function* generator(){}
- 内部可以使用yield语句,这个语句只能用于generator
调用generator的时候并不会立即执行函数,而是返回一个迭代器,调用这个迭代的next方法可以遍历到函数内部的下一个状态,即运行到下一个yield。
|
|
显然yield和return出来的值会出现在value字段中,可以通过done字段知道是否已经遍历完。
另外我们也可以使用for..of
循环来遍历,不过此时不会遍历到return的值。
虽然yield只能用在generator里面,不过generator不一定要有yield。没有yield的generator相当于一个暂缓执行函数。
另外要注意一点:,yield只能用在generator函数中,因此要注意不要讲yield放在generator里面的回调函数中。
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
所谓函数体内外的数据交换就是指既可以通过next方法返回的对象的value字段将数据传递出来,也可以通过给next方法传参把数据传递到函数内部。next方法的参数会作为该实例内部上一个yield语句的返回值,如不通过next方法传值,yield语句的返回值总是undefined。
generator函数内部也能捕获函数体外抛出的错误。Generator实例的throw方法可以在函数体外抛出错误,并在函数体内捕获,但是同时只能捕获一条错误。如果函数体内没有使用try catch,没有捕获外部throw方法抛出的错误,那么该错误也能在外部被捕获。
这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
说了这么多,下面谈谈如何使用generator来做异步编程。下面的例子来自阮一峰的Generator 函数的含义与用法。
|
|
代码中首先『实例化』了一个generator函数,然后调用next方法,即执行函数内部的fetch,由于fetch返回的是一个promise,因此要用then方法执行下一个next方法。
可以看到使用了generator函数的确能让异步操作变得简洁,然而流程控制依然不方便。generator用于异步编程终究是一个过渡方案,下面要介绍的promise+async/await才是『终极』的解决方案。
promise+async/await
Promise是什么这里就不介绍了,不了解的可以看阮一峰的es6入门。这里具体讲讲async/await。
先看一个例子
|
|
- async放在function前面用于修饰,代表这是一个async函数,await只能用在async修饰的函数里面。
- await代表这里等待一个promise对象返回,函数的执行会暂停在这里,知道promise返回了结果。
明显这样的写法很像同步写法,十分优雅。
另外还有一些要注意的:
通过await可以获得promise resolve出来的值,而不必通过then
1234var start = async function () {let result = await sleep(3000);console.log(result); // 收到 ‘ok’};通过await也能直接通过try catch捕获错误
1234567891011var start = async function () {try {console.log('start');await sleep(3000); // 这里得到了一个返回错误// 所以以下代码不会被执行了console.log('end');} catch (err) {console.log(err); // 这里捕捉到错误 `error`}};await也能写在循环里,并且因为类似同步代码,使用await不必担心出现以往需要用闭包解决的问题。使用循环时需要注意,await只能在async修饰的函数里面执行,不要在forEach里面的函数使用await。
123456var start = async function () {for (var i = 1; i <= 10; i++) {console.log(`当前是第${i}次等待..`);await sleep(1000); // 不需要用闭包}};
总结
拥抱promise+async/await吧,相信这个就是终极的异步编程方法。