JavaScript 处理异步的几种方法

Javascript语言的执行环境是”单线程”(single thread)。为了解决由于执行耗时任务导致整个页面的卡顿的问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

1
2
3
4
// 可能会涉及到跨域请求的问题,tornado 解决方案:
// CORS是一个W3C标准,全称是“跨域资源共享”(Cross-origin resource sharing)
// 后面的*可以换成ip地址,意为允许访问的地址
self.set_header('Access-Control-Allow-Origin', '*')

async/await

虽然 co 是社区里面的优秀异步解决方案,但是并不是语言标准,只是一个过渡方案。ES7语言层面提供 async/await 去解决语言层面的难题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const request = require('request');

const options = {
url: 'https://localhost:9997/',
headers: {
'User-Agent': 'request'
}
};

const getRepoData = () => {
return new Promise((resolve, reject) => {
request(options, (err, res, body) => {
if (err) {
reject(err);
}
resolve(body);
});
});
};

async function asyncFun() {
try {
const value = await getRepoData();
// ... 和上面的yield类似,如果有多个异步流程,可以放在这里,比如
// const r1 = await getR1();
// const r2 = await getR2();
// const r3 = await getR3();
// 每个await相当于暂停,执行await之后会等待它后面的函数
//(不是generator)返回值之后再执行后面其它的 await 逻辑。
return value;
} catch (err) {
console.log(err);
}
}

asyncFun().then(x => console.log(`x: ${x}`)).catch(err => console.error(err));

tips:

  • async 用来申明里面包裹的内容可以进行同步的方式执行,await则是进行执行顺序控制,每次执行一个 await,程序都会暂停等待 await 返回值,然后再执行之后的 await。
  • await 后面调用的函数需要返回一个 promise,另外这个函数是一个普通的函数即可,而不是 generator。
  • await 只能用在 async 函数之中,用在普通函数中会报错。
  • await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。

其实,async/await 的用法和 co 差不多,await 和 yield 都是表示暂停,外面包裹一层 async 或者 co 来表示里面的代码可以采用同步的方式进行处理。不过 async/await 里面的await 后面跟着的函数不需要额外处理,co 是需要将它写成一个 generator 的。

Promise

使用 Promise 可以很好的减少嵌套的层数,Promise 的实现采用了状态机,在函数里面可以很好的通过 resolve 和 reject 进行流程控制,可以按照顺序链式的去执行一系列代码逻辑。下面是使用 Promise 的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const request = require('request');
// 请求的url和header
const options = {
url: 'https://localhost:9997/',
headers: {
'User-Agent': 'request'
}
};

// 获取信息
const getRepoData = () => {
return new Promise((resolve, reject) => {
request(options, (err, res, body) => {
if (err) {
reject(err);
}
resolve(body);
});
});
};

getRepoData()
.then((result) => console.log(result))
.catch((reason) => console.error(reason));

// 此处如果是多个 Promise 顺序执行的话,如下:
// 每个 then 里面去执行下一个 promise
// getRepoData()
// .then((value2) => {return promise2})
// .then((value3) => {return promise3})
// .then((x) => console.log(x))

不过 Promise 仍然存在缺陷,它只是减少了嵌套,并不能完全消除嵌套。举个例子,对于多个 promise 串行执行的情况,第一个 promise 的逻辑执行完之后,我们需要在它的 then 函数里面去执行第二个 promise,这个时候会产生一层嵌套。

Generator

在 Node.js 中经常用的 tj/co 就是使用 generator 结合 promise 来实现的,co 是 coroutine 的简称,借鉴于 python、lua 等语言中的协程。它可以将异步的代码逻辑写成同步的方式,这使得代码的阅读和组织变得更加清晰,也便于调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const co = require('co');
const request = require('request');

const options = {
url: 'https://localhost:9997/',
headers: {
'User-Agent': 'request'
}
};
// yield 后面是一个生成器 generator
const getRepoData = function* () {
return new Promise((resolve, reject) => {
request(options, (err, res, body) => {
if (err) {
reject(err);
}
resolve(body);
});
});
};

co(function* () {
const result = yield getRepoData;
// ... 如果有多个异步流程,可以放在这里,比如
// const r1 = yield getR1;
// const r2 = yield getR2;
// const r3 = yield getR3;
// 每个yield相当于暂停,执行yield之后会等待它后面的 generator
// 返回值之后再执行后面其它的yield逻辑。
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err);

回调函数

这是异步编程最基本的方法。假定有两个函数fa和fb,后者等待前者的执行结果。如果fa是一个很耗时的任务,可以考虑改写fa,把fb写成fa的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 改写前
fa();
fb();

// 改写后
function fa(callback) {
setTimeout(function() {
// fa 的任务代码
callback;
}, 1000);
}

// 执行改写后的代码
fa(fb);

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function a() {
  setTimeout(function() {
    console.log("setTimeout");
  }, 7000);
  
  function oneSecond() {
    var now = new Date();
    var exitTime = now.getTime() + 1000;
    while (true) {
      now = new Date();
      if (now.getTime() >= exitTime) {
        console.log("oneSecond");
        return;
      }
    }
  }
  oneSecond();
  function twoSecond() {
    var now = new Date();
    var exitTime = now.getTime() + 2000;
    while (true) {
      now = new Date();
      if (now.getTime() >= exitTime) {
        console.log("twoSecond");
        return;
      }
    }
  }
  twoSecond();
  function threeSecond() {
    var now = new Date();
    var exitTime = now.getTime() + 3000;
    while (true) {
      now = new Date();
      if (now.getTime() >= exitTime) {
        console.log("threeSecond");
        return;
      }
    }
  }
  threeSecond();
}

a();
console.log("Continue...");

执行结果:

1
2
3
4
5
6
7
oneSecond
twoSecond
threeSecond
Continue...
// 1(7-3-2-1)秒后输出 setTimeout,如果 setTimeout 小于7秒,
// 立即输出,但是顺序不变
setTimeout