程序执行分为同步和异步,如果程序每执行一步都需要等待上一步完成才能开始,此所谓同步。如果程序在执行一段代码的同时可以去执行另一段代码,等到这段代码执行完毕再吧结果交给另一段代码,此所谓异步。
比如我们需要请求一个网络资源,由于网速比较慢,同步编程就意味着用户必须等待下载处理结束才能继续操作,所以用户体验极为不好;如果采用异步,下载进行中用户继续操作,当下载结束了,告诉用户下载的数据,这样体检就提升了很多。因此异步编程十分重要。
从计算机的角度来讲,js 只有一个线程,如果没有异步编程那一定会卡死的!异步编程主要包括以下几种:

  • 回调函数
  • 事件监听
  • 发布/订阅模型
  • Promise对象
  • ES6异步编程

回调函数 和 Promise

回调函数应该是 js 中十分基础和简单的部分,我们在定义事件,在计时器等等使用过程中都使用过:

  1. fs.readFile('/etc/passwd', function(err, data){
  2. if(err) throw err;
  3. console.log(data);
  4. });

比如这里的这个文件读取,定义了一个回调函数,在读取文件成功或失败是调用,并不会立刻调用。

如同之前在 Promise 中提到的,当我想不断的读入多个文件,就会遇到回调函数嵌套,书写代码及其的不方便,我们称之为”回调地狱”。因此 ES6 中引入是了 Promise 解决这个问题。具体表现参看之前的 Promise 部分。但是 Promise 也带来了新的问题,就是代码冗余很严重,一大堆的 then 使得回调的语义不明确。

协程

所谓协程就是几个程序交替执行:A开始执行,执行一段时间后 B 执行,执行一段时间后再 A 继续执行,如此反复。

  1. function* asyncJob(){
  2. //...
  3. var f = yield readFile(fileA);
  4. //...
  5. }

通过一个 Generator 函数的 yield, 可以将一个协程中断,去执行另一个协程。我们可以换一个角度理解 Generator 函数:它是协程在 ES6 中的具体体现。我们可以简单写一个异步任务的封装:

  1. var fetch = require('node-fetch');
  2. function* gen(){
  3. var url = 'http://api.github.com/users/github';
  4. var result = yield fetch(url);
  5. console.log(result.bio);
  6. }
  7. var g = gen();
  8. var result = g.next(); //返回的 value 是一个 Promise 对象
  9. result.value.then(function(data){
  10. return data.json;
  11. }).then(function(data){
  12. g.next(data);
  13. });

Thunk 函数

在函数传参数时我们考虑这样一个问题:

  1. function fun(x){
  2. return x + 5;
  3. }
  4. var a = 10;
  5. fun(a + 10);

这个函数返回25肯定没错,但是,我们传给函数 fun 的参数在编译时到底保留 a + 10 还是直接传入 20?显然前者没有事先计算,如果函数内多次使用这个参数,就会产生多次计算,影响性能;而后者事先计算了,但如果函数里不使用这个变量就白浪费了性能。采用把参数原封不动的放入一个函数(我们将这个函数称为 Thunk 函数),用的使用调用该函数的方式。也就是上面的前一种方式传值。所以上面代码等价于:

  1. function fun(x){
  2. return x() + 5;
  3. }
  4. var a = 10;
  5. var thunk = function(){ return a + 10};
  6. fun(thunk);

但是 js 不是这样的!js 会把多参数函数给 Thunk 了,以减少参数:

  1. var fs = require('fs');
  2. var Thunk = function(fileName) {
  3. return function(callback) {
  4. return fs.readFile(fileName, callback);
  5. };
  6. };
  7. fs.readFile(fileName, callback);
  8. var readFileThunk = Thunk(fileName);
  9. readFileThunk(callback);

这里任何具有回调函数的函数都可以写成这样的 Thunk 函数,方法如下:

  1. function Thunk(fn){
  2. return function(){
  3. var args = Array.prototype.slice.call(arguments);
  4. return function (callback){
  5. args.push(callback);
  6. return fn.apply(this, args);
  7. }
  8. }
  9. }
  10. //这样fs.readFile(fileName, callback); 写作如下形式
  11. Thunk(fs.readFile)(fileName)(callback);

关于 Thunk 函数, 可以直接使用 thunkify 模块:

  1. npm install thunkify

使用格式和上面的Thunk(fs.readFile)(fileName)(callback);一致,但使用过程中需要注意,其内部加入了检查机制,只允许 callback 被回调一次!

结合 Thunk 函数和协程,我们可以实现自动流程管理。之前我们使用 Generator 时候使用 yield 关键字将 cpu 资源释放,执行移出 Generator 函数。可以怎么移回来呢?之前我们手动调用 Generator 返回的迭代器的 next() 方法,可这毕竟是手动的,现在我们就利用 Thunk 函数实现一个自动的:

  1. var fs = require('fs');
  2. var thunkify = require('thunkify');
  3. var readFile = thunkify(fs.readFile);
  4. var gen = function*(...args){ //args 是文件路径数组
  5. for(var i = 0, len = args.length; i < len; i++){
  6. var r = yield readFile(args[i]);
  7. console.log(r.toString());
  8. }
  9. };
  10. (function run(fn){
  11. var gen = fn();
  12. function next(err, data){
  13. if(err) throw err;
  14. var result = gen.next(data);
  15. if(result.done) return; //递归直到所以文件读取完成
  16. result.value(next); //递归执行
  17. }
  18. next();
  19. })(gen);
  20. //之后可以使用 run 函数继续读取其他文件操作

如果说 Thunk 可以有现成的库使用,那么这个自动执行的 Generator 函数也有现成的库可以使用——co模块(https://github.com/tj/co)。用法与上面类似,不过 co 模块返回一个 Promise 对象。使用方式如下:

  1. var co = require('co');
  2. var fs = require('fs');
  3. var thunkify = require('thunkify');
  4. var readFile = thunkify(fs.readFile);
  5. var gen = function*(...args){ //args 是文件路径数组
  6. for(var i = 0, len = args.length; i < len; i++){
  7. var r = yield readFile(args[i]);
  8. console.log(r.toString());
  9. }
  10. };
  11. co(gen).then(function(){
  12. console.log("files loaded");
  13. }).catch(function(err){
  14. console.log("load fail");
  15. });

这里需要注意的是:yield 后面只能跟一个 thunk 函数或 promise 对象。上例中第8行 yield 后面的 readFile 是一个 thunk 函数,所以可以使用。
上面已经讲解了 thunk 函数实现自动流程管理,下面使用 Promise 实现一下:

  1. var fs = require('fs');
  2. var readFile = function(fileName){
  3. return new Promise(function(resolve, reject){
  4. fs.readFile(fileName, function(error,data){
  5. if(error) reject(error);
  6. resolve(data);
  7. });
  8. });
  9. };
  10. var gen = function*(){
  11. for(var i = 0, len = args.length; i < len; i++){
  12. var r = yield readFile(args[i]);
  13. console.log(r.toString());
  14. }
  15. };
  16. (function run(gen){
  17. var g = gen();
  18. var resolve = function(data){
  19. var result = g.next(data);
  20. if(result.done) return result.value;
  21. result.value.then(resolve);
  22. }
  23. g.next().value.then(function(data){
  24. resolve(data);
  25. });
  26. resolve();
  27. })(gen);
  28. //之后可以使用 run 函数继续读取其他文件操作

async 函数

ES7 中提出了 async 函数,但是现在已经可以用了!可这个又是什么呢?其实就是 Generator 函数的改进,我们上文写过一个这样的 Generator 函数:

  1. var gen = function*(){
  2. for(var i = 0, len = args.length; i < len; i++){
  3. var r = yield readFile(args[i]);
  4. console.log(r.toString());
  5. }
  6. };

我们把它改写成 async 函数:

  1. var asyncReadFiles = async function(){ //* 替换为 async
  2. for(var i = 0, len = args.length; i < len; i++){
  3. var r = await readFile(args[i]); //yield 替换为 await
  4. console.log(r.toString());
  5. }
  6. };

async 函数对 Generator 函数做了一下改进:

  • Generator 函数需要手动通过返回值的 next 方法执行,而 async 函数自带执行器,执行方式和普通函数完全一样。

    1. var result = asyncReadFiles(fileA, fileB, fileC);
  • 语义明确,async 表示异步,await 表示后续表达式需要等待触发的异步操作结束

  • co 模块中 yield 后面只能跟一个 thunk 函数或 promise 对象,而 await 后面可以是任何类型(不是 Promise 对象就同步执行)
  • 返回值是一个 Promise 对象,不是 Iterator ,比 Generator 方便

我们可以实现这样的一个 async 函数:

  1. async function asyncFun(){
  2. //code here
  3. }
  4. //equal to...
  5. function asyncFun(args){
  6. return fun(function*(){
  7. //code here...
  8. });
  9. function fun(genF){
  10. return new Promise(function(resolve, reject){
  11. var gen = genF();
  12. function step(nextF){
  13. try{
  14. var next = nextF();
  15. } catch(e) {
  16. return reject(e);
  17. }
  18. if(next.done){
  19. return resolve(next.value);
  20. }
  21. Promise.resolve(next.value).then(function(data){
  22. step(function(){ return gen.next(data); });
  23. }, function(e){
  24. step(function(){ return gen.throw(e); });
  25. });
  26. }
  27. step(function() { return gen.next(undefined); });
  28. });
  29. }
  30. }

我们使用 async 函数做点简单的事情:

  1. function timeout(ms){
  2. return new Promise((resolve) => {
  3. setTimeout(resolve, ms);
  4. });
  5. }
  6. async function delay(nap, ...values){
  7. while(1){
  8. try{
  9. await timeout(nap);
  10. } catch(e) {
  11. console.log(e);
  12. }
  13. var val = values.shift();
  14. if(val)
  15. console.log(val)
  16. else
  17. break;
  18. }
  19. }
  20. delay(600,1,2,3,4); //每隔 600ms 输出一个数

这里需要注意:应该把后面跟 promise对象的 await 放在一个 try 中,防止其被 rejected。当然上面的 try 语句也可以这样写:

  1. var ms = await timeout(nap).catch((e) => console.log(e));

对于函数参数中的回调函数不建议使用,避免出现不应该的错误

  1. //反例: 会得到错误结果
  2. async function fun(db){
  3. let docs = [{},{},{}];
  4. docs.forEach(async function(doc){ //ReferenceError: Invalid left-hand side in assignment
  5. await db.post(doc);
  6. });
  7. }
  8. //改写, 但依然顺序执行
  9. async function fun(db){
  10. let docs = [{},{},{}];
  11. for(let doc of docs){
  12. await db.post(doc);
  13. }
  14. }
  15. //改写, 并发执行
  16. async function fun(db){
  17. let docs = [{},{},{}];
  18. let promises = docs.map((doc) => db.post(doc));
  19. let result = await Promise.all(promises)
  20. console.log(result);
  21. }
  22. //改写, 并发执行
  23. async function fun(db){
  24. let docs = [{},{},{}];
  25. let promises = docs.map((doc) => db.post(doc));
  26. let result = [];
  27. for(let promise of promises){
  28. result.push(await promise);
  29. }
  30. console.log(result);
  31. }

Promise,Generator 和 async 函数比较

这里我们实现一个简单的功能,可以直观的比较一下。实现如下功能:

在一个 DOM 元素上绑定一系列动画,每一个动画完成才开始下一个,如果某个动画执行失败,返回最后一个执行成功的动画的返回值

  • Promise 方法

    1. function chainAnimationPromise(ele, animations){
    2. var ret = null; //存放上一个动画的返回值
    3. var p = Promise.resolve();
    4. for(let anim of animations){
    5. p = p.then(function(val){
    6. ret = val;
    7. return anim(ele);
    8. });
    9. }
    10. return p.catch(function(e){
    11. /*忽略错误*/
    12. }).then(function(){
    13. return ret; //返回最后一个执行成功的动画的返回值
    14. });
    15. }
  • Generator 方法

    1. function chainAnimationGenerator(ele, animations){
    2. return fun(function*(){
    3. var ret = null;
    4. try{
    5. for(let anim of animations){
    6. ret = yield anim(ele);
    7. }
    8. } catch(e) {
    9. /*忽略错误*/
    10. }
    11. return ret;
    12. });
    13. function fun(genF){
    14. return new Promise(function(resolve, reject){
    15. var gen = genF();
    16. function step(nextF){
    17. try{
    18. var next = nextF();
    19. } catch(e) {
    20. return reject(e);
    21. }
    22. if(next.done){
    23. return resolve(next.value);
    24. }
    25. Promise.resolve(next.value).then(function(data){
    26. step(function(){ return gen.next(data); });
    27. }, function(e){
    28. step(function(){ return gen.throw(e); });
    29. });
    30. }
    31. step(function() { return gen.next(undefined); });
    32. });
    33. }
    34. }
  • async 函数方法

    1. async function chainAnimationAsync(ele, animations){
    2. var ret = null;
    3. try{
    4. for(let anim of animations){
    5. ret = await anim(elem);
    6. }
    7. } catch(e){
    8. /*忽略错误*/
    9. }
    10. return ret;
    11. }

一个经典题

  1. console.log(0);
  2. setTimeout(function(){
  3. console.log(1)
  4. },0);
  5. setTimeout(function(){
  6. console.log(2);
  7. },1000);
  8. var pro = new Promise(function(resolve, reject){
  9. console.log(3);
  10. resolve();
  11. }).then(resolve => console.log(4));
  12. console.log(5);
  13. setTimeout(function(){
  14. console.log(6)
  15. },0);
  16. pro.then(resolve => console.log(7));
  17. var pro2 = new Promise(function(resolve, reject){
  18. console.log(8);
  19. resolve(10);
  20. }).then(resolve => console.log(11))
  21. .then(resolve => console.log(12))
  22. .then(resolve => console.log(13));
  23. console.log(14);
  24. // 0 3 5 8 14 4 11 7 12 13 1 6 2