一段引言:javascript
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。html
它由社区最先提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了
Promise对象。
java
Ref: Javascript异步编程的4种方法node
Javascript语言将任务的执行模式分红两种:同步(Synchronous)和异步(Asynchronous)。git
*** "同步模式"就是上一段的模式,后一个任务等待前一个任务结束,而后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;es6
*** "异步模式"则彻底不一样,每个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,因此程序的执行顺序与任务的排列顺序是不一致的、异步的。github
function f1(callback){ setTimeout(function () { // ----> 将耗时的操做推迟执行,什么垃圾的初级思想,固然不可行 // f1的任务代码 callback(); }, 1000); }
另外一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。shell
// 当f1发生done事件,就执行f2。
f1.on('done', f2);
---------------------------------------------------------------------
function f1(){ setTimeout(function () { // f1的任务代码
// 执行完成后,当即触发done事件,从而开始执行f2
f1.trigger('done');
}, 1000); }
这就叫作"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。编程
就是多出了一个“订阅中心”统一管理信号。json
// f2向"信号中心"jQuery订阅"done"信号。
jQuery.subscribe("done", f2);
-----------------------------------------------------------------------
function f1(){ setTimeout(function () { // f1的任务代码
// f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引起f2的执行
jQuery.publish("done"); }, 1000); }
f1的回调函数f2,回调函数变成了链式写法。
f1().then(f2);
f1要进行以下改写:
function f1(){ var dfd = $.Deferred(); setTimeout(function () { // f1的任务代码 dfd.resolve(); }, 500); return dfd.promise; }
Ref: ECMAScript 6 入门 - Promise 对象
所谓Promise
,简单说就是一个容器,里面保存着某个将来才会结束的事件(一般是一个异步操做)的结果。
从语法上说,Promise 是一个对象,从它能够获取异步操做的消息。
Promise 提供统一的 API,各类异步操做均可以用一样的方法进行处理。
Promise
对象有如下两个特色:(1)对象的状态不受外界影响。
Promise
对象表明一个异步操做,有三种状态:
- pending
(进行中)
- fulfilled
(已成功)
- rejected
(已失败)
只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。
这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其余手段没法改变。
(2)一旦状态改变,就不会再变,任什么时候候均可以获得这个结果。
Promise
对象的状态改变,只有两种可能:
- pending ---->
fulfilled
- pending ---->
rejected
只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
若是改变已经发生了,你再对Promise
对象添加回调函数,也会当即获得这个结果。
【晚一步也能获得信息:这与事件(Event)彻底不一样,事件的特色是,若是你错过了它,再去监听,是得不到结果的】
有了Promise
对象,就能够将异步操做以同步操做的流程表达出来,避免了层层嵌套的回调函数。【这是相对于事件监听而言】
此外,Promise
对象提供统一的接口,使得控制异步操做更加容易。
Promise
也有一些缺点。
- 首先,没法取消Promise
,一旦新建它就会当即执行,没法中途取消。
- 其次,若是不设置回调函数,Promise
内部抛出的错误,不会反应到外部。
- 第三,当处于pending
状态时,没法得知目前进展到哪个阶段(刚刚开始仍是即将完成)。
若是某些事件不断地反复发生,通常来讲,使用 Stream 模式是比部署Promise
更好的选择。
创造了一个Promise
实例。
/**
* 函数做为参数
*/
const promise = new Promise( function(resolve, reject) { // ... some code if (/* 异步操做成功 */){ resolve(value); // pending ----> resolved,在异步操做成功时调用,并将异步操做的结果,做为参数传递出去; } else { reject(error); // pending ----> rejected,在异步操做失败时调用,并将异步操做报出的错误,做为参数传递出去。 } });
Promise
实例生成之后,能够用then
方法分别指定resolved
状态和rejected
状态的回调函数,这里是两个参数哦,第二个函数是可选的,不必定要提供。
三个例子,用来讲明:promise分装一个须要“异步”的流程。
Ref: 浅谈ES6的Promise对象
Jeff: 在某种状况下执行预先设定好的方法,可是使用它却可以让代码变得更简洁清晰
使用已经较为成熟的有大量小伙伴使用的第三方Promise库,下面就为小伙伴推荐一个—— Bluebird
Promise内部的setTimeout这样的函数,
执行成功:走then这个策略;
执行失败:应该走error的策略。
/* 延迟执行 */
// 1.返回一个实例,一段时间之后才会发生的结果(算是一种承诺)
function timeout(ms) { return new Promise( (resolve, reject) => { setTimeout(resolve, ms, 'done'); // [开始异步流程] } ); } timeout(100).then( (value) => { // 2.过了指定的时间(参数)之后,实例的状态变为,就会触发方法绑定的回调函数 console.log(value); } );PromisemsPromiseresolvedthen
then的方法执行优先级略低。
/* 当即执行 */
// 1.Promise 新建后就会当即执行
let promise = new Promise(function(resolve, reject) { console.log('Promise'); // [开始异步流程] resolve(); }); promise.then(function() { console.log('resolved.'); // 2. 方法指定的回调函数,将在当前脚本全部同步任务执行完才会执行 }); console.log('Hi!'); // Promise // Hi! // resolvedthen
function loadImageAsync(url) {
// 使用包装了一个图片加载的异步操做 return new Promise( function(resolve, reject){ const image = new Image(); // [开始异步流程] image.onload = function() { // --> 加载成功,就执行这个 resolve(image); }; image.onerror = function() { // --> 加载失败,就执行这个 reject(new Error('Could not load image at ' + url)); }; image.src = url; }); }Promise
参数promise
若是调用resolve
函数和reject
函数时带有参数,那么它们的参数会被传递给回调函数。
- reject
函数的参数一般是Error
对象的实例,表示抛出的错误;
- resolve
函数的参数除了正常的值之外,还多是另外一个 Promise 实例,好比像下面这样。
"p2
的resolve
方法将p1
做为参数,即一个异步操做的结果是返回另外一个异步操做"
注意,这时p1
的状态就会传递给p2
,也就是说,p1
的状态决定了p2
的状态。
若是p1
的状态是pending
,那么p2
的回调函数就会等待p1
的状态改变;
若是p1
的状态已是resolved
或者rejected
,那么p2
的回调函数将会马上执行。
Detail:
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) // 1. 是一个 Promise,3 秒以后变为 }) const p2 = new Promise(function (resolve, reject) { // 2. 的状态在 1 秒以后改变,方法返回的是 setTimeout(() => resolve(p1), 1000) // 3. 返回的是另外一个 Promise,致使本身的状态无效了,被p1决定 }) p2.then(result => console.log(result)) // 4. 语句都针对的是后者() .catch(error => console.log(error)) // 5. 又过了 2 秒,变为,致使触发方法指定的回调函数。 // Error: failp1rejectedp2resolvep1p2p2thenp1p1rejectedcatch
注意:调用resolve
或reject
并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => { resolve(1); // 当即 resolved 的 Promise 是在本轮事件循环的末尾执行,老是晚于本轮循环的同步任务【变为:return resolve(1)就不会执行后面的了】 console.log(2); // 仍然会执行,而且仍是首先打印出来【后继操做应该放到方法里面,而不该该直接写在或的后面】 }).then(r => { console.log(r); }); // 2 // 1thenresolvereject
/* implement */
Ref:nodejs与Promise的思想碰撞【有必要一读】
RUAN
以上只是基础概念,要进入实践体系,须要研读下面四篇文章。
《深刻掌握 ECMAScript 6 异步编程》系列文章
ES2017 标准引入了 async 函数,使得异步操做变得更加方便,它就是 Generator 函数的语法糖。
那么,Generator函数是什么?是协程在 ES6 的实现。
Ref: Generator 函数的语法
Ref: Generator 函数的异步应用
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数彻底不一样。
Generator 函数有多种理解角度。
- 状态机
语法上,首先能够把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
- 遍历器对象生成函数
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,仍是一个遍历器对象生成函数。
返回的遍历器对象,能够依次遍历 Generator 函数内部的每个状态。
- 两大特色
形式上,Generator 函数是一个普通函数,可是有两个特征。
(1) function
关键字与函数名之间有一个星号;
(2) 函数体内部使用yield
表达式,定义不一样的内部状态。
function* helloWorldGenerator() { yield 'hello'; // 状态一 yield 'world'; // 状态二 return 'ending'; // 状态三 } var hw = helloWorldGenerator(); // 不会当即执行,返回的是:一个指向内部状态的指针对象
下一步,必须调用遍历器对象的next
方法,使得指针移向下一个状态。
调用 Generator 函数,返回一个遍历器对象,表明 Generator 函数的内部指针。
之后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。
value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;
done属性是一个布尔值,表示是否遍历结束。
提供了一种能够暂停执行的函数。yield
表达式就是暂停标志。
(1)遇到yield表达式,就暂停执行后面的操做,并将紧跟在yield后面的那个表达式的值,做为返回的对象的value属性值。 (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。 (3)若是没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,做为返回的对象的value属性值。 (4)若是该函数没有return语句,则返回的对象的value属性值为undefined。
为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
Generator 函数能够不用yield
表达式,这时就变成了一个单纯的暂缓执行函数。
由于:函数f
若是是普通函数,在为变量generator
赋值时就会执行。
注意:yield
表达式只能用在 Generator 函数里面,用在其余地方都会报错。
如若否则:瞧这个反例子
var arr = [1, [[2, 3], 4], [5, 6]]; var flat = function* (a) { a.forEach(function (item) { if (typeof item !== 'number') { yield* flat(item); } else { yield item; } }); }; for (var f of flat(arr)){ console.log(f); }
function* outer() { yield 'open' yield inner() // --> (A) yield 'close' } function* inner() { yield 'hello!' }
(A) 加了星号,意思为:看这个表达式的本质,而非表象。
== yield inner() == var gen = outer() gen.next() // -> 'open' gen.next() // -> a generator,这是表象 gen.next() // -> 'close' == yield* inner() == var gen = outer() gen.next() // -> 'open' gen.next() // -> 'hello!',这是表象背后的本质 gen.next() // -> 'close'
"传值调用"(call by value)
"传名调用"(call by name)
编译器的"传名调用"实现,每每是将参数放到一个临时函数之中,再将这个临时函数【Thunk函数】传入函数体。
function f(m){ return m * 2; } f(x + 5); // 等同于 var thunk = function () { return x + 5; }; function f(thunk){ return thunk() * 2; }
在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数做为参数。
// 正常版本的readFile(多参数版本) fs.readFile(fileName, callback);
-------------------------------------------- // Thunk版本的readFile(单参数版本) var readFileThunk = Thunk(fileName); readFileThunk(callback); var Thunk = function (fileName){ return function (callback){ return fs.readFile(fileName, callback); }; };
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。
生产环境的转换器,建议使用 Thunkify 模块。
做为Generator 函数的流程管理而使用。
var fs = require('fs'); var thunkify = require('thunkify'); var readFile = thunkify(fs.readFile); var gen = function* (){ var r1 = yield readFile('/etc/fstab'); console.log(r1.toString());
var r2 = yield readFile('/etc/shells'); console.log(r2.toString()); };
目的:co 能够自动执行 Generator 函数。
好比,有一个 Generator 函数,用于依次读取两个文件。
var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
co 函数库可让你不用编写 Generator 函数的执行器。
var co = require('co'); co(gen); // ----> 使gen自动执行
上面代码中,Generator 函数只要传入 co 函数,就会自动执行。
co 函数返回一个 Promise 对象,所以能够用 then 方法添加回调函数。
co(gen).then(function (){ console.log('Generator 函数执行完成'); })
上面代码中,等到 Generator 函数执行结束,就会输出一行提示。
当异步操做有告终果,可以自动交回执行权。
两种方法能够作到这一点。
(1)回调函数。将异步操做包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操做包装成 Promise 对象,用 then 方法交回执行权。
co 函数库其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。
使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。
(1) 基于 Thunk 函数的自动执行器。【上一部分】
(2) 基于 Promise 对象的自动执行器。【以下】
只要 Generator 函数还没执行到最后一步,next 函数就调用自身,以此实现自动执行。
var fs = require('fs'); var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) reject(error); resolve(data); }); }); }; var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
而后,手动执行上面的 Generator 函数。
var g = gen(); g.next().value.then(function(data){ g.next(data).value.then(function(data){ g.next(data); }); })
手动执行其实就是用 then 方法,层层添加回调函数。理解了这一点,就能够写出一个自动执行器。
function run(gen){ var g = gen(); function next(data){ var result = g.next(data); if (result.done) return result.value; result.value.then(function(data){ next(data); }); } next(); } run(gen);
异步I/O不就是读取一个文件吗,干吗要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是否是异步。
async 函数就是隧道尽头的亮光,不少人认为它是异步操做的终极解决方案。
一句话,async 函数就是 Generator 函数的语法糖。
var fs = require('fs'); var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) reject(error); resolve(data); }); }); }; # 一个 Generator 函数,依次读取两个文件 var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; # 写成 async 函数,就是下面这样 var asyncReadFile = async function (){ var f1 = await readFile('/etc/fstab'); var f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
var sleep = function (time) { return new Promise(function (resolve, reject) { setTimeout(function () { // 模拟出错了,返回 ‘error’ reject('error'); }, time); }) }; var start = async function () { try { console.log('start'); await sleep(3000); // 这里获得了一个返回错误 // 因此如下代码不会被执行了 console.log('end'); } catch (err) { console.log(err); // 这里捕捉到错误 `error` } };
故,能够理所固然的写在for
循环里,没必要担忧以往须要闭包
才能解决的问题。
..省略以上代码 var start = async function () { for (var i = 1; i <= 10; i++) { console.log(`当前是第${i}次等待..`); await sleep(1000); } };
await
必须在async函数的上下文中
..省略以上代码 let 一到十 = [1,2,3,4,5,6,7,8,9,10]; // 错误示范 一到十.forEach(function (v) { console.log(`当前是第${v}次等待..`); await sleep(1000); // 错误!! await只能在async函数中运行 }); // 正确示范 for(var v of 一到十) { console.log(`当前是第${v}次等待..`); await sleep(1000); // 正确, for循环的上下文还在async函数中 }
import fs from 'fs'; import path from 'path'; import request from 'request'; var movieDir = __dirname + '/movies', exts = ['.mkv', '.avi', '.mp4', '.rm', '.rmvb', '.wmv'];
///////////////// // 读取文件列表
/////////////////
var readFiles = function () { return new Promise(function (resolve, reject) { fs.readdir(movieDir, function (err, files) { resolve(files.filter((v) => exts.includes(path.parse(v).ext))); }); }); };
///////////////// // 获取海报
/////////////////
var getPoster = function (movieName) { let url = `https://api.douban.com/v2/movie/search?q=${encodeURI(movieName)}`; return new Promise(function (resolve, reject) { request({url: url, json: true}, function (error, response, body) { if (error) return reject(error); resolve(body.subjects[0].images.large); }) }); };
/////////////// // 保存海报
///////////////
var savePoster = function (movieName, url) { request.get(url).pipe(fs.createWriteStream(path.join(movieDir, movieName + '.jpg'))); }; ////////////////////////////////////////////////////////////////////////////////////////
(async () => { let files = await readFiles(); // await只能使用在原生语法 for (var file of files) { let name = path.parse(file).name; console.log(`正在获取${name}的海报`); savePoster(name, await getPoster(name)); } console.log('=== 获取海报完成 ==='); })();