关于ecmascript:ES6中的Promise和Generator详解

43次阅读

共计 8200 个字符,预计需要花费 21 分钟才能阅读完成。

简介

ES6 中除了上篇文章讲过的语法新个性和一些新的 API 之外,还有两个十分重要的新个性就是 Promise 和 Generator, 明天咱们将会具体解说一下这两个新个性。

Promise

什么是 Promise

Promise 是异步编程的一种解决方案,比传统的解决方案“回调函数和事件”更正当和更弱小。

所谓 Promise,简略说就是一个容器,外面保留着某个将来才会完结的事件(通常是一个异步操作)的后果。

从语法上说,Promise 是一个对象,从它能够获取异步操作的音讯。

Promise 的特点

Promise 有两个特点:

  1. 对象的状态不受外界影响。

Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已实现,又称 Fulfilled)和 Rejected(已失败)。

只有异步操作的后果,能够决定以后是哪一种状态,任何其余操作都无奈扭转这个状态。

  1. 一旦状态扭转,就不会再变,任何时候都能够失去这个后果。

Promise 对象的状态扭转,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。

这与事件(Event)齐全不同,事件的特点是,如果你错过了它,再去监听,是得不到后果的。

Promise 的长处

Promise 将异步操作以同步操作的流程表达出来,防止了层层嵌套的回调函数。

Promise 对象提供对立的接口,使得管制异步操作更加容易。

Promise 的毛病

  1. 无奈勾销 Promise,一旦新建它就会立刻执行,无奈中途勾销。
  2. 如果不设置回调函数,Promise 外部抛出的谬误,不会反馈到内部。
  3. 当处于 Pending 状态时,无奈得悉目前停顿到哪一个阶段(刚刚开始还是行将实现)。

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 一经创立就会立马执行。然而 Promise.then 中的办法,则会等到一个调用周期过后再次调用,咱们看上面的例子:

let promise = new Promise(((resolve, reject) => {console.log('Step1');
    resolve();}));

promise.then(() => {console.log('Step3');
});

console.log('Step2');

输入:Step1
Step2
Step3

Promise.prototype.then()

then 办法返回的是一个新的 Promise 实例(留神,不是原来那个 Promise 实例)。因而能够采纳链式写法,即 then 办法前面再调用另一个 then 办法.

getJSON("/users.json").then(function(json){return json.name;}).then(function(name){console.log(name);
});

下面的代码应用 then 办法,顺次指定了两个回调函数。第一个回调函数实现当前,会将返回后果作为参数,传入第二个回调函数

Promise.prototype.catch()

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.all 办法用于将多个 Promise 实例,包装成一个新的 Promise 实例

var p = Promise.all([p1,p2,p3]);
  1. 只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
  2. 只有 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

Promise.race()

Promise.race 办法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

var p = Promise.race([p1,p2,p3]);

只有 p1、p2、p3 之中有一个实例率先扭转状态,p 的状态就跟着扭转。那个率先扭转的 Promise 实例的返回值,就传递给 p 的回调函数.

Promise.resolve()

Promise.resolve()将现有对象转为 Promise 对象.

Promise.resolve('js');
// 等价于
new Promise(resolve => resolve('js'));

那么什么样的对象可能转化成为 Promise 对象呢?

  1. 参数是一个 Promise 实例
  2. 参数是一个 thenable 对象
  3. 参数不是具备 then 办法的对象,或基本就不是对象
  4. 不带有任何参数

Promise.reject()

Promise.reject(reason)办法也会返回一个新的 Promise 实例,该实例的状态为 rejected

var p = Promise.reject('error');
// 等价于
var p = new Promise((resolve,reject) => reject('error'));

Promise.reject()办法的参数,会一成不变地作为 reject 的理由,变成后续办法的参数。这一点与 Promise.resolve 办法不统一

done()

Promise 对象的回调链,不论以 then 办法或 catch 办法结尾,要是最初一个办法抛出谬误,都有可能无奈捕捉到(因为 Promise 外部的谬误不会冒泡到全局)。因而,咱们能够提供一个 done 办法,总是处于回调链的尾端,保障抛出任何可能呈现的谬误

asyncFunc().then(f1).catch(f2).then(f3).done();

finally()

finally 办法用于指定不论 Promise 对象最初状态如何,都会执行的操作。它与 done 办法的最大区别,它承受一个一般的回调函数作为参数,该函数不管怎样都必须执行.

server.listen(1000).then(function(){//do something}.finally(server.stop);

Generator

什么是 Generator

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}

yield

遍历器对象的 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,成为真正的返回值。

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 办法中做的事件。

Generator 的异步利用

什么是异步利用呢?

所谓 ” 异步 ”,简略说就是一个工作不是间断实现的,能够了解成该工作被人为分成两段,先执行第一段,而后转而执行其余工作,等做好了筹备,再回过头执行第二段。

比方,有一个工作是读取文件进行解决,工作的第一段是向操作系统发出请求,要求读取文件。而后,程序执行其余工作,等到操作系统返回文件,再接着执行工作的第二段(解决文件)。这种不间断的执行,就叫做异步。

相应地,间断的执行就叫做同步。因为是间断执行,不能插入其余工作,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。

ES6 诞生以前,异步编程的办法,大略有上面四种。
回调函数
事件监听
公布 / 订阅
Promise 对象

回调函数

fs.readFile(fileA, 'utf-8', function(error,data){fs.readFile(fileB, 'utf-8', function(error,data){}})

如果顺次读取两个以上的文件,就会呈现多重嵌套。代码不是纵向倒退,而是横向发展,很快就会乱成一团,无奈治理。因为多个异步操作造成了强耦合,只有有一个操作须要批改,它的下层回调函数和上层回调函数,可能都要跟着批改。这种状况就称为 ” 回调函数天堂 ”(callback hell)。

Promise

Promise 对象就是为了解决这个问题而提出的。它不是新的语法性能,而是一种新的写法,容许将回调函数的嵌套,改成链式调用。

let readFile = require('fs-readfile-promise');
readFile(fileA).then(function(){return readFile(fileB);
}).then(function(data){console.log(data);
})

Thunk 函数和异步函数主动执行

在讲 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 的博客

欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

正文完
 0