说一说javascript的异步编程

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

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

图片来自网络

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

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

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

什么是“异步”?

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

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

同步

thread ->|----A-----||-----B-----------||-------C------|

异步:

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请求,服务器性能会急剧下降,很快就会失去响应。

本文简单梳理总结了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, then…

  if (/* 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

posted @ 2018-11-08 10:18  陈术芳  阅读(433)  评论(0编辑  收藏  举报