众所周知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异步函数的发展历史如下图:
- 回调函数
- Promise
- Generator+co
- 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(宏任务和微任务)
(完)