异步编程

异步编程

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

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

回调函数 和 Promise

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

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

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

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

协程

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

function* asyncJob(){
  //...
  var f = yield readFile(fileA);
  //...
}

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

var fetch = require('node-fetch');
function* gen(){
  var url = 'http://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();    //返回的 value 是一个 Promise 对象
result.value.then(function(data){
  return data.json;
}).then(function(data){
  g.next(data);
});

Thunk 函数

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

function fun(x){
  return x + 5;
}
var a = 10;
fun(a + 10);

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

function fun(x){
  return x() + 5;
}
var a = 10;
var thunk = function(){ return a + 10};
fun(thunk);

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

var fs = require('fs');
fs.readFile(fileName, callback);
var readFileThunk = Thunk(fileName);
readFileThunk(callback);

var Thunk = function(fileName){
  return function(callback){
    return fs.readFile(fileName,callback);
  };
};

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

function Thunk(fn){
  return function(){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  }
}

//这样fs.readFile(fileName, callback); 写作如下形式

Thunk(fs.readFile)(fileName)(callback);

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

npm install thunkify

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

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

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

var gen = function*(...args){    //args 是文件路径数组
  for(var i = 0, len = args.length; i < len; i++){
    var r = yield readFile(args[i]);
    console.log(r.toString());
  }
};

(function run(fn){
  var gen = fn();
  function next(err, data){
    if(err) throw err;
    var result =  gen.next(data);
    if(result.done) return;    //递归直到所以文件读取完成
    result.value(next);    //递归执行
  }
  next();
})(gen);

//之后可以使用 run 函数继续读取其他文件操作

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

var co = require('co');
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

var gen = function*(...args){    //args 是文件路径数组
  for(var i = 0, len = args.length; i < len; i++){
    var r = yield readFile(args[i]);
    console.log(r.toString());
  }
};
co(gen).then(function(){
  console.log("files loaded");
}).catch(function(err){
  console.log("load fail");
});

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

var fs = require('fs');
var readFile = function(fileName){
  return new Promise(function(resolve, reject){
    fs.readFile(fileName, function(error,data){
      if(error) reject(error);
      resolve(data);
    });
  });
};

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

(function run(gen){
  var g = gen();

  var resolve = function(data){
    var result = g.next(data);
    if(result.done) return result.value;
    result.value.then(resolve);
  }
  g.next().value.then(function(data){
    resolve(data);
  });
  resolve();
})(gen);
//之后可以使用 run 函数继续读取其他文件操作

async 函数

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

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

我们把它改写成 async 函数:

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

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

  • Generator 函数需要手动通过返回值的 next 方法执行,而 async 函数自带执行器,执行方式和普通函数完全一样。
var result = asyncReadFiles(fileA, fileB, fileC);
  • 语义明确,async 表示异步,await 表示后续表达式需要等待触发的异步操作结束
  • co 模块中 yield 后面只能跟一个 thunk 函数或 promise 对象,而 await 后面可以是任何类型(不是 Promise 对象就同步执行)
  • 返回值是一个 Promise 对象,不是 Iterator ,比 Generator 方便

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

async function asyncFun(){
  //code here
}
//equal to...
function asyncFun(args){
  return fun(function*(){
    //code here...
  });
  function fun(genF){
    return new Promise(function(resolve, reject){
      var gen = genF();
      function step(nextF){
        try{
          var next = nextF();
        } catch(e) {
          return reject(e);
        }
        if(next.done){
          return resolve(next.value);
        }
        Promise.resolve(next.value).then(function(data){
          step(function(){ return gen.next(data); });
        }, function(e){
          step(function(){ return gen.throw(e); });
        });
      }
      step(function() { return gen.next(undefined); });
    });
  }
}

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

function timeout(ms){
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function delay(nap, ...values){
  while(1){
    try{
      await timeout(nap);
    } catch(e) {
      console.log(e);
    }
    var val = values.shift();
    if(val)
      console.log(val)
    else
      break;
  }
}
delay(600,1,2,3,4);   //每隔 600ms 输出一个数

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

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

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

//反例: 会得到错误结果
async function fun(db){
  let docs = [{},{},{}];

  docs.forEach(async function(doc){   //ReferenceError: Invalid left-hand side in assignment
    await db.post(doc);
  });
}

//改写, 但依然顺序执行
async function fun(db){
  let docs = [{},{},{}];

  for(let doc of docs){
    await db.post(doc);
  }
}

//改写, 并发执行
async function fun(db){
  let docs = [{},{},{}];
  let promises = docs.map((doc) => db.post(doc));
  let result = await Promise.all(promises)
  console.log(result);
}

//改写, 并发执行
async function fun(db){
  let docs = [{},{},{}];
  let promises = docs.map((doc) => db.post(doc));
  let result = [];
  for(let promise of promises){
    result.push(await promise);
  }
  console.log(result);
}

Promise,Generator 和 async 函数比较

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

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

  • Promise 方法
function chainAnimationPromise(ele, animations){
  var ret = null;  //存放上一个动画的返回值
  var p = Promise.resolve();
  for(let anim of animations){
    p = p.then(function(val){
      ret = val;
      return anim(ele);
    });
  }
  return p.catch(function(e){
    /*忽略错误*/
  }).then(function(){
    return ret;  //返回最后一个执行成功的动画的返回值
  });
}
  • Generator 方法
function chainAnimationGenerator(ele, animations){
  return fun(function*(){
    var ret = null;
    try{
      for(let anim of animations){
        ret = yield anim(ele);
      }
    } catch(e) {
      /*忽略错误*/
    }
    return ret;
  });

  function fun(genF){
    return new Promise(function(resolve, reject){
      var gen = genF();
      function step(nextF){
        try{
          var next = nextF();
        } catch(e) {
          return reject(e);
        }
        if(next.done){
          return resolve(next.value);
        }
        Promise.resolve(next.value).then(function(data){
          step(function(){ return gen.next(data); });
        }, function(e){
          step(function(){ return gen.throw(e); });
        });
      }
      step(function() { return gen.next(undefined); });
    });
  }
}
  • async 函数方法
async function chainAnimationAsync(ele, animations){
  var ret = null;
  try{
    for(let anim of animations){
      ret = await anim(elem);
    }
  } catch(e){
    /*忽略错误*/
  }
  return ret;
}

一个经典题

console.log(0);

setTimeout(function(){
  console.log(1)
},0);
setTimeout(function(){
  console.log(2);
},1000);

var pro = new Promise(function(resolve, reject){
  console.log(3);
  resolve();
}).then(resolve => console.log(4));

console.log(5);

setTimeout(function(){
  console.log(6)
},0);

pro.then(resolve => console.log(7));

var pro2 = new Promise(function(resolve, reject){
  console.log(8);
  resolve(10);
}).then(resolve => console.log(11))
  .then(resolve => console.log(12))
  .then(resolve => console.log(13));

console.log(14);

// 0 3 5 8 14 4 11 7 12 13 1 6 2
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,311评论 5 22
  • 由于JavaScript是单线程的语言,因此异步编程对于js的重要程度可想而知,可以说没有异步的js程序寸步难行。...
    会飞小超人阅读 965评论 0 7
  • 回调函数 最原始的callback,优点是简单、容易理解,当然也有很严重的缺点是不利于代码的阅读和维护,多了之后的...
    席小白阅读 324评论 3 4
  • 异步编程在JavaScript中非常重要。过多的异步编程也带了回调嵌套的问题,本文会提供一些解决“回调地狱”的方法...
    AlienZHOU阅读 27,490评论 2 52
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,711评论 0 5