当前位置: 首页 > news >正文

Javascript异步编程深入浅出

前言

熟悉前端的同学对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中有两个非常重要的参数:resolve 和 reject。

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之间协作问题,协程是指多个线程交互协作,完成异步任务,大概流程如下:

协程A开始运行
执行到某处,暂停,然后执行权交给协程B
一段时间后,协程B交换执行权给协程A
协程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();

相关文章:

  • 脉冲波形的产生和整形
  • 2022最后一个月,我们该如何学Java​?
  • Python用一行代码,截取图片
  • C# 11 中的新增功能
  • 【蓝桥杯国赛真题06】python绘制菱形圆环 蓝桥杯青少年组python编程 蓝桥杯国赛真题解析
  • 我为什么选择博客园!
  • BUG系列路径规划算法原理介绍(六)——BugFlood算法
  • 毕设选题推荐基于python的django框架的自媒体社推广平台系统
  • LaTex入门(二):LaTex控制序列的作用
  • [Linux](16)网络编程:网络概述,网络基本原理,套接字,UDP,TCP,并发服务器编程,守护(精灵)进程
  • UI自动化总结
  • Zabbix6.0使用教程 (二)—zabbix6.0常用术语
  • java计算机毕业设计网络游戏管理网站源程序+mysql+系统+lw文档+远程调试
  • 【机器学习】Rasa NLU以及Rasa Core概念和语法简介(超详细必看)
  • Service (一) 启动/绑定服务
  • IDEA常用插件整理
  • JS 面试题总结
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • TypeScript迭代器
  • 动态规划入门(以爬楼梯为例)
  • 服务器从安装到部署全过程(二)
  • 高性能JavaScript阅读简记(三)
  • 巧用 TypeScript (一)
  • 如何用Ubuntu和Xen来设置Kubernetes?
  • 如何在 Tornado 中实现 Middleware
  • 什么是Javascript函数节流?
  • 推荐一款sublime text 3 支持JSX和es201x 代码格式化的插件
  • 无服务器化是企业 IT 架构的未来吗?
  • 一个SAP顾问在美国的这些年
  • Java总结 - String - 这篇请使劲喷我
  • 大数据全解:定义、价值及挑战
  • 关于Android全面屏虚拟导航栏的适配总结
  • 小白应该如何快速入门阿里云服务器,新手使用ECS的方法 ...
  • 新年再起“裁员潮”,“钢铁侠”马斯克要一举裁掉SpaceX 600余名员工 ...
  • ​Python 3 新特性:类型注解
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • (4.10~4.16)
  • (42)STM32——LCD显示屏实验笔记
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (Java实习生)每日10道面试题打卡——JavaWeb篇
  • (LeetCode 49)Anagrams
  • (MATLAB)第五章-矩阵运算
  • (windows2012共享文件夹和防火墙设置
  • (剑指Offer)面试题34:丑数
  • (论文阅读22/100)Learning a Deep Compact Image Representation for Visual Tracking
  • (十)T检验-第一部分
  • (十六)串口UART
  • (一)搭建springboot+vue前后端分离项目--前端vue搭建
  • (转)shell调试方法
  • .net core 6 集成和使用 mongodb
  • .net framework4与其client profile版本的区别
  • .net oracle 连接超时_Mysql连接数据库异常汇总【必收藏】
  • .Net 知识杂记
  • .netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项
  • .NET多线程执行函数