说一说javascript的异步编程

众所周知javascript是单线程的,它的设计之初是为浏览器设计的GUI编程语言,GUI编程的特性之一是保证UI线程必定不能阻塞,不然体验不佳,甚至界面卡死。javascript

所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待同样,前面的那我的没有搞定,你就只能站在后面排队等着。java

图片来自网络

这种模式的好处是实现起来简单,执行环境相对单纯,坏处就是只要有一个任务耗时很长,后面的任务都会必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),每每就是由于某一段Javascript代码长时间运行(好比死循环),致使了整个页面卡在这个地方,其余任务没法执行。node

为了解决这个问题,Javascript语言将任务的执行模式分红两种:同步(Synchronous)和异步(Asynchronous)。jquery

“同步”就是上面所说的,后面的任务等待上一个任务结束,而后再执行。git

什么是“异步”?

所谓异步简单说就是一个任务分红两段,先执行一段,转而执行其余任务,等作好了准备转而执行第二段。github

如下是当有ABC三个任务,同步或异步执行的流程图:npm

同步编程

thread ->|----A-----||-----B-----------||-------C------|
复制代码

异步:api

A-Start ---------------------------------------- A-End   
           | B-Start ----------------------------------------|--- B-End   
           |   |     C-Start -------------------- C-End      |     |   
           V   V       V                           V         V     V      
  thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
复制代码

"异步"很是重要。在浏览器端,耗时很长的操做都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操做。在服务器端,"异步模式"甚至是惟一的模式,由于执行环境是单线程的,若是容许同步执行全部http请求,服务器性能会急剧降低,很快就会失去响应。promise

本文简单梳理总结了JavaScript异步函数的发展历史以下图:

图片来自网络

  1. 回调函数
  2. Promise
  3. Generator+co
  4. async,await

回调函数Callbacks

彷佛一切应该从回调函数开始谈起。

异步JavaScript

在Javascript 中,异步编程方式只能经过JavaScript中的一等公民函数才能完成:这种方式意味着咱们能够将一个函数做为另外一个函数的参数,在这个函数的内部能够调用被传递进来的函数(即回调函数)。

这也正是回调函数诞生的缘由:若是你将一个函数做为参数传递给另外一个函数(此时它被称为高阶函数),那么在函数内部, 你能够调用这个函数来完成相应的任务。

回调函数没有返回值(不要试图用return),仅仅被用来在函数内部执行某些动做。

看下面的例子:

step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});
复制代码

这里只是作4步,嵌套了4层回调,若是更多步骤呢?显然这样的代码只是写起来比较爽可是缺点也不少。

过分使用回调函数所会遇到的挑战:

  • 若是不能合理的组织代码,很是容易形成回调地狱(callback hell),这会使得你的代码很难被别人所理解。
  • 不能捕获异常 (try catch 同步执行,回调函数会加入队列,没法捕获错误)
  • 没法使用return语句返回值,而且也不能使用throw关键字。

也正是基于这些缘由,在JavaScript世界中,一直都在寻找着可以让异步JavaScript开发变得更简单的可行的方案。这个时候就出现了promise,它解决了上述的问题。

Promise

Promise 的最大优点是标准化,各种异步工具库都按照统一规范实现,即便是async函数也能够无缝集成。因此用 Promise 封装 API 通用性强,用起来简单,学习成本低。

一个Promise表明的是一个异步操做的最终结果。

Promise意味着[许愿|承诺]一个尚未完成的操做,但在将来会完成的。与Promise最主要的交互方法是经过将函数传入它的then方法从而获取得Promise最终的值或Promise最终拒绝(reject)的缘由。要点有三个:

  • 递归,每一个异步操做返回的都是promise对象
  • 状态机:三种状态转换,只在promise对象内部能够控制,外部不能改变状态
  • 全局异常处理

1)定义

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, thenif (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});
复制代码

每一个Promise定义都是同样的,在构造函数里传入一个匿名函数,参数是resolve和reject,分别表明成功和失败时候的处理。

2) 调用

promise.then(function(text){
    console.log(text)// Stuff worked!
    return Promise.reject(new Error('我是故意的'))
}).catch(function(err){
    console.log(err)
})
复制代码

它的主要交互方式是经过then函数,若是Promise成功执行resolve了,那么它就会将resolve的值传给最近的then函数,做为它的then函数的参数。若是出错reject,那就交给catch来捕获异常就行了。

咱们能够经过调用promise的示例,了解一下propmise的一些原理及特性:

普通调用实例:

let fs = require('fs');
let p = new Promise(function(resolve,reject){
  fs.readFile('./1.txt','utf8',(err,data)=>{
      err?reject(err):resolve(data);
  })
})

p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
复制代码

1.promise实例能够屡次调用then方法

p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
p.then((data)=>{console.log(data)},(err)=>{console.log(err)});

复制代码

2.promise实例能够支持then方法的链式调用,jquery实现链式是经过返回当前的this。可是promise不能够经过返回this来实现。由于后续经过链式增长的then不是经过原始的promise对象的状态来决定走成功仍是走失败的。

p.then((data)=>{console.log(data)},(err)=>{console.log(err)}).then((data)=>{console.log(data)})
复制代码

3.只要then方法中的成功回调和失败回调,有返回值(包括undefiend),都会走到下个then方法中的成功回调中,而且把返回值做为下个then成功回调的参数传进去。

第一个then走成功:
p.then((data)=>{return undefined},(err)={console.log()}).then((data)=>{console.log(data)})
输出:undefiend
第一个then走失败:
  p.then((data)=>{console.log(1)},(err)={return undefined).then((data)=>{console.log(data)})
输出:undefiend

复制代码

4.只要then方法中的成功回调和失败回调,有一个抛出异常,则都会走到下一个then中的失败回调中

第一个then走成功:
p.then((data)=>{throw new Err("错误")},(err)={console.log(1)}).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
输出:错误
第一个then走失败:
  p.then((data)=>{console.log(1)},(err)={throw new Err("错误")).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
输出:错误

复制代码

5.成功和失败 只能走一个,若是成功了,就不会走失败,若是失败了,就不会走成功;

6.若是then方法中,返回的不是一个普通值,仍旧是一个promise对象,该如何处理?

答案:它会等待这个promise的执行结果,而且传给下一个then方法。若是成功,就把这个promise的结果传给下一个then的成功回调而且执行,若是失败就把错误传给下一个then的失败回调而且执行。

7.具有catch捕获错误;若是catche前面的全部then方法都没有失败回调,则catche会捕获到错误信息执行他就是用来兜儿底用的

p是一个失败的回调:
p.then((data)=>{console.log('成功')}).then((data)=>{成功}).catche(e){console.log('错误')}
复制代码

8.返回的结果和 promise是同一个,永远不会成功和失败

var  r  = new Promise(function(resolve,reject){
   return r;
})
r.then(function(){
    console.log(1)
},function(err){
    console.log(err)
})
复制代码

能够看到结果一直都是pending状态

图片来自网络

当你没有现成的Promise时,你可能须要借助一些Promise库,一个流行的选择是使用 bluebird。 这些库可能会提供比原生方案更多的功能,而且不局限于Promise/A+标准所规定的特性。

Generator(ECMAScript6)+co

JavaScript 生成器是个相对较新的概念, 它是ES6(也被称为ES2015)的新特性。想象下面这样的一个场景:

当你在执行一个函数的时候,你能够在某个点暂停函数的执行,而且作一些其余工做,而后再返回这个函数继续执行, 甚至是携带一些新的值,而后继续执行。

上面描述的场景正是JavaScript生成器函数所致力于解决的问题。当咱们调用一个生成器函数的时候,它并不会当即执行, 而是须要咱们手动的去执行迭代操做(next方法)。也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每一个中断点。

function* foo () {  
  var index = 0;
  while (index < 2) {
    yield index++; //暂停函数执行,并执行yield后的操做
  }
}
var bar =  foo(); // 返回的实际上是一个迭代器

console.log(bar.next());    // { value: 0, done: false }  
console.log(bar.next());    // { value: 1, done: false }  
console.log(bar.next());    // { value: undefined, done: true }  
复制代码

更进一步的,若是你想更轻松的使用生成器函数来编写异步JavaScript代码,咱们可使用 co 这个库,co是著名的tj大神写的。

Co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可使用更加优雅的方式编写非阻塞代码。

使用co,前面的示例代码,咱们可使用下面的代码来改写:

co(function* (){  
  yield Something.save();
}).then(function() {
  // success
})
.catch(function(err) {
  //error handling
});
复制代码

你可能会问:如何实现并行操做呢?答案可能比你想象的简单,以下(其实它就是Promise.all而已):

yield [Something.save(), Otherthing.save()];  
复制代码

终极解决方案Async/ await

简而言之,使用async关键字,你能够轻松地达成以前使用生成器和co函数所作到的工做。

在这背后,async函数实际使用的是Promise,这就是为何async函数会返回一个Promise的缘由。

所以,咱们使用async函数来完成相似于前面代码所完成的工做,可使用下面这样的方式来从新编写代码:

async function save(Something) {  
  try {
    await Something.save(); // 等待await后面的代码执行完,相似于yield
  } catch (ex) {
    //error handling
  }
  console.log('success');
} 
复制代码

使用async函数,你须要在函数声明的最前面加上async关键字。这以后,你能够在函数内部使用await关键字了,做用和以前的yield做用是相似的。

使用async函数完成并行任务与yiled的方式很是的类似,惟一不一样的是,此时Promise.all再也不是隐式的,你须要显示的调用它:

async function save(Something) {  
    await Promise.all[Something.save(), Otherthing.save()]
}
复制代码

Async/Await是异步操做的终极解决方案,Koa 2在node 7.6发布以后,立马发布了正式版本,而且推荐使用async函数来编写Koa中间件。

这里给出一段Koa 2应用里的一段代码:

exports.list = async (ctx, next) => {
  try {
    let students = await Student.getAllAsync();
  
    await ctx.render('students/index', {
      students : students
    })
  } catch (err) {
    return ctx.api_error(err);
  }
};
复制代码

它作了3件事儿

  • 经过await Student.getAllAsync();来获取全部的students信息。
  • 经过await ctx.render渲染页面
  • 因为是同步代码,使用try/catch作的异常处理

以后还会分享node的基本概念和eventLoop(宏任务和微任务)

(完)

参考: The Evolution of Asynchronous JavaScript