ES6中除了上篇文章讲过的语法新特性和一些新的API以外,还有两个很是重要的新特性就是Promise和Generator,今天咱们将会详细讲解一下这两个新特性。node
Promise 是异步编程的一种解决方案,比传统的解决方案“回调函数和事件”更合理和更强大。es6
所谓Promise,简单说就是一个容器,里面保存着某个将来才会结束的事件(一般是一个异步操做)的结果。ajax
从语法上说,Promise 是一个对象,从它能够获取异步操做的消息。编程
Promise有两个特色:json
Promise对象表明一个异步操做,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。数组
只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。promise
Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。框架
这与事件(Event)彻底不一样,事件的特色是,若是你错过了它,再去监听,是得不到结果的。koa
Promise将异步操做以同步操做的流程表达出来,避免了层层嵌套的回调函数。异步
Promise对象提供统一的接口,使得控制异步操做更加容易。
Promise对象是一个构造函数,用来生成Promise实例:
var promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操做成功 */){ resolve(value); } else { reject(error); } } );
promise能够接then操做,then操做能够接两个function参数,第一个function的参数就是构建Promise的时候resolve的value,第二个function的参数就是构建Promise的reject的error。
promise.then(function(value) { // success }, function(error) { // failure } );
咱们看一个具体的例子:
function timeout(ms){ return new Promise(((resolve, reject) => { setTimeout(resolve,ms,'done'); })) } timeout(100).then(value => console.log(value));
Promise中调用了一个setTimeout方法,并会定时触发resolve方法,并传入参数done。
最后程序输出done。
Promise一经建立就会立马执行。可是Promise.then中的方法,则会等到一个调用周期事后再次调用,咱们看下面的例子:
let promise = new Promise(((resolve, reject) => { console.log('Step1'); resolve(); })); promise.then(() => { console.log('Step3'); }); console.log('Step2'); 输出: Step1 Step2 Step3
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。所以能够采用链式写法,即then方法后面再调用另外一个then方法.
getJSON("/users.json").then(function(json){ return json.name; }).then(function(name){ console.log(name); });
上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成之后,会将返回结果做为参数,传入第二个回调函数
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getJSON("/users.json").then(function(json){ return json.name; }).catch(function(error){ console.log(error); });
Promise 对象的错误具备“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误老是会被下一个catch语句捕获
getJSON("/users.json").then(function(json){ return json.name; }).then(function(name){ console.log(name); }).catch(function(error){ //处理前面全部产生的错误 console.log(error); });
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例
var p = Promise.all([p1,p2,p3]);
Promise.race方法一样是将多个Promise实例,包装成一个新的Promise实例
var p = Promise.race([p1,p2,p3]);
只要p一、p二、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数.
Promise.resolve()将现有对象转为Promise对象.
Promise.resolve('js'); //等价于 new Promise(resolve => resolve('js'));
那么什么样的对象可以转化成为Promise对象呢?
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected
var p = Promise.reject('error'); //等价于 var p = new Promise((resolve,reject) => reject('error'));
Promise.reject()方法的参数,会原封不动地做为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致
Promise对象的回调链,无论以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能没法捕捉到(由于Promise内部的错误不会冒泡到全局)。所以,咱们能够提供一个done方法,老是处于回调链的尾端,保证抛出任何可能出现的错误
asyncFunc().then(f1).catch(f2).then(f3).done();
finally方法用于指定无论Promise对象最后状态如何,都会执行的操做。它与done方法的最大区别,它接受一个普通的回调函数做为参数,该函数无论怎样都必须执行.
server.listen(1000).then(function(){ //do something }.finally(server.stop);
Generator 函数是 ES6 提供的一种异步编程解决方案
从语法上,首先能够把它理解成,Generator函数是一个状态机,封装了多个内部状态
执行 Generator 函数会返回一个遍历器对象.
形式上,Generator 函数是一个普通函数,可是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield语句,定义不一样的内部状态。
举个例子:
function * helloWorldGenerator(){ yield 'hello'; yield 'world'; return 'ending'; } var gen = helloWorldGenerator();
输出结果:
console.log(gen.next()); console.log(gen.next()); console.log(gen.next()); { value: 'hello', done: false } { value: 'world', done: false } { value: 'ending', done: true }
遍历器对象的next方法的运行逻辑以下:
(1)遇到yield语句,就暂停执行后面的操做,并将紧跟在yield后面的那个表达式的值,做为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。
(3)若是没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,做为返回的对象的value属性值。
(4)若是该函数没有return语句,则返回的对象的value属性值为undefined。
注意,yield句自己没有返回值,或者说老是返回undefined。
next方法能够带一个参数,该参数就会被看成上一个yield语句的返回值。
function * f() { for( let i =0; true; i++){ let reset = yield i; if(reset){ i = -1; } } } let g = f(); console.log(g.next()); console.log(g.next()); console.log(g.next(true));
输出结果:
{ value: 0, done: false } { value: 1, done: false } { value: 0, done: false }
能够看到最后的一步,咱们使用next传入的true替代了i的值,最后致使i= -1 + 1 = 0.
咱们再看一个例子:
function * f2(x){ var y = 2 * ( yield ( x + 1)); var z = yield (y / 3); return (x + y + z); } var r1= f2(5); console.log(r1.next()); console.log(r1.next()); console.log(r1.next()); var r2= f2(5); console.log(r2.next()); console.log(r2.next(12)); console.log(r2.next(13));
输出结果:
{ value: 6, done: false } { value: NaN, done: false } { value: NaN, done: true } { value: 6, done: false } { value: 8, done: false } { value: 42, done: true }
若是next不传值的话,yield自己是没有返回值的,因此咱们会获得NaN。
可是若是next传入特定的值,则该值会替换该yield,成为真正的返回值。
若是在 Generator 函数内部,调用另外一个 Generator 函数,默认状况下是没有效果的
function * a1(){ yield 'a'; yield 'b'; } function * b1(){ yield 'x'; a1(); yield 'y'; } for(let v of b1()){ console.log(v); }
输出结果:
x y
能够看到,在b1中调用a1是没有效果的。
将上面的例子修改一下:
function * a1(){ yield 'a'; yield 'b'; } function * b1(){ yield 'x'; yield * a1(); yield 'y'; } for(let v of b1()){ console.log(v); }
输出结果:
x a b y
Generator函数的暂停执行的效果,意味着能够把异步操做写在yield语句里面,等到调用next方法时再日后执行。这实际上等同于不须要写回调函数了,由于异步操做的后续操做能够放在yield语句下面,反正要等到调用next方法时再执行。因此,Generator函数的一个重要实际意义就是用来处理异步操做,改写回调函数。
咱们看一个怎么经过Generator来获取一个Ajax的结果。
function * ajaxCall(){ let result = yield request("http://www.flydean.com"); let resp = JSON.parse(result); console.log(resp.value); } function request(url){ makeAjaxCall(url, function(response){ it.next(response); }); } var it = ajaxCall(); it.next();
咱们使用一个yield来获取异步执行的结果。可是咱们如何将这个yield传给result变量呢?要记住yield自己是没有返回值的。
咱们须要调用generator的next方法,将异步执行的结果传进去。这就是咱们在request方法中作的事情。
什么是异步应用呢?
所谓"异步",简单说就是一个任务不是连续完成的,能够理解成该任务被人为分红两段,先执行第一段,而后转而执行其余任务,等作好了准备,再回过头执行第二段。
好比,有一个任务是读取文件进行处理,任务的第一段是向操做系统发出请求,要求读取文件。而后,程序执行其余任务,等到操做系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫作异步。
相应地,连续的执行就叫作同步。因为是连续执行,不能插入其余任务,因此操做系统从硬盘读取文件的这段时间,程序只能干等着。
ES6诞生之前,异步编程的方法,大概有下面四种。
回调函数
事件监听
发布/订阅
Promise 对象
fs.readFile(fileA, 'utf-8', function(error,data){ fs.readFile(fileB, 'utf-8', function(error,data){ } })
若是依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,没法管理。由于多个异步操做造成了强耦合,只要有一个操做须要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种状况就称为"回调函数地狱"(callback hell)。
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,容许将回调函数的嵌套,改为链式调用。
let readFile = require('fs-readfile-promise'); readFile(fileA).then(function(){ return readFile(fileB); }).then(function(data){ console.log(data); })
在讲Thunk函数以前,咱们讲一下函数的调用有两种方式,一种是传值调用,一种是传名调用。
"传值调用"(call by value),即在进入函数体以前,就计算x + 5的值(等于6),再将这个值传入函数f。C语言就采用这种策略。
“传名调用”(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。
编译器的“传名调用”实现,每每是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫作 Thunk 函数。
举个例子:
function f(m){ return m * 2; } f(x + 5);
上面的代码等于:
var thunk = function () { return x + 5; } function f(thunk){ return thunk() * 2; }
在 JavaScript 语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数做为参数的单参数函数。
怎么解释呢?
好比nodejs中的:
fs.readFile(filename,[encoding],[callback(err,data)])
readFile接收3个参数,其中encoding是可选的。咱们就以两个参数为例。
通常来讲,咱们这样调用:
fs.readFile(fileA,callback);
那么有没有办法将其改写成为单个参数的function的级联调用呢?
var Thunk = function (fn){ return function (...args){ return functon (callback){ return fn.call(this,...args, callback); } } } var readFileThunk = Thunk(fs.readFile); readFileThunk(fileA)(callback);
能够看到上面的Thunk将两个参数的函数改写成为了单个参数函数的级联方式。或者说Thunk是接收一个callback并执行方法的函数。
这样改写有什么用呢?Thunk函数如今能够用于 Generator 函数的自动流程管理。
以前在讲Generator的时候,若是Generator中有多个yield的异步方法,那么咱们须要在next方法中传入这些异步方法的执行结果。
手动传入异步执行结果固然是能够的。可是有没有自动执行的办法呢?
let fs = require('fs'); let thunkify = require('thunkify'); let readFileThunk = thunkify(fs.readFile); let gen = function * (){ let r1 = yield readFileThunk('/tmp/file1'); console.log(r1.toString()); let r2 = yield readFileThunk('/tmp/file2'); console.log(r2.toString()); } let g = gen(); function run(fn){ let gen = fn(); function next (err, data){ let result = gen.next(data); if(result.done) return; result.value(next); } next(); } run(g);
gen.next返回的是一个对象,对象的value就是Thunk函数,咱们向Thunk函数再次传入next callback,从而出发下一次的yield操做。
有了这个执行器,执行Generator函数方便多了。无论内部有多少个异步操做,直接把 Generator 函数传入run函数便可。固然,前提是每个异步操做,都要是Thunk函数,也就是说,跟在yield命令后面的必须是Thunk函数。
Promise和Generator是ES6中引入的很是中要的语法,后面的koa框架就是Generator的一种具体的实现。咱们会在后面的文章中详细讲解koa的使用,敬请期待。
本文做者:flydean程序那些事本文连接:http://www.flydean.com/es6-promise-generator/
本文来源:flydean的博客
欢迎关注个人公众号:「程序那些事」最通俗的解读,最深入的干货,最简洁的教程,众多你不知道的小技巧等你来发现!