[JS] ECMAScript 6 - Async : compare with c#

一段引言:

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

 

 

 

开胃菜 - 四种方法


 

Ref: Javascript异步编程的4种方法

Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

*** "同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;

*** "异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

  

一、回调函数

function f1(callback){

  setTimeout(function () {  // ----> 将耗时的操作推迟执行,什么垃圾的初级思想,当然不可行
    // f1的任务代码
    callback();
  }, 1000);

}

  

二、事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生

// 当f1发生done事件,就执行f2。
f1.on('done', f2);
---------------------------------------------------------------------
function f1(){   setTimeout(function () {     // f1的任务代码
   // 执行完成后,立即触发done事件,从而开始执行f2
    f1.trigger('done');  
  }, 1000);
}

 

三、发布/订阅

这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

就是多出了一个“订阅中心”统一管理信号。

// f2向"信号中心"jQuery订阅"done"信号。
jQuery.subscribe("done", f2);
-----------------------------------------------------------------------
function f1(){   setTimeout(function () {     // f1的任务代码
    // f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行
    jQuery.publish("done");     }, 1000); }

 

四、Promises对象     (重点,接下来的内容)

f1的回调函数f2,回调函数变成了链式写法

 f1().then(f2);

f1要进行如下改写:

function f1(){
  var dfd = $.Deferred();
  setTimeout(function () {
    // f1的任务代码
    dfd.resolve();
  }, 500);

  return dfd.promise;
}

 

 

 

 

Promise 对象 - 最后也是最流行的一个


Ref: ECMAScript 6 入门 - Promise 对象

 

一、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.返回一个Promise实例,一段时间以后才会发生的结果(算是一种承诺)

function
timeout(ms) { return new Promise( (resolve, reject) => { setTimeout(resolve, ms, 'done');   // [开始异步流程] } ); } timeout(100).then( (value) => {  // 2.过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数 console.log(value); } );

 

  • 再来一个

then的方法执行优先级略低。

/* 立即执行 */
// 1.Promise 新建后就会立即执行

let
promise = new Promise(function(resolve, reject) { console.log('Promise'); // [开始异步流程] resolve(); }); promise.then(function() { console.log('resolved.');  // 2. then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行 }); console.log('Hi!'); // Promise // Hi! // resolved

 

  • 加载图片
function loadImageAsync(url) {
// 使用Promise包装了一个图片加载的异步操作
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

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。

 - reject函数的参数通常是Error对象的实例,表示抛出的错误;

 - resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。

 

"p2resolve方法将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. p1是一个 Promise,3 秒之后变为rejected
})

const p2 = new Promise(function (resolve, reject) {  // 2. p2的状态在 1 秒之后改变,resolve方法返回的是p1
  setTimeout(() => resolve(p1), 1000)           // 3. p2返回的是另一个 Promise,导致p2自己的状态无效了,被p1决定
})

p2.then(result => console.log(result))          // 4. then语句都针对的是后者(p1)
  .catch(error => console.log(error))           // 5. 又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。
// Error: fail

注意:调用resolvereject并不会终结 Promise 的参数函数的执行。

new Promise((resolve, reject) => {
  resolve(1);       // 立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务【变为:return resolve(1)就不会执行后面的了】
  console.log(2);  // 仍然会执行,并且还是首先打印出来【后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面】
}).then(r => {
  console.log(r);
});
// 2
// 1

 

/* implement */

 

Ref:nodejs与Promise的思想碰撞【有必要一读】

 

 

 

 

RUAN

以上只是基础概念,要进入实践体系,需要研读下面四篇文章。

 

《深入掌握 ECMAScript 6 异步编程》系列文章

 

 

Generator 函数


 

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表达式 与 next()

  • 基本示范

提供了一种可以暂停执行的函数。yield表达式就是暂停标志

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
遍历器对象的next方法的运行逻辑如下

为 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'

 

 

 

  

Thunk 函数


一、基本概念

"传值调用"(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 模块

生产环境的转换器,建议使用 Thunkify 模块

 

四、Thunk的用法

作为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 函数库


一、基本概念

目的: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 函数,就会自动执行。

 

  • 返回 Promise

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);

 

 

 

 

async 函数


 

异步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());
};

 

 

  • 支持 try...catch 方法
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`
    }  
};

 

  • await看起来就像是同步代码

故,可以理所当然的写在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('=== 获取海报完成 ==='); })();

 

posted @ 2018-04-15 18:06  郝壹贰叁  阅读(223)  评论(0编辑  收藏  举报