chenshufang
众所周知javascript是单线程的,它的设计之初是为浏览器设计的GUI编程语言,GUI编程的特性之一是保证UI线程一定不能阻塞,否则体验不佳,甚至界面卡死。 所谓的**单线程**就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着。 ![图片来自网络](http://www.ruanyifeng.com/blogimg/asset/201212/bg2012122101.jpg) 这种模式的好处是实现起来简单,执行环境相对单纯,坏处就是只要有一个任务耗时很长,后面的任务都会必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段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异步函数的发展历史如下图: ![图片来自网络](https://dn-cnode.qbox.me/FowNmdNw00ghB3PxKtMz9ajo2i5c) 1. 回调函数 1. Promise 1. Generator+co 1. 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状态 ![图片来自网络](https://os4ty6tab.qnssl.com/cblued/static/promise.1cjsidchb1l2ocp.png) 当你没有现成的Promise时,你可能需要借助一些Promise库,一个流行的选择是使用 [bluebird](https://github.com/petkaantonov/bluebird)。 这些库可能会提供比原生方案更多的功能,并且不局限于Promise/A+标准所规定的特性。 ## Generator(ECMAScript6)+co JavaScript [生成器](https://blog.risingstack.com/introduction-to-koa-generators/)是个相对较新的概念, 它是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](https://www.npmjs.com/package/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](https://blog.risingstack.com/asynchronous-javascript/)

相关文章: