前言
熟悉前端的同学对 JavaScript 的第一印象是什么?不论是弱类型、脚本语言、异步、原型...但用过的同学都对一个特性又爱又恨,那就是异步。本文会首先从异步的原理开始,介绍一些异步编程的方法,从 jQuery 中的异步到 Promise、Generator,再到 async/wait,一步步讲解,内容尽量用最简单的例子来梳理、介绍。
开发中总是会遇到各种异步问题,现粗略的说下 JS 的异步,抛块砖,讲下异步发展,并没有太深入挖掘,否则文章太长,本篇幅稍微有点长,需要有点耐心,比较了解的同学可以直接跳过,内容有错误或不恰当的地方请及时指出,欢迎大家指正、交流。
-
异步
-
为什么要有异步
在浏览器中,JS是单线程、异步执行的。单线程,就是同一时刻 JS 引擎只能执行一段代码,浏览器是直接面对用户的,而且往往一个页面会有很多请求,如果所有请求都是同步的,那体验就太糟了,所以请求采用异步,避免用户长时间的等待。
而 Node 中,“一切皆异步”的思想,更是指出了异步的重要性,目的也是让猿儿们编写高效的程序,不因为请求或 DB 操作而阻塞了服务。
-
异步原理
先来说下同步,同步就是事件1干完,再干事件2,事件1干完之前,事件2只能傻傻的等待。如排队上厕所,前面那个人完事出来你才能进去舒服一下,否则...自己脑补吧,画面太美不敢看。
代码块 同步// 代码块 同步 // doSomething1 const s = Date.now(); for (let i = 0; i < 100000000; i++) { // ... } console.log(Date.now() - s); // 约330ms // doSomething2
上面的为同步,在 doSomething2 开始之前,必须等待330ms才能开始 doSomething2,因为浏览器在执行 for 的时候,干不了别的。
异步,是为了解决“傻傻的等待”的问题,还是上厕所问题,但是这次加了一步,先拿号,然后等待叫号,等轮到你的号通知你之后,直接去厕所就行了,而在这等待期间,你完全可以来两局王者农药,不耽误你干别的。
// 代码块 异步 // 1、异步1 setTimeout(() => { // doSomething }, 1000) // 2、异步2 $.ajax({ url: '/test/data.json', success: () => { console.log('success'); } });
像上面这种不立即执行,而是等待有了结果之后,再去执行的函数,称为 callback,即回调函数。
异步的原理:就是将 callback 作为参数传递给异步执行的函数,等有结果之后再去调用 callback 执行。
-
常见异步
开发中常见的异步操作有:
-
网络请求,如 ajax,request
-
IO 操作,如 fs.readFile,DB的CRUD
-
定时函数,如 setTimeout, setInterval
-
事件监听,如 $btn.on('click', callback)
-
-
结束语
异步是一开始就有的,但是怎么把异步从回调地狱中解放出来,则是一步一步发展的。jQuery 大家都很熟悉,基本是个前端同学都必用过的,N年前 jQuery 的源码、写法都是标杆,下面先说下它当中的异步解决方案。
-
-
jQuery异步方案
-
v1.5版本之前的异步
在1.5版本之前,ajax 主要是通过回调函数的写法来实现的:
const ajax = $.ajax({ url: '/test/data.json', success: () => { console.log('success'); }, err: () => { console.log('err') } }); console.log(ajax); // 返回的是一个XHR对象
这是标准的 callback 的写法,每个人都很熟悉,单着一层还好,如果有三层甚至更多层,那么代码会成“>”状,阅读很糟糕,也不便维护。
-
v1.5+的异步
2011年1月31日,jQuery v1.5发布,重写了ajax的API,ajax写法如下:
const ajax = $.ajax('/test/data.json') .done(() => { console.log('done'); }) .fail(() => { console.log('fail'); }) .always(() => { console.log('finished'); }); console.log(ajax); // 返回的是一个deferred对象
可以看到,最大的区别,是采用了链式的写法,最终返回的是一个 deferred object(延迟对象),实现了Promise接口,至于什么是deferred对象,请移驾此处:deferred。
链式写法的好处,不用把所有请求都丢到callback里,明确了成功就放入 done,失败放入 fail,如果成功后有很多步骤,可以写很多 done,然后链式起来就行了。熟悉 Promise 的同学是不是觉得有一点熟悉,如果看不出来,那么上面的写法还可以像这么写:
const ajax = $.ajax('/test/data.json') .then(() => { console.log('success'); }, () => { console.log('err'); }) .then(() => { console.log('success'); }, () => { console.log('err'); });
就是用 then 来代替 done、fail 等状态,then有两个参数,都是回调函数,第一个是 doneCallback,第二个是 failCallback。此时是不是与 Promise 更像了。其实链式写法,也并没有改变层层回调问题,只是发展路上的一个过渡。
-
结束语
在这章节说下 jQuery 的变化,也是为了说明JS异步发展的一个过程,由 callbac k到链式的写法,jQuery 从一开始的 callback 到之后的 then 链式调用,其实也为之后的 Promise 奠定了基础,下面先讲下async 的处理方式,然后就轮到 Promise。
-
-
Async.js
讲 Promise 之前,先说下 Async.js。Promise,它是需要学习成本的,有人不想用 Promise,但是多层回调嵌套又确实很恶心人,所有就有了async、then 等库的诞生,这些库其实并没有用到 Promise,但是却能以优美的方式去书写异步,算是 callback 的语法糖,但是也可以一定程度逃离“回调地狱”了。
下面简单介绍下 Async,它可以用在 browser 跟 node 端,它的方法很多,具体可移驾 Async.js 挑出几个常用的 api,看看它的写法。
-
async.series(tasks, [callback]): 顺序执行数组或集合内的函数,执行完一个就执行下一个,错误可在 callback 中获得:
const async = require('async'); async.series([ (callback) => { callback(null, 'ok1'); // 为了方便,直接返回字符串 }, (callback) => { callback(null, 'ok2'); } ], (err, data) => { console.log(data); // ['ok1', 'ok2'] });
-
async.parallel(tasks, [callback]): 并行执行数组、集合内的函数:
async.parallel([ (callback) => { callback(null, 'ok1'); }, (callback) => { callback(null, 'ok2'); } ], (err, data) => { console.log(data); // ['ok1', 'ok2'] });
async.waterfall(tasks, [callback]): 瀑布流方式,任务依次执行,前一个函数的回调,会作为后一个函数的参数:
-
async.waterfall([ (callback) => { callback(null, 'ok1', 'ok2'); }, (arg1, arg2, callback) => { // 此处 arg1='ok1', arg2='ok2' callback(null, 'ok3'); } ], (err, data) => { console.log(data); // 'ok3' });
从上面几个例子可以看出,它可以很方便的把多个请求放到同一级去处理(数组或集合中),而且功能很多,如顺序执行、并行执行、竞速执行等,非常方便。如果用传统的回调函数形式,则要复杂的多,一层层嵌套,难以书写与维护。所以 Async 这个库很受欢迎,现在 github 上的 stars 将近 25k,也能看出火爆程度,大家是多么想逃离回调地狱。
但是时代在发展,随着 ES6、ES7、ES8 等的发布,Promise 还是有必要了解、学习一下的。
-
-
Promise
callback一层嵌一层的回调,导致了金字塔问题的出现,也即callhack hell,写个代码都不能愉快的写了。所有新兴事物的快速发展一定是戳中了原来的一些痛点。
在开发者的千呼万唤中,终于,2015年6月份,ES2015规范正式发布,也是JavaScript的20周年,ES6的发布,也标志着JS开始升级为企业级大型应用的开发语言。Promise也正式加入到ES6,成为一个原生对象,可以直接用。-
什么是Promise
Promise是一个拥有then方法的函数或对象,一个Promise对象可以理解为一次将要执行的操作,主要是异步操作,之后可以用一种链式调用的方式来组织代码。目前Promise的规范是Promise/A+规范,核心内容如下:
-
状态:一个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时调用。
-
-
基本用法
先来看一个fs的异步读取文件的方法:
const fs = require('fs'); const read = (fileName) => { fs.readFile(fileName, (err, data) => { if (err) { console.log(err); } else { console.log(data.toString()); } }); }
然后用Promise对fs.readFile进行封装:
const fs = require('fs'); // readPromise这个方法以后会多次使用 const readPromise = (fileName) => { // 把fs.readFile用Promise包装一层 const promise = new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err); // 失败就reject出去 } else { resolve(data.toString()); // 成功就resolve出去 } }); }) return promise; // 最后返回一个promise对象 }
注意看程序中注释的部分,Promise的callback中有两个非常重要的参数:resolve 和 reject。
resolve方法:使Promise对象状态变化 pending-->fulfilled,即等待状态变为已完成,表示成功,resolve方法的参数用于成功之后的操作,此处就是获得的文件的内容。
reject方法:使Promise对象状态变化 pending-->rejected,即等待状态变为已拒绝,表示失败,reject方法的参数用于失败之后的操作,此处就是失败的原因。
通过上小节的规范可以知道,Promise对象都有then方法,所以readPromise方法可以这么用:readPromise('./test.txt') .then((data) => { console.log(data); // 上面代码中的resolve回的值 }, (err) => { console.log(err); // 上面代码中的reject回的值 });
then有两个参数,第一个是成功之后的callback,第二个是失败之后的callback,而参数分别是上步包装的resolve与reject函数的参数。
上面还有种写法,就是then只接受一个参数,表示成功之后的操作,后续跟上catch方法,捕获reject的异常:readPromise('./test.txt') .then((data) => { console.log(data); // 上面代码中的resolve回的值 }) .catch((err) => { console.log(err); // 上面代码中的reject回的值 });
上面这种写法更清晰点。
-
参数传递
理解Promise的参数传递是很重要的,这样才能得到自己想要的数据。上面已经讲了,resolve的数据会在第一个then接收,reject的数据会在catch接收。因为then返回的还是Promise,所以then可以链式调用,如想对上面的test.txt的数据进行处理,则可以继续then下去:
readPromise('./test.txt') .then((data) => { console.log(data); // resolve回的值 return data; // 此处return的data,将在下个then的参数处获得 }) .then((data) => { console.log(data + " 数据已经处理了~"); // 此处的data就是上个then里return回的数据 }) .catch((err) => { console.log(err); // 上面代码中的reject回的值 });
then链式操作中返回的值,将会在下个步骤处获得,而如果返回的是一个Promise,那么下个then处获得的就是Promise的第一个then的值。这句话怎么理解,来看个例子,我想读取test1.txt之后,再读取test2.txt,传统callback处理以及Promise处理对比:
// 普通回调,层层嵌套 fs.readFile('./test1.txt',(err1, data1) => { if (err1) { console.log(err1); } else { console.log(data1); // 然后再读取第二个文件 fs.readFile('./test1.txt', (err2, data2) => { if (err2) { console.log(err2); } else { console.log(data2); } }); } }); // Promise方式 const read1 = readPromise('./test1.txt'); const read2 = readPromise('./test2.txt'); read1.then((data1) => { console.log(data1); // 此处是test1.txt的内容 return read2; // 此处返回的是read2,一个Promise对象 }) .then((data2) => { console.log(data2); // 此处是上一步返回的read2的then,所以打印的是test2.txt的内容 })
对比可以发现,Promise方式更优雅,也更容易看懂,这只是读取2个文件,如果读取三个甚至更多,那用Promise就更方便了,当然如果不需要读取的有依赖关系,则可用Promise对象的all或race方法。
如果想读取test1.txt, text2.txt的内容,读完再做其他操作,则可以如下:const read1 = readPromise('./test1.txt'); const read2 = readPromise('./test2.txt'); Promise.all([read1, read2]) .then((datas) => { console.log(datas[0]); // test1.txt的内容 console.log(datas[1]); // test2.txt的内容 });
如果想读取test1.txt, text2.txt的内容,但是只要有一个返回就可以做其他操作,谁执行的快就用谁,则可以如下:
const read1 = readPromise('./test1.txt'); const read2 = readPromise('./test2.txt'); Promise.race([read1, read2]) .then((data) => { console.log(data); // 先读取完那个文件的内容 });
有人说还看到过Promise.resolve,它的作用是把一个thenable对象转换为Promise对象,如下:
// thenable对象,有then属性,且属性值如下 const thenable = { then: (resolve, reject) => { resolve('success'); } } // 把thenable对象转换为Promise对象 const thenToPromise = Promise.resolve(thenable); // 然后就可以这么用了 thenToPromise.then((data) => { console.log(data); // 'success' });
-
相关库
实际开发中,使用原生的Promise当然可以,不过市面上有现成的第三方库,而且很好用,比较流行的是Q、Bluebird等。他们都可用于浏览器端以及node端,并且可以在不支持Promise的环境中使用,至于用那个,则看个人爱好了,bluebird号称Promise库里最快的,比原生的Promise都快,其实原生的Promise比传统的callback慢不少。
这里介绍Q.js一些基本的用法,引用官网的一个例子,再次体验下传统回调与Promise库之间的对比:// 传统回调 step1((value1) => { step2(value1, (value2) => { step3(value2, (value3) => { step4(value3, (value4) => { // Do something with value4 }); }); }); }); // 用Q Q.fcall(promisedStep1) .then(promisedStep2) .then(promisedStep3) .then(promisedStep4) .then((value4) => { // Do something with value4 }) .catch((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等。用法都放到一段代码里:
const Q = require('q'); const fs = require('fs'); // Q.fcall: 接收函数或defer实例,返回一个Promise对象 const promiseFcall = Q.fcall(() => { return 'hello'; }); // Q.nfcall: Node function call, 处理callback是这种形式的:function(err, result),可以直接封装成Promise const promiseNfcall = Q.nfcall(fs.readFile, './test.txt', 'utf-8'); // Q.nfapply: 与Q.nfcall类似,只是参数不一样,很像js的call与apply用法 const promiseNfapply = Q.nfapply(fs.readFile, ['./test.txt', 'utf-8']); // Q.defer: 可以定义Promise生成器,如果浏览器不支持Promise,则比较有用,很像原生Promise的写法 const promiseDefer = (fileName) => { const defer = Q.defer(); fs.readFile(fileName, (err, data) => { if (err) { defer.reject(err); } else { defer.resolve(data.toString()); } }) } // Q.all: 与Promise.all类似 const read1 = Q.nfcall(fs.readFile, './test1.txt', 'utf-8'); const read2 = Q.nfcall(fs.readFile, './test2.txt', 'utf-8'); Q.all([read1, read2], (data) => { console.log(data[0]); console.log(data[1]); }); // Q.any: 与Promise.race类似 Q.any([read1, read2]) .then((data) => { console.log(data); });
以上只是简单介绍了最基本的用法,具体可以自行去github上看下。
-
结束语
到此,Promise差不多介绍完了,当然Promise还有很多用法,就不一一列举了,那么Promise有没有改变callback的本质?并没有,Promise只是换了种对异步的写法,优化了对代码的可读性,其实还是依赖callback,获得的数据,还是在then的callback里获取到的。上面看到需要的数据,还是在callback中获得的,还没有真正像同步那样的写法,如果用Generator配合Promise,则写法就完全不同了,接下来进入Generator。
-
-
Generator
-
协程(非携程)
介绍Generator前,先讲下协程,协程最初诞生是为了解决低速IO与高速的CPU之间协作问题,协程是指多个线程交互协作,完成异步任务,大概流程如下:
-
协程A开始运行
-
执行到某处,暂停,然后执行权交给协程B
-
一段时间后,协程B交换执行权给协程A
-
协程A恢复执行
还以读取文件为例,代码表示如下:
function asyncFunction() { // doSomething1 yield readFile('./test.txt'); // doSomething2 }
上面函数asyncFunction就是一个协程,一开始执行doSomething1,当遇到yield后,自身先暂停,执行权移交给readFile,当readFile执行完之后,执行权又交还回asyncFunction,然后接着执行doSomething2。
-
-
什么是Generator
Generator(生成器)可以说是协程在ES6中的实现,它最大的特点是:可以交出执行权,暂停执行。先看一个简单的Generator写法:
function* gen() { yield 'hello'; yield 'world'; return 'ok'; } const 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迭代器。
-
-
Iterator迭代器
在讲Iterator之前,先说下ES6新引入的一个基本类型:Symbol。
ES6之前JS有6个基本数据类型:string, object, null, boolean, undefined, number。现在增加一个:Symbol,表示独一无二的值。
Symbol不能用new关键字,因为是一个原始类型的值,不是对象,所以也不能添加属性,可理解为类似字符串数据类型。它可以接收一个字符串参数,主要是为了控制台显示或转换为字符串时容易区分:let s1 = Symbol(); let s2 = Symbol(); s1 == s2 // false s1 = Symbol('foo'); s2 = Symbol('foo'); s1 // Symbol(foo); typeof s1 // 'symbol' s1 == s2 // false
Symbol也可以作为对象的属性key来使用:
const 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循环遍历:
const arr = ['foo1', 'foo2', 'foo3']; for (let i of arr) { console.log(i); // 'foo1' 'foo2' 'foo3' 这里注意i为value,并不是对应的key }
Iterator对象:具有[Symbol.iterator]属性的数据,都可以生成一个Iterator对象,而怎么使用Iterator对象,有两种方式: next(), for...of。以数组举例:
const arr = ['foo1', 'foo2', 'foo3']; const 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 (let i of it) { console.log(i); // 'foo1' 'foo2' 'foo3' }
而Generator,就是天生的Iterator对象,所以才有next(),也可以用for...of遍历,针对一开始的例子,现详细解释一下:
function* gen() { yield 'hello'; yield 'world'; return 'ok'; } const 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*
-
const 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}格式。
-
-
yield、next
上面其实已经用到yield、next了,这儿再详细说下。
-
yield* : yield 可以返回一个值或一个表达式,但还可以 yield* 这么用,在Generator里面再套一个Generator:
function* gen() { yield 'a'; yield 'b'; } function* gen2() { yield 'c'; yield* gen(); yield 'd'; } const g = gen2(); g.next(); // {value: "c", done: false} g.next(); // {value: "a", done: false} g.next(); // {value: "b", done: false}
-
next: next也可以向yield传递参数:
function* gen() { const a = yield 'a'; console.log(a); // 100 const b = yield 'b'; console.log(b); // 200 yield 'c'; } const 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与异步联系起来。
-
-
Thunk
其实Thunk函数并不是Generator的一部分,这节重在介绍Generator,所以放在此处。
-
Thunk函数:将多参数函数替换成单参数函数,只接受一个参数,并且参数是回调函数。任何函数,只要含有回调函数,都可以写成Thunk函数形式。
看一个例子,以fs.readFile为例:
// 1、多参数函数 fs.readFile(fileName, calback); // 2、定义一个fs的thunk转换器 const thunk = (fileName) => { return (callback) => { return fs.readFile(fileName, callback); } } // readFileThunk为Thunk函数,用的时候,只传入callback就行 const readFileThunk = thunk(fileName); readFileThunk(callback);
看着是不是又复杂了...是的,不过现在的复杂是为以后的简单准备的,具体后续会讲。手动写Thunk方法比较麻烦,所以出现了第三方库:thunkify。
-
thunkify: 封装了thunk转换器,可以简化写法:
const fs = require('fs'); const thunkify = require('thunkify'); const readFile = thunkify(fs.readFile); const readFileThunk = readFile(fileName); readFileThunk(callback);
-
Thunk配合Generator
先看个例子:
const fs = require('fs'); const thunkify = require('thunkify'); const readFile = thunkify(fs.readFile); const gen = function* () { const data1 = yield readFile('./test1.txt'); console.log(data1.toString()); // test1.txt的内容 const 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) { const gen = generator(); // 这个next其实就是Thunk函数的回调函数 function next(err, data) { const res = gen.next(data); // 类似{value: Thunk函数, done: false} if (res.done) { return; } res.value(next); // res.value是一个Thunk函数,而参数next就是一个callback } next(); } const gen = function* () { const data1 = yield readFile('./test1.txt'); const data2 = yield readFile('./test2.txt'); } // run执行Generator函数 run(gen);
其实,要用Generator解决回调地狱问题,需要首先处理一下调用的函数,使函数正确执行后能够自动执行next方法,并且传递执行完方法后的结果。
-
Promise配合Generator
yield 后能跟Thunk函数,也可以跟Promise对象,所以Promise也可以配合Generator解决回调地狱问题。下面是一个基于Promise的Generator执行器:
// 执行器 function run(generator) { const gen = generator(); function go(res) { // res类似{value: Promise对象, done: false} if (res.done) { return res.value; } // Promise对象有then方法,两个参数为doneCallback,failCallback return res.value.then((data) => { return go(gen.next(data)); }, (err) => { return go(err); }) } go(gen.next()); } const gen = function* () { const data1 = yield readPromise('./test1.txt'); const data2 = yield readPromise('./test2.txt'); } // run执行Generator函数 run(gen);
每次写生成器函数很麻烦,所以TJ大神写了一个co库,下面介绍。
-
-
co
co库可以自动执行Generator函数,其实就是类似上面的run方法,Generator是一个异步操作容器,自动执行需要交换执行权给Generator,有两种方法可以做到:
-
回调函数,将异步操作包装成Thunk函数,在回调函数里执行交换执行权
-
Promise对象,将异步操作包装成Promise对象,用then里执行交换执行权
co库就是将两种执行器包装成一个库,所以使用co的时候,yield后面只能是Thunk函数或Promise对象。co现在返回的是一个Promise对象(之前版本返回的是Thunk函数),co非常好用,把刚才的代码重新,将会非常简单:
const co = require('co'); const gen = function* () { const data1 = yield readPromise('./test1.txt'); const data2 = yield readPromise('./test2.txt'); } // co执行Generator函数 co(gen);
-
-
结束语
Generator终于讲完了,配合Thunk函数或Promise对象,确实比之前的callback或then链式调用“顺畅”了很多,很像同步的写法,已经很符合人的顺序执行的思维了。其实Generator的本质是“暂停”,有了这个,才能让程序到一个地方先暂停,执行异步,然后执行完了再继续执行程序,这样就可以把操作连起来了。
可以看到Generator的异步处理,学习成本比较高,Generator、Thunk、Promise...等,都需要时间去学习,这显然还不够友好,所以在ES7中基于Promise实现了一套异步处理方案:async/await,这个才是最终的方案。
-
-
async/await
async/await是在ES7中实现的,目前好多浏览器不支持,node从7.0.0开始就支持使用--harmony-async-await来支持此功能,另外babel也已经支持async的transform了,使用的时候引入babel就行。
-
基本用法
先介绍下async/await:
-
基于Promise实现的,不适用于普通的回调函数
-
非阻塞的
-
函数声明用async function, 遇到异步用await,且await只能放在async函数中
先看一段使用async/await的代码:
// 定义async函数,注意async关键字 const readAsync = async function() { const data1 = await readPromise('./test1.txt'); // 注意await关键字 const data2 = await readPromise('./test2.txt'); return 'ok'; // 返回值可以在调用处通过then拿到 } // 执行 readAsync(); // 或者 readAsync().then((data) => { console.log(data); // 'ok' });
是不是非常简单,无需用co,直接执行就行。需要注意一点,await后只能跟Promise对象、字符串,数值等,不能跟Thunk函数,也暗示Promise可能是解决异步的最终方案。同时,async函数也默认返回的是一个Promise对象,函数最后可以return一个值,最后调用时在then里获取。
-
-
与Generator的对比
上面也看到,async/await与Generator的解决方案很像,区别如下:
-
声明,async function 代替 function*
-
await 代替 yield
-
内置执行器,可以直接运行,不需要co这种第三方库
-
-
结束语
其实async与Generator很像,是因为async相当于把Generator跟执行器进行了包装,是Generator的语法糖,但是也方便了很多,目前也有很多人认为async就是异步的最终方案。
-
-
小结
讲完了,里面可能有些例子不恰当,也参考了官网或别人的一些例子,总之尽量用简单的例子去铺开。
由于浏览器的特殊性,JS只能采用异步解决请求,从而性能也比较好,但是也带了了各种麻烦,所以人们从一开始就寻找它的同步写法,试图摆脱恶心的callback-hell,从一开始callback,到Promise对象,到Generator,再到async,异步方案是越来越好,也越来越优雅,随着ES7的普及,其实直接用async就好,不过技术发展总有一些过程,了解这些过程对我们的眼界扩展以及对这门语音会有更好的认识。
希望讲了这么多能帮助一些同学理解异步,有错误也轻及时指正,最后再总览下法中中的几种写法,体验异步发展过程:callback方式:
fs.readFile('./test1.txt', (err1, data1) => { fs.readFile('./test2.txt', (err2, data2) => { fs.readFile('./test2.txt', (err2, data2) => { }); }); });
Promise方式:
readPromise('./test1.txt', (data1) => { return readPromise('./test2.txt'); }) .then((data2) => { return readPromise('./test3.txt'); }) .then((data3) => { });
Generator方式:
const gen = function* () { const data1 = yield readPromise('./test1.txt'); const data2 = yield readPromise('./test2.txt'); const data3 = yield readPromise('./test3.txt'); } co(gen);
async/await方式:
const readAsync = async function() { const data1 = await readPromise('./test1.txt'); const data2 = await readPromise('./test2.txt'); const data3 = await readPromise('./test3.txt'); } readAsync();