#Javascript异步介绍

前言

image

熟悉前端的同学对JavaScript的第一印象是什么?不论是弱类型、脚本语言、异步、原型...但用过的同学都对一个特性又爱又恨,那就是异步。本文会首先从异步的原理开始,介绍一些异步编程的方法,从jQuery中的异步到Promise、Generator,再到async/wait,一步一步讲解,内容尽量通俗易懂,用最简单的例子。

开发中总是会遇到各种异步问题,今天粗略的说下JS的异步,抛块砖,讲下异步发展,并没有太深入挖掘,本篇幅稍微有点长,需要有点耐心,如果比较了解的同学可以直接跳过,内容有错误或不恰当的地方请不吝指出,欢迎大家指正、交流。

1、异步

1.1 为什么要有异步

在浏览器中,JS是单线程、异步执行的。单线程,就是同一时刻JS引擎只能执行一段代码,浏览器是直接面对用户的,而且往往一个页面会有很多请求,如果所有请求都是同步的,那体验就太糟了,所以请求采用异步,避免用户长时间的等待。

而Node中,“一切皆异步”的思想,更是指出了异步的重要性,目的也是让猿儿们编写高效的程序,不因为请求或DB操作而阻塞了服务。

1.2 异步原理

先来说下同步,同步就是事件1干完,再干事件2,事件1干完之前,事件2只能傻傻的等待。如排队上厕所,前面那个人完事出来你才能进去舒服一下,否则...自己脑补吧,画面太美不敢看。

同步

// doSomething1

var s = Date.now();

for (var i = 0; i < 100000000; i++) {

// ...

}

console.log(Date.now() - s);    // 约330ms

// doSomething2

上面的为同步,在doSomething2开始之前,必须等待350ms才能开始doSomething2,因为浏览器在执行for的时候,干不了别的。

异步,是为了解决“傻傻的等待”的问题,还是上厕所问题,但是这次加了一步,先拿号,然后等待叫号,等轮到你的号通知你之后,直接去厕所就行了,而在这等待期间,你完全可以来两局王者农药,不耽误你干别的。


// 1、异步1

setTimeout(function () {

// doSomething

}, 1000)

// 2、异步2

$.ajax({

url: '/test/data.json',

success: function() {

console.log('success');

}

});

像上面这种不立即执行,而是等待有了结果之后,再去执行的函数,称为callback,即回调函数。

异步的原理就是将callback作为参数传递给异步执行的函数,等有结果之后再去调用callback执行。

1.3 常见异步

开发中常见的异步操作有:

  • 网络请求,如ajax,request

  • IO操作,如fs.readFile,DB的CRUD

  • 定时函数,如setTimeout, setInterval

  • 事件监听,如$btn.on('click', callback)

1.4 结束语

异步是一开始就有的,但是怎么把异步从回调地狱中解放出来,则是一步一步发展的。jQuery大家都很熟悉,基本是个前端同学都必用过的,下面先说下它中的异步解决方案。

2、jQuery异步解决方案

jQuery中异步很多,本文主要讲述 $.ajax的变化。

2.1 v1.5版本之前的异步

在1.5版本之前,ajax主要是通过回调函数的写法来实现的:


var ajax = $.ajax({

url: '/test/data.json',

success: function() {

console.log('success');

},

err: function() {

console.log('err')

}

})

console.log(ajax); // 返回的是一个XHR对象

这种是传统的callback的写法,单看这一层还好,如果有三层甚至更多,那么会看到代码呈“>”状,一层层的缩进,代码阅读很糟,也不便于维护。

2.2 v1.5版本以及之后的异步

2011年1月31日,jQuery v1.5发布,重写了ajax的API,ajax写法如下:


var ajax = $.ajax('/test/data.json')

.done(function() {

console.log('done');

})

.fail(function() {

console.log('fail');

})

.always(function() {

console.log('finished');

});

console.log(ajax); // 返回的是一个deferred对象

可以看到,这次采用了链式调用的写法,返回的是一个deferred对象,何为deferred对象,请移驾此处:deferred对象。

链式写法的好处,不用把所有请求都丢到callback里,明确了成功就放入done,失败放入fail,如果成功后有很多步骤,可以写很多done,然后链式起来就行了。熟悉Promise的同学是不是觉得有一点熟悉,如果看不出来,那么上面的写法还可以像这么写:


var ajax = $.ajax('/test/data.json')

.then(function() {

console.log('success');

}, function() {

console.log('err');

})

.then(function() {

console.log('success');

}, function() {

console.log('err');

})

就是用then来代替done、fail,then两个参数,第一个是doneCallback,第二个是failCallback。是不是与Promise更像了。

2.3 结束语

在这章节说下jQuery的变化,也是为了说明JS异步发展的一个过程,由callback到链式的写法,jQuery从一开始的callback到之后的then链式调用,其实也为之后的Promise奠定了基础,下面先讲下async的处理方式,然后就轮到Promise。

3、Async.js

讲Promise之前,先说下Async.js,像Promise,它是需要学习成本的,有人不想用Promise,但是多层回调嵌套又确实很恶心人,所有就有了async、then等库的诞生,这些库并没有用到Promise,能以优美的方式去书写异步,只是callback的语法糖,但是也可以一定程度逃离“回调地狱”了。

下面简单介绍下Async,它可以用在browser跟node端,它的方法很多,具体可移驾Github Async.js,挑出几个常用的,看看它的写法。

  • async.series(tasks, [callback]): 顺序执行数组或集合内的函数,执行完一个就执行下一个,错误可在callback中获得

var async = require('async');

async.series([

function (callback) {

callback(null, 'ok1'); // 为了方便,直接返回字符串

},

function (callback) {

callback(null, 'ok2');

}

], function (err, data) {

console.log(data); // ['ok1', 'ok2']

});

  • async.parallel(tasks, [callback]): 并行执行数组、集合内的函数

async.parallel([

function (callback) {

callback(null, 'ok1');

},

function (callback) {

callback(null, 'ok2');

}

], function (err, data) {

console.log(data); // ['ok1', 'ok2']

});

  • async.waterfall(tasks, [callback]): 瀑布流方式,任务依次执行,前一个函数的回调,会作为后一个函数的参数

async.waterfall([

function (callback) {

callback(null, 'ok1', 'ok2');

},

function (arg1, arg2, callback) {

// 此处 arg1='ok1', arg2='ok2'

callback(null, 'ok3');

}

], function (err, data) {

console.log(data); // 'ok3'

});

可以看出,对于连续多个任务或请求,利用async库可以轻易的把他们放到同一级(数组或集合)来执行,避免了callback的多层嵌套。所以这个库很受欢迎,githut的star将近23k,但是如果想顺应时代发展,特别是ES6,甚至ES7,Promise还是有必要学下的,接下来是Promise。

4、Promise

callback一层嵌一层的回调,导致了金字塔问题的出现,也即callhack hell,写个代码都不能愉快的写了。所有新兴事物的快速发展一定是戳中了原来的一些痛点。

在开发者的千呼万唤中,终于,2015年6月份,ES2015规范正式发布,也是JavaScript的20周年,ES6的发布,也标志着JS开始升级为企业级大型应用的开发语言。Promise也正式加入到ES6,成为一个原生对象,可以直接用。

4.1 什么是Promise

Promise是一个拥有then方法的函数或对象,一个Promise对象可以理解为一次将要执行的操作,主要是异步操作,之后可以用一种链式调用的方式来组织代码。目前Promise的规范是Promise/A+规范,核心内容如下:

I

  • 状态:一个Promise只有3种状态:pending(等待), fulfilled(已完成)或rejected(已拒绝),且必须在其中状态之一。

  • 状态只能从 pending-->fulfilled,或者 pending-->rejected,不能逆向转换,fulfilled、rejected也不能相互转换。

  • then方法:一个Promise必须提供一个then方法来获取其值,而且then必须返回一个Promise,以供链式调用。

  • then方法接收两个可选参数,promise.then(onFulfilled, onRejected)

  • onFulfilled:pending-->fulfilled时调用,onRejected:pending-->rejected时调用。

图片1

4.2 基本用法

  • 先来看一个fs的异步读取文件的方法:

var fs = require('fs');

var read = function(fileName) {

fs.readFile(fileName, function (err, data) {

if (err) {

console.log(err);

} else {

console.log(data.toString());

}

});

}

然后用Promise对fs.readFile进行封装:


var fs = require('fs');

// readPromise这个方法以后会多次使用

var readPromise = function(fileName) {

// 把fs.readFile用Promise包装一层

var promise = new Promise(function (resolve, reject) {

fs.readFile(fileName, function (err, data) {

if (err) {

reject(err); // 失败就reject出去

} else {

resolve(data.toString()); // 成功就resolve出去

}

});

})

return promise; // 最后返回一个promise对象

}

大家注意看程序中注释的部分,Promise的callback中有两个非常重要的参数:resolvereject

resolve方法:使Promise对象状态变化 pending-->fulfilled,即等待状态变为已完成,表示成功,resolve方法的参数用于成功之后的操作,此处就是获得的文件的内容。

reject方法:使Promise对象状态变化 pending-->rejected,即等待状态变为已拒绝,表示失败,reject方法的参数用于失败之后的操作,此处就是失败的原因。

通过上小节的规范可以知道,Promise对象都有then方法,所以readPromise方法可以这么用:


readPromise('./test.txt')

.then(function (data) {

console.log(data); // 上面代码中的resolve回的值

}, function (err) {

console.log(err); // 上面代码中的reject回的值

});

then有两个参数,第一个是成功之后的callback,第二个是失败之后的callback,而参数分别是上步包装的resolve与reject函数的参数。

上面还有种写法,就是then只接受一个参数,表示成功之后的操作,后续跟上catch方法,捕获reject的异常:


readPromise('./test.txt')

.then(function (data) {

console.log(data); // 上面代码中的resolve回的值

})

.catch(function (err) {

console.log(err); // 上面代码中的reject回的值

});

上面这种写法更清晰点。

4.3 参数传递

理解Promise的参数传递是很重要的,这样才能得到自己想要的数据。上面已经讲了,resolve的数据会在第一个then接收,reject的数据会在catch接收。因为then返回的还是Promise,所以then可以链式调用,如想对上面的test.txt的数据进行处理,则可以继续then下去:


readPromise('./test.txt')

.then(function (data) {

console.log(data); // resolve回的值

return data; // 此处return的data,将在下个then的参数处获得

})

.then(function (data) {

console.log(data + " 数据已经处理了~"); // 此处的data就是上个then里return回的数据

})

.catch(function (err) {

console.log(err); // 上面代码中的reject回的值

});

then链式操作中返回的值,将会在下个步骤处获得,而如果返回的是一个Promise,那么下个then处获得的就是Promise的第一个then的值。这句话怎么理解,来看个例子,我想读取test1.txt之后,再读取test2.txt,传统callback处理以及Promise处理对比:


// 普通回调,层层嵌套

fs.readFile('./test1.txt',function (err1, data1) {

if (err1) {

console.log(err1);

} else {

console.log(data1);

// 然后再读取第二个文件

fs.readFile('./test1.txt', function (err2, data2) {

if (err2) {

console.log(err2);

} else {

console.log(data2);

}

});

}

});

// Promise方式

var read1 = readPromise('./test1.txt');

var read2 = readPromise('./test2.txt');

read1.then(function (data1) {

console.log(data1); // 此处是test1.txt的内容

return read2; // 此处返回的是read2,一个Promise对象

})

.then(function (data2) {

console.log(data2); // 此处是上一步返回的read2的then,所以打印的是test2.txt的内容

})

对比可以发现,Promise方式更优雅,也更容易看懂,这只是读取2个文件,如果读取三个甚至更多,那用Promise就更方便了,当然如果不需要读取的有依赖关系,则可用Promise对象的all或race方法。

如果想读取test1.txt, text2.txt的内容,读完再做其他操作,则可以如下:


var read1 = readPromise('./test1.txt');

var read2 = readPromise('./test2.txt');

Promise.all([read1, read2])

.then(function (datas) {

console.log(datas[0]); // test1.txt的内容

console.log(datas[1]); // test2.txt的内容

});

如果想读取test1.txt, text2.txt的内容,但是只要有一个返回就可以做其他操作,谁执行的快就用谁,则可以如下:


var read1 = readPromise('./test1.txt');

var read2 = readPromise('./test2.txt');

Promise.race([read1, read2])

.then(function (data) {

console.log(data); // 先读取完那个文件的内容

});

有人说还看到过Promise.resolve,它的作用是把一个thenable对象转换为Promise对象,如下:


// thenable对象,有then属性,且属性值如下

var thenable = {

then: function (resolve, reject) {

resolve('success');

}

}

// 把thenable对象转换为Promise对象

var thenToPromise = Promise.resolve(thenable);

// 然后就可以这么用了

thenToPromise.then(function (data) {

console.log(data); // 'success'

});

4.4 相关库

实际开发中,使用原生的Promise当然可以,不过市面上有现成的第三方库,而且很好用,比较流行的是Q、Bluebird等。他们都可用于浏览器端以及node端,并且可以在不支持Promise的环境中使用,至于用那个,则看个人爱好了,bluebird号称Promise库里最快的,比原生的Promise都快,其实原生的Promise比传统的callback慢不少。

这里介绍Q.js一些基本的用法,引用官网的一个例子,再次体验下传统回调与Promise库之间的对比:


// 传统回调

step1(function (value1) {

    step2(value1, function(value2) {

        step3(value2, function(value3) {

            step4(value3, function(value4) {

                // Do something with value4

            });

        });

    });

});

// 用Q

Q.fcall(promisedStep1)

.then(promisedStep2)

.then(promisedStep3)

.then(promisedStep4)

.then(function (value4) {

    // Do something with value4

})

.catch(function (err) {

    // Handle any err from all above steps

})

.done();

可以看到,传统回调方式也不错嘛,也有美感,but,这只是简写,如果加上各种异常判断,还有其他操作,那么维护起来很麻烦,也容易出错。而用Q,则清晰了很多,一步完成之后继续下一步,比较符合人的思维,这里看到一个Q的用法:Q.fcall,常用的方法有:Q.fcall, Q.nfcall, Q.nfapply, Q.defer, Q.all, Q.any等。用法都放到一段代码里:


var Q = require('q');

var fs = require('fs');

// Q.fcall: 接收函数或defer实例,返回一个Promise对象

var promiseFcall = Q.fcall(function () {

return 'hello';

});

// Q.nfcall: Node function call, 处理callback是这种形式的:function(err, result),可以直接封装成Promise

var promiseNfcall = Q.nfcall(fs.readFile, './test.txt', 'utf-8');

// Q.nfapply: 与Q.nfcall类似,只是参数不一样,很像js的call与apply用法

var promiseNfapply = Q.nfapply(fs.readFile, ['./test.txt', 'utf-8']);

// Q.defer: 可以定义Promise生成器,如果浏览器不支持Promise,则比较有用,很像原生Promise的写法

var promiseDefer = function(fileName) {

var defer = Q.defer();

fs.readFile(fileName, function (err, data) {

if (err) {

defer.reject(err);

} else {

defer.resolve(data.toString());

}

})

}

// Q.all: 与Promise.all类似

var read1 = Q.nfcall(fs.readFile, './test1.txt', 'utf-8');

var read2 = Q.nfcall(fs.readFile, './test2.txt', 'utf-8');

Q.all([read1, read2], function (data) {

console.log(data[0]);

console.log(data[1]);

});

// Q.any: 与Promise.race类似

Q.any([read1, read2])

.then(function (data) {

    console.log(data);

});

以上只是简单介绍了最基本的用法,具体可以自行去github上看下。

4.5 结束语

到此,Promise差不多介绍完了,当然Promise还有很多用法,就不一一列举了,那么Promise有没有改变callback的本质?并没有,Promise只是换了种对异步的写法,优化了对代码的可读性,其实还是依赖callback,获得的数据,还是在then的callback里获取到的。上面看到需要的数据,还是在callback中获得的,还没有真正像同步那样的写法,如果用Generator配合Promise,则写法就完全不同了,接下来进入Generator。

5、Generator

5.1 协程

介绍Generator前,先讲下协程,协程最初诞生是为了解决低速IO与高速的CPU之间协作问题,协程是指多个线程交互协作,完成异步任务,大概流程如下:

  1. 协程A开始运行

  2. 执行到某处,暂停,然后执行权交给协程B

  3. 一段时间后,协程B交换执行权给协程A

  4. 协程A恢复执行

还以读取文件为例,代码表示如下:


function asyncFunction() {

// doSomething1

yield readFile('./test.txt');

// doSomething2

}

上面函数asyncFunction就是一个协程,一开始执行doSomething1,当遇到yield后,自身先暂停,执行权移交给readFile,当readFile执行完之后,执行权又交还回asyncFunction,然后接着执行doSomething2。

5.2 什么是Generator

Generator(生成器)可以说是协程在ES6中的实现,它最大的特点是:可以交出执行权,暂停执行。先看一个简单的Generator写法:


function* gen() {

yield 'hello';

yield 'world';

return 'ok';

}

var g = gen();

g.next(); // {value: "hello", done: false}

g.next(); // {value: "world", done: false}

g.next(); // {value: "ok", done: true}

g.next(); // {value: undefined, done: true}

这看上去像是一个函数,所以也可以称为Generator函数,但是要明白,Generator并不是函数,它与普通函数有几点区别:

  • 以function* 开始,注意这个*

  • 内部有一个 yield 关键字,跟return有点像,不同是yield可以有多个

Generator返回的其实是一个Iterator对象,下面先说下Iterator迭代器。

5.3 Iterator迭代器

在讲Iterator之前,先说下ES6新引入的一个基本类型:Symbol。

ES6之前JS有6个基本数据类型:string, object, null, boolean, undefined, number。现在增加一个:Symbol,表示独一无二的值。

Symbol不能用new关键字,因为是一个原始类型的值,不是对象,所以也不能添加属性,可理解为类似字符串数据类型。它可以接收一个字符串参数,主要是为了控制台显示或转换为字符串时容易区分:


var s1 = Symbol();

var s2 = Symbol();

s1 == s2 // false

s1 = Symbol('foo');

s2 = Symbol('foo');

s1 // Symbol(foo);

typeof s1 // 'symbol'

s1 == s2 // false

Symbol也可以作为对象的属性key来使用:


var obj = {

a: 'foo',

[Symbol.iterator]: 'foo2'

}

console.log(obj); // {a: "foo", Symbol(Symbol.iterator): "foo2"}

Symbol有个iterator属性,指向该对象的默认遍历器方法。在ES6中有些原生就具有[Symbol.iterator]属性,如数组、Set、Map(也是ES6新引进的)、arguments对象等,这些对象有个特点,就是可以用for...of循环遍历:


var arr = ['foo1', 'foo2', 'foo3'];

for (var i of arr) {

console.log(i); // 'foo1' 'foo2' 'foo3' 这里注意i为value,并不是对应的key

}

Iterator对象:具有[Symbol.iterator]属性的数据,都可以生成一个Iterator对象,而怎么使用Iterator对象,有两种方式: next(), for...of。以数组举例:


var arr = ['foo1', 'foo2', 'foo3'];

var it = arr[Symbol.iterator](); // 生成arr的iterator对象

// next

it.next(); // {value: "foo1", done: false}

it.next(); // {value: "foo2", done: false}

it.next(); // {value: "foo3", done: false}

it.next(); // {value: undefined, done: true}, done=true表示获取完成

// for...of,这种用法不会遍历到return的数据

for (var i of it) {

console.log(i); // 'foo1' 'foo2' 'foo3'

}

Generator,就是天生的Iterator对象,所以才有next(),也可以用for...of遍历,针对一开始的例子,现详细解释一下:


function* gen() {

yield 'hello';

yield 'world';

return 'ok';

}

var g = gen();

g.next(); // {value: "hello", done: false}

g.next(); // {value: "world", done: false}

g.next(); // {value: "ok", done: true}

g.next(); // {value: undefined, done: true}

  • 首先定义Generator gen,注意声明用function*

  • var g = gen()这步生成Generator对象,但是并没有立即执行代码,处于暂停状态

  • 第一个g.next()会激活状态,开始执行代码,直到遇到第一个yield,此时返回yield之后的数据,再次进入暂停状态

  • 第二个g.next()与之前的类似,最后返回结果,进入暂停

  • 第三个g.next()也是先激活,但是遇到了return,所以就结束了,返回return的数据,已经结束了,此时done=true

  • 第四个g.next(),此时因为已经结束,所以只能返回value=undefined, done=true

注意,每次next返回的数据,都是{value:xxx, done:xxx}格式。

5.4 yield、next

上面其实已经用到yield、next了,这儿再详细说下。

  • yield* : yield 可以返回一个值或一个表达式,但还可以 yield* 这么用,在Generator里面再套一个Generator:

function* gen() {

yield 'a';

yield 'b';

}

function* gen2() {

yield 'c';

yield* gen();

yield 'd';

}

var g = gen2();

g.next(); // {value: "c", done: false}

g.next(); // {value: "a", done: false}

g.next(); // {value: "b", done: false}

g.next(); // {value: "d", done: true}

  • next: next也可以向yield传递参数:

function* gen() {

var a = yield 'a';

console.log(a); // 100

var b = yield 'b';

console.log(b); // 200

yield 'c';

}

var g = gen();

g.next(); // {value: "a", done: false}

g.next(100); // {value: "b", done: false}

g.next(200); // {value: "c", done: false}

g.next(100)是将100传递给上一个已经执行完了的yield的变量,请各位自己先看下是否能准确判断a,b的值以及每个next的返回值。

  • 第一个next返回当然是value='a'

  • 第二个next,传递100给a变量,所以console.log(a)打印100,然后next返回的是'b'

  • 第三个next同上,200传递给b变量,打印200,然后next返回'c'

讲了这么多,还没到Generator怎么跟异步联系,马上了,下面先说下Thunk函数,已经怎么把Thunk与异步联系起来。

5.5 Thunk

其实Thunk函数并不是Generator的一部分,这节重在介绍Generator,所以放在此处。

  • Thunk函数:将多参数函数替换成单参数函数,只接受一个参数,并且参数是回调函数。任何函数,只要含有回调函数,都可以写成Thunk函数形式。

看一个例子,以fs.readFile为例:


// 1、多参数函数

fs.readFile(fileName, calback);

// 2、定义一个fs的thunk转换器

var thunk = function (fileName) {

return function (callback) {

return fs.readFile(fileName, callback);

}

}

// readFileThunk为Thunk函数,用的时候,只传入callback就行

var readFileThunk = thunk(fileName);

readFileThunk(callback);

看着是不是又复杂了...是的,不过现在的复杂是为以后的简单准备的,具体后续会讲。手动写Thunk方法比较麻烦,所以出现了第三方库:thunkify。

  • thunkify: 封装了thunk转换器,可以简化写法:

var fs = require('fs');

var thunkify = require('thunkify');

var readFile = thunkify(fs.readFile);

var readFileThunk = readFile(fileName);

readFileThunk(callback);

  • Thunk配合Generator

先看个例子:


var fs = require('fs');

var thunkify = require('thunkify');

var readFile = thunkify(fs.readFile);

var gen = function* () {

var data1 = yield readFile('./test1.txt');

console.log(data1.toString()); // test1.txt的内容

var data2 = yield readFile('./test2.txt');

console.log(data2.toString()); // test2.txt的内容

}

看上面的代码,获取data1, data2跟同步写法是否基本一样?想读取那个文件,直接按顺序写就行,不用callback里获得结果,或者then中获得结果,只是前面多了一个yield关键字,是不是很爽?

上面说过,yield会把程序的执行权移出gen函数,但是怎么交换回来呢?这就是Thunk函数的妙用了,它可以用于Generator函数的自动流程管理。下面是一个基于Thunk函数的Generator执行器:


// 执行器

function run(generator) {

var gen = generator();

// 这个next其实就是Thunk函数的回调函数

function next(err, data) {

var res = gen.next(data); // 类似{value: Thunk函数, done: false}

if (res.done) {

return;

}

res.value(next); // res.value是一个Thunk函数,而参数next就是一个callback

}

next();

}

var gen = function* () {

var data1 = yield readFile('./test1.txt');

var data2 = yield readFile('./test2.txt');

}

// run执行Generator函数

run(gen);

其实,要用Generator解决回调地狱问题,需要首先处理一下调用的函数,使函数正确执行后能够自动执行next方法,并且传递执行完方法后的结果。

  • Promise配合Generator

yield 后能跟Thunk函数,也可以跟Promise对象,所以Promise也可以配合Generator解决回调地狱问题。下面是一个基于Promise的Generator执行器:


// 执行器

function run(generator) {

var gen = generator();

function go(res) {

// res类似{value: Promise对象, done: false}

if (res.done) {

return res.value;

}

// Promise对象有then方法,两个参数为doneCallback,failCallback

return res.value.then(function (data) {

return go(gen.next(data));

}, function (err) {

return go(err);

})

}

go(gen.next());

}

var gen = function* () {

var data1 = yield readPromise('./test1.txt');

var data2 = yield readPromise('./test2.txt');

}

// run执行Generator函数

run(gen);

每次写生成器函数很麻烦,所以TJ大神写了一个co库,下面介绍。

5.6 co

co库可以自动执行Generator函数,其实就是类似上面的run方法,Generator是一个异步操作容器,自动执行需要交换执行权给Generator,有两种方法可以做到:

  • 回调函数,将异步操作包装成Thunk函数,在回调函数里执行交换执行权

  • Promise对象,将异步操作包装成Promise对象,用then里执行交换执行权

co库就是将两种执行器包装成一个库,所以使用co的时候,yield后面只能是Thunk函数或Promise对象。co现在返回的是一个Promise对象(之前版本返回的是Thunk函数),co非常好用,把刚才的代码重新,将会非常简单:


var co = require('co');

var gen = function* () {

var data1 = yield readPromise('./test1.txt');

var data2 = yield readPromise('./test2.txt');

}

// co执行Generator函数

co(gen);

5.7 结束语

Generator终于讲完了,配合Thunk函数或Promise对象,确实比之前的callback或then链式调用“顺畅”了很多,很像同步的写法,已经很符合人的顺序执行的思维了。其实Generator的本质是“暂停”,有了这个,才能让程序到一个地方先暂停,执行异步,然后执行完了再继续执行程序,这样就可以把操作连起来了。

可以看到Generator的异步处理,学习成本比较高,Generator、Thunk、Promise...等,都需要时间去学习,这显然还不够友好,所以在ES7中基于Promise实现了一套异步处理方案:async/await,这个才是最终的方案。

6、async/await

async/await是在ES7中实现的,目前好多浏览器不支持,node从7.0.0开始就支持使用--harmony-async-await来支持此功能,另外babel也已经支持async的transform了,使用的时候引入babel就行。

6.1 基本用法

先介绍下async/await:

  • 基于Promise实现的,不适用于普通的回调函数

  • 非阻塞的

  • 函数声明用async function, 遇到异步用await,且await只能放在async函数中

先看一段使用async/await的代码:


// 定义async函数,注意async关键字

var readAsync = async function() {

var data1 = await readPromise('./test1.txt'); // 注意await关键字

var data2 = await readPromise('./test2.txt');

return 'ok'; // 返回值可以在调用处通过then拿到

}

// 执行

readAsync();

// 或者

readAsync().then(function (data) {

console.log(data); // 'ok'

});

是不是非常简单,无需用co,直接执行就行。需要注意一点,await后只能跟Promise对象、字符串,数值等,不能跟Thunk函数,也暗示Promise可能是解决异步的最终方案。同时,async函数也默认返回的是一个Promise对象,函数最后可以return一个值,最后调用时在then里获取。

6.2 与Generator的对比

上面也看到,async/await与Generator的解决方案很像,区别如下:

  • 声明,async function 代替 function*

  • await 代替 yield

  • 内置执行器,可以直接运行,不需要co这种第三方库

6.3 结束语

其实async与Generator很像,是因为async相当于把Generator跟执行器进行了包装,是Generator的语法糖,但是也方便了很多,目前很多人认为async就是异步的最终方案。

小结

讲完了,里面可能有些例子不恰当,也参考了官网或别人的一些例子,总之尽量用简单的例子去铺开。

由于浏览器的特殊性,JS只能采用异步解决请求,从而性能也比较好,但是也带了了各种麻烦,所以人们从一开始就寻找它的同步写法,试图摆脱恶心的callback-hell,从一开始callback,到Promise对象,到Generator,再到async,异步方案是越来越好,也越来越优雅,随着ES7的普及,其实直接用async就好,不过技术发展总有一些过程,了解这些过程对我们的眼界扩展以及对这门语音会有更好的认识。

希望讲了这么多能帮助一些同学理解异步,有错误也轻及时指正,谢谢~最后再总览下法中中的几种写法,体验异步发展过程:

callback方式:


fs.readFile('./test1.txt', function (err1, data1) {

fs.readFile('./test2.txt', function (err2, data2) {

fs.readFile('./test2.txt', function (err2, data2) {

});

});

});

Promise方式:


readPromise('./test1.txt', function (data1) {

return readPromise('./test2.txt');

})

.then(function (data2) {

return readPromise('./test3.txt');

})

.then(function (data3) {

});

Generator方式:


var gen = function* () {

var data1 = yield readPromise('./test1.txt');

var data2 = yield readPromise('./test2.txt');

var data3 = yield readPromise('./test3.txt');

}

co(gen);

async/await方式:


var readAsync = async function() {

var data1 = await readPromise('./test1.txt');

var data2 = await readPromise('./test2.txt');

var data3 = await readPromise('./test3.txt');

}

readAsync();

你可能感兴趣的:(#Javascript异步介绍)