Javascript异步编程(一)

我们知道Javascript是单线程的,同一时间只能执行一个任务,这就意味着即使一个任务耗时很长,下一个任务也只能等下去,整个程序就像卡死了一样。为了解决这个问题,Javascript将任务分为同步任务与异步任务:同步任务会阻塞程序,只有一个任务执行完才会开始下一个任务;异步任务则是非阻塞的,异步任务没有完成的时候下一个任务就可以开始执行,常见的异步任务有setTimeout,Ajax操作等。

由于异步不会阻塞程序,因此从设计上说更有优势。然而异步会使得程序的流程控制变得麻烦,如何编写出优雅的异步代码一直困扰着人们,下面介绍几种常见的异步编程方法。

callback

这是大家最熟悉的异步编程方式了,例如我们在setTimeout里面就会指定一个回调函数,在时间到的时候执行;又或者在发起Ajax请求的时候也会指定一个回调函数,在请求返回的时候执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getSomething(url,callback){
var xhr = new XMLHttpRequest();
xhr.open("GET",url,true);
xhr.onreadystatechange = function(){
if (xhr.readyState === 4 ) {
if (xhr.status === 200) {
callback.call(this,xhr.responseText) // 执行回调
}
}
}
}
var print = function(text){
console.log(text);
}
getSomething('http://m.test.com/api',print); // 执行异步操作
  • 优点
    • 简单、容易理解
    • 非常容易部署到自己的库/函数中
  • 缺点
    • 容易导致嵌套过深,造成所谓回调地狱,导致代码高度耦合,可读性差
    • 每次只能传递一个回调函数,有很大局限
    • 不容易进行错误处理,一是因为异步函数是立即返回的,异步事物中发生的错误无法通过try-catch捕获;二是回调函数执行时异步函数的上下文已经不存在了

generator

generator是es6新提出的一个东西,可以用于异步编程。不过目前看来这只是一个过渡方案,了解即可,未来还是属于下面会提到的promise。

generator跟普通函数很像,区别有两点:

  1. 声明的时候需要在function后面加一个*。就像这样:function* generator(){}
  2. 内部可以使用yield语句,这个语句只能用于generator

调用generator的时候并不会立即执行函数,而是返回一个迭代器,调用这个迭代的next方法可以遍历到函数内部的下一个状态,即运行到下一个yield。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* generator(){
yield 'hello';
yield 'ecmascript';
return 'end';
}
var gen = generator();
gen.next();
// Object { value: "hello", done: false }
gen.next()
// Object { value: "ecmascript", done: false }
gen.next()
// Object { value: "end", done: true }
gen.next()
// Object { value: undefined, done: true }

显然yield和return出来的值会出现在value字段中,可以通过done字段知道是否已经遍历完。

另外我们也可以使用for..of循环来遍历,不过此时不会遍历到return的值。

1
2
3
4
5
for(let x of generator()){
console.log(x);
}
// hello
// ecmascript

虽然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 函数的含义与用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});

代码中首先『实例化』了一个generator函数,然后调用next方法,即执行函数内部的fetch,由于fetch返回的是一个promise,因此要用then方法执行下一个next方法。

可以看到使用了generator函数的确能让异步操作变得简洁,然而流程控制依然不方便。generator用于异步编程终究是一个过渡方案,下面要介绍的promise+async/await才是『终极』的解决方案。

promise+async/await

Promise是什么这里就不介绍了,不了解的可以看阮一峰的es6入门。这里具体讲讲async/await。

先看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var sleep = function (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve();
}, time);
})
};
var start = async function () {
// 在这里使用起来就像同步代码那样直观
console.log('start');
await sleep(3000);
console.log('end');
};
start();// 输出start,3秒后输出end
  • async放在function前面用于修饰,代表这是一个async函数,await只能用在async修饰的函数里面。
  • await代表这里等待一个promise对象返回,函数的执行会暂停在这里,知道promise返回了结果。

明显这样的写法很像同步写法,十分优雅。

另外还有一些要注意的:

  1. 通过await可以获得promise resolve出来的值,而不必通过then

    1
    2
    3
    4
    var start = async function () {
    let result = await sleep(3000);
    console.log(result); // 收到 ‘ok’
    };
  2. 通过await也能直接通过try catch捕获错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var start = async function () {
    try {
    console.log('start');
    await sleep(3000); // 这里得到了一个返回错误
    // 所以以下代码不会被执行了
    console.log('end');
    } catch (err) {
    console.log(err); // 这里捕捉到错误 `error`
    }
    };
  3. await也能写在循环里,并且因为类似同步代码,使用await不必担心出现以往需要用闭包解决的问题。使用循环时需要注意,await只能在async修饰的函数里面执行,不要在forEach里面的函数使用await。

    1
    2
    3
    4
    5
    6
    var start = async function () {
    for (var i = 1; i <= 10; i++) {
    console.log(`当前是第${i}次等待..`);
    await sleep(1000); // 不需要用闭包
    }
    };

总结

拥抱promise+async/await吧,相信这个就是终极的异步编程方法。

Reference

Javascript异步编程的4种方法

ES6 - Note7:Generator函数

Generator 函数的含义与用法

体验异步的终极解决方案-ES7的Async/Await

快速理解和使用 ES7 await/async