前言
javascript是单线程的一门语言,所以在执行任务的时候,所有任务必须排队,然后一个一个的执行,
在javascript中有分同步代码,和异步代码,顾名思义,同步代码,就是依此执行的代码,异步代码可能不会立即执行,得等到某一特定事件触发时才会执行,javascript有个任务队列,用来存放异步代码,任务队列中的任务又有优先级之分,微任务(microtask)的优先级大于宏任务(macrotask),在javascript中代码的执行顺序为,主线程会先执行完同步代码,并将异步代码放到任务队列中,当同步代码执行完毕,就轮询任务队列,先询问微任务,如果有则执行微任务,如果没有询问宏任务。
//异步代码
setTimeout(function () { //属于宏任务
console.log('hello world3');
},0);
new Promise(resolve => { //属于微任务
console.log('hello world4'); //Promise 对象会立即执行 所以new Promise里面的类似与同步代码
resolve('hello world5');
}).then(data => {console.log(data)});
//同步代码
function main(){
console.log('hello world');
}
console.log('hello world1');
console.log('hello world2');
main();
输出结果为:
hello world4
hello world1
hello world2
hello world
hello world5
hello world3
按照上面所说的顺序,同步代码先执行,那么会先输出hello world4 然后hello world1 ,hello world2,hello world 接下来执行任务队列的异步代码,先轮询微任务是否有要执行的代码,由于Promise对象属于微任务的,故先执行它,输出hello world5 ,然后执行宏任务的代码,及setTimeout的代码,输出hello world3
本例比较简单,讲述了一下javascript代码的执行流程,希望对理解异步有帮助,其中涉及的Promise对象会在本文详细介绍。
本文代码可能比较多,所有涉及的代码均在我的github上
https://github.com/sundial-dreams/javascript_async
接下来回归正题,Javascript中异步的5种实现方法,并以ajax等为例子,实现几种异步的编写方式
javascript中的异步实现方式有以下几种
-
callback (回调函数)
-
发布订阅模式
-
Promise对象
-
es6的生成器函数
-
async/await
1.callback (回调函数)
回调函数是Javascript异步编程中最常见的,由于JavaScript中的函数是一等公民,可以将其以参数形式传递,故就有了回调函数一说,熟悉nodejs的人知到,里面涉及非常多的回调,这些回调代表着,当某个任务处理完,然后需要做的事,比如读取文件,连接数据库,等文件准备好,或数据库连接成功执行编写的回调函数,又比如像一些动画处理,当动画走完,然后执行回调,举个例子
function load(url,callback){
//something
setTimeout(callback,3000);//假设某个异步任务处理需要3s 3s后执行回调
}
load('xxx',function() {
//do something
console.log('hello world')
})
再来看个ajax例子 (代码 )
//ajax_callback.js
function ajax(object, callback) {
function isFunction(func) { // 是否为函数
return typeof func === 'function';
}
function isObject(object) { //是否为对象
return typeof object === 'object';
}
function toQuerystring(data) { //对象转成查询字符串 例如{a:1,b:2} => a=1&b=2 或{a:[1,2],b:3} => a=1&a=2&b=3
if (!isObject(data) || !data) throw new Error('data not object');
var result = '';
for (var key in data) {
if (data.hasOwnProperty(key)) {
if (isObject(data[key]) && !Array.isArray(data[key])) throw new Error('not support error');//除去对象
if (Array.isArray(data[key])) {
data[key].forEach(function (v) {
result += key + '=' + v + '&'
});
} else {
result += key + '=' + data[key] + '&';
}
}
}
return result.substr(0, result.length - 1);//去掉末尾的&
}
var url = object.url || '';
var method = object.method.toUpperCase() || 'GET';
var data = object.data || Object.create(null);
var async = object.async || true;
var dataType = object.dataType || 'json';//相应的数据类型 可选json ,text, xml
var xhr = new XMLHttpRequest();
url = ajax.baseUrl + url;
data = toQuerystring(data);
method === 'GET' && (url += '?' + data) && (data = null); //get 请求 => url 后面加上 ?a=1&b=2这种
try {
xhr.open(method, url, async);
method === 'POST' && (xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'));//post请求需要设置请求头为 application/x-www-form-urlencoded 类型
console.log(data);
xhr.send(data);
xhr.onreadystatechange = function () {//监听事件
if (this.readyState === 4) {
if (this.status === 200)
if (isFunction(callback))
switch (dataType) {
case 'json': {
callback(JSON.parse(this.responseText));//完成时执行传进来的回调
break
}
case 'text': {
callback(this.responseText);//重点在这,ajax接到数据就执行传入的函数
break
}
case 'xml': {
callback(this.responseXML);
break
}
default: {
break;
}
}
}
}
} catch (e) {
console.log(e);
}
}
ajax.get = function (url, data, callback) { //get方法
this({url: url, method: 'GET', data: data}, callback);
};
ajax.post = function (url, data, callback) { //post方法
this({url: url, method: 'POST', data: data}, callback);
};
ajax.baseUrl = '';
以上是个完整的ajax实例,当ajax完成执行回调
以下是使用koa实现的一个简易的服务端,模拟处理ajax的响应,之后的例子都会用这个来模拟ajax响应
//koa_test_server.js
const Koa = require('koa');
const Router = require('koa-router');
const bodyparser = require('koa-bodyparser');
const app = new Koa();
const api = new Router();
api.get('/api/test1', async ctx => { //处理get请求
ctx.res.setHeader('Access-Control-Allow-Origin', '*');//允许跨域访问
let querystring = ctx.querystring;
console.log(querystring);
ctx.body = JSON.stringify({
errno: false,
data: 'it is ok',
message: `you send me ${querystring} type is GET`
});
}).post('/api/test2', async ctx => {//处理post请求
ctx.res.setHeader('Access-Control-Allow-Origin', '*');//允许跨域访问
let data = ctx.request.body;
console.log(data);
ctx.body = JSON.stringify({
errno: false,
data: 'it is ok',
message: `you send me ${JSON.stringify(data)} type is POST`
})
});
app.use(bodyparser());
app.use(api.routes()).use(api.allowedMethods());
app.listen(3000, () => {
console.log('listen in port 3000')
});
简单使用如下
//test.html
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19},function (data) {
//do something such as render page
console.log(data);
});
ajax.post('/api/test2',{name: 'youname', age: 19}, function (data) {
//do something such as render page
console.log(data);
});
结果如下:
回调的好处就是容易编写,缺点就是过多的回调会产生回调地狱,代码横向扩展,代码可读性变差
不过回调还有很多应用,而且回调也是最常用的实现Javascript异步的方式。
2.发布订阅模式
发布订阅模式是设计模式的一种,并不是javascript特有的内容,所以javascript可以用发布订阅模式来做异步,那么其他语言如C++ java python php 等自然也能,其他语言实现的均在我的github里。
简单介绍一下发布订阅模式,发布订阅是两个东西,即发布和订阅,想象一下,有家外卖,你可以点外卖,这就是订阅,当你的外卖做好了,就会有人给你打电话叫你去取外卖,这就是发布,简单来说,发布订阅模式,有一个事件池,用来给你订阅(注册)事件,当你订阅的事件发生时就会通知你,然后你就可以去处理此事件,模型如下
接下来简单实现这个发布订阅模式
//async_Event.js
//单对象写法 Event 就相当于事件中心
const Event = function () { //使用闭包的好处 : 把EventPool私有化,外界无法访问EventPool
const EventPool = new Map();//使用es6 map来存 event,callback 键值对
const isFunction = func => typeof func === 'function';
const on = (event, callback) => { //注册事件
EventPool.get(event) || EventPool.set(event, []);
if (isFunction(callback)) {
EventPool.get(event).push(callback);
}
else {
throw new Error('callback not is function')
}
};
const addEventListenr = (event, callback) => { //on方法别名
on(event, callback)
};
const emit = (event, ...args) => { //触发(发布)事件
//让事件的触发为一个异步的过程,即排在同步代码后执行
//也可以setTimeout(fn,0)
Promise.resolve().then(() => {
let funcs = EventPool.get(event);
if (funcs) {
funcs.forEach(f => f(...args))
} else {
throw new Error(`${event} not register`)
}
})
};
const send = (event, ...args) => {//emit方法别名
emit(event,...args)
};
const removeListener = event => {//删除事件
Promise.resolve(() => {//删除事件也为异步的过程
if(event){
EventPool.delete(event)
}else{
throw new Error(`${event} not register`)
}
})
};
return {
on, emit, addEventListenr, send
}
}();
简单使用
Event.on('event', data => {//注册事件,名为event
console.log(data)
});
setTimeout(() => {
Event.emit('event','hello wrold')
},1000);
//1s后触发事件,输出hello world
使用发布订阅模式,修改之前的ajax例子
//仅看这部分代码
xhr.onreadystatechange = function () {//监听事件
if (this.readyState === 4) {
if (this.status === 200)
switch (dataType) {
case 'json': {
Event.emit('data '+method,JSON.parse(this.responseText));//触发事件
break
}
case 'text': {
Event.emit('data '+method,this.responseText);
break
}
case 'xml': {
Event.emit('data '+method,this.responseXML);
break
}
default: {
break;
}
}
}
}
使用如下
//test.html
//注册事件
Event.on('data GET',data => {
//do something such as render page
console.log(data)
});
Event.on('data POST',data => {
//do something such as render page
console.log(data)
});
//使用ajax
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19});
ajax.post('/api/test2',{name: 'youname', age: 19});
发布订阅模式是很重要的一种设计模式,在JavaScript中应用非常广泛,比如一些前端框架比如React,Vue等,都有使用这一设计模式,nodejs使用的就更多了。
使用发布订阅模式的好处是事件集中管理,修改方便,缺点就是,代码可读性下降,事件容易冲突。
3.Promise对象
Promise对象是异步编程的一种解决方案,比传统的回调函数和事件更合理更强大。
Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果,相比回调函数,Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
Promisel对象的两个特点:
1.对象状态不受外界影响。
Promise对象有三种状态:pending(进行中),fulfilled(已成功),rejected(已失败),当异步操作有结果时可以指定pending状态到fulfilled状态或pending状态到rejected状态的转换,状态一旦变为fulfilled,或rejected则这个Promise对象状态不会在改变。
2.一旦状态改变,就不再变化,任何时候都可以得到这个结果。
上面的看不懂也没关系,先把基本格式记住,然后多写写,也就理解了
//基本格式
let promise = new Promise((resolve, reject) => {//Promise对象接受一个函数
try {
setTimeout(() => {//模拟某异步操作 , 若操作成功返回数据
resolve('hello world'); //resolve() 使pending状态变为 fulfilled,需要注意resolve()函数最多只能接收1个参数,若要传多个参数,需要写成数组,或对象,比如resolve([1,2,2,2])或resolve({data,error})
reject(); //状态已变为fulfilled 故下面这个reject()不执行
}, 1000);
}catch (e) {
reject(e) //操作失败 返回Error对象 reject() 使pending状态变为rejected
}
});
promise.then((data) => {
console.log(data) //resolve()函数里面传的值
},(err) => {
console.log(err) //reject()函数里传的值
});
//1s后输出hello world
Promise对象的几个方法
1. then(fulfilled,rejected)方法:
异步任务完成时执行的方法,其中fulfilled(data)和rejected(err)分别是单参的回调函数,fulfilled对应的是成功时执行的回调,rejected对应的是失败时执行的回调,fulfilled函数的所接参数为resolve()函数传的值,rejected函数的参数则为reject()函数所传的值。
2. catch(rejected)方法:
then(null,rejected)的别名 捕获Promise对象中的错误
3. Promise.resolve(data):
等价于new Promise(resolve => {resolve(data)})
4.Promise.all([promise1,promise2,...,promisen]):
用于多个Promise对象的执行,执行时间取最慢的那个,例如:
let promise1 = new Promise(resolve => {
setTimeout(() => {
resolve(1);
}, 1000);
});
let promise2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 2000)
});
let promise3 = new Promise(resolve => {
setTimeout(() => {
resolve(3)
}, 3000)
});
let start = Date.now();
Promise.all([promise1, promise2, promise3]).then(([data1, data2, data3]) => {//使用数组解构获得每个Promise对象的data
console.log(`datas = ${data1},${data2},${data3} total times = ${Date.now() - start}ms`);
});
//输出结果为 datas = 1,2,3 total times = 3000ms
5.Promise.race([promise1,promise2,...,promisen]):
和Promise.all类似,不过它取Promise对象中最快的那个。
6.Promise.reject(err):
等价于new Promise((resolve,reject) => reject(err))
对有了Promise对象有了基本的理解,然后可以用它来替代回调函数的模式,比如一个图片加载例子
//回调形式
function asyncLoadImage_callback(url,callback) {//异步加载图片
var proxyImage = new Image();//图片代理
proxyImage.src = url;
proxyImage.onload = callback;//加载完时执行回调
}
asyncLoadImage_callback('xxx', function () {
image.src = 'xxx'//让真正的图片对象显示
});
//Promise对象形式
function asyncLoadImage_Promise(url) {
return new Promise((resolve,reject) => {
var proxyImage = new Image();
proxyImage.src = url;
proxyImage.onload = resolve;
proxyImage.onerror = reject;
})
}
asyncLoadImage_Promise('xxx')
.then(() => {
image.src = 'xxx'//让真正的图片对象显示
}).catch(err => console.log(err));
使用Promise对象的好处比较明显,除了写起来有一些麻烦而已,不过设计Promise可不仅仅让你这么用用,替代回调函数就完事了的,在后面的生成器实现异步,及async/await中,Promise将发挥巨大作用。
接下来将介绍将回调函数形式与Promise对象形式的相互转换,以下纯属个人兴趣,其实nodejs里的util包里面又promisify和callbackify两个函数,专门实现这个的。
1.回调函数形式转换为Promise对象形式
//promisify.js
//callback => Promise
/**
*
* @param fn_callback
* @returns {function(...[*]): Promise<any>}
*/
function promisify(fn_callback) { //接收一个有回调函数的函数,回调函数一般在最后一个参数
if(typeof fn_callback !== 'function') throw new Error('The argument must be of type Function.');
return function (...args) {//返回一个函数
return new Promise((resolve, reject) => {//返回Promise对象
try {
if(args.length > fn_callback.length) reject(new Error('arguments too much.'));
fn_callback.call(this,...args,function (...args) {
args[0] && args[0] instanceof Error && reject(args[0]);//nodejs的回调,第一个参数为err, Error对象
args = args.filter(v => v !== undefined && v !== null);//除去undefined,null参数
resolve(args)
}.bind(this));//保证this还是原来的this
} catch (e) {
reject(e)
}
})
}
}
简单使用
//nodejs的fs.readFile为例
let asyncReadFile = promisify(require('fs').readFile);
asyncReadFile('async.js').then(([data]) => {
console.log(data.toString());
}, err => console.log(err));
//将上面的asyncLoadImage_callback转换为例
let asyncLoadImage = promisify(asyncLoadImage_callback);
asyncLoadImage.then(() => {
image.src = 'xxx'//让真正的图片对象显示
});
2. Promise对象形式转换为回调函数形式
//callbackify.js
//Promise => callback
/**
*
* @param fn_promise
* @returns {Function}
*/
function callbackify(fn_promise) {
if(typeof fn_promise !== 'function') throw new Error('The argument must be of type Function.');
return function (...args) {
let callback = args.pop();//返回一个函数 最后一个参数是回调
if(typeof callback !== 'function') throw new Error('The last argument must be of type Function.');
if(fn_promise() instanceof Promise){
fn_promise(args).then(data => {
callback(null,data)//回调执行
}).catch(err => {
callback(err,null)//回调执行
})
}else{
throw new Error('function must be return a Promise object');
}
}
}
简单使用
let func = callbackify(timer => new Promise((resolve, reject) => {
setTimeout(() => {resolve('hello world')},timer);
}));
func(1000,function (err,data) {
console.log(data)//1s后打印hello world
});
接下来对之前的ajax例子进行改写,将回调形式变为Promise形式,可以直接改写,或使用promisify函数
先看改写的
//ajax_promise.js
function ajax(object) {
return new Promise(function (resolve,reject) {//返回个Promise对象
//省略部分代码
try {
//省略部分代码
xhr.onreadystatechange = function () {//监听事件
if (this.readyState === 4) {
if (this.status === 200) {
switch (dataType) {
case 'json': {
resolve(JSON.parse(this.responseText));//ajax有结果,resolve一下结果
break
}
case 'text': {
resolve(this.responseText);
break
}
case 'xml': {
resolve(this.responseXML);
break
}
default: {
break;
}
}
}else{
reject(new Error('error'))
}
}
}
} catch (e) {
reject(e)
}
});
}
ajax.get = function (url, data) { //get方法
return this({url: url, method: 'GET', data: data});
};
ajax.post = function (url, data) { //post方法
return this({url: url, method: 'POST', data: data});
};
ajax.baseUrl = '';
简单使用
//test.html
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19}).then(data => {
console.log(data)
});
ajax.post('/api/test2',{name: 'youname', age: 19}).then(data => {
console.log(data)
});
不修改原代码是最好的,故看第二种方式
//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
ajax.get('/api/test1', {name: 'dpf', age: 19}).then(([data]) => {
console.log(data)
});
ajax.post('/api/test2', {name: 'youname', age: 19}).then(([data]) => {
console.log(data)
});
Promise对象目前是比较流行的异步解决方案,相比回调函数而言,代码不再横向扩展,而且没有回调地狱这一说,好处还是挺多的,不过也有不足,就是写起来费劲(相比回调而言),不过Promise对象仍然是javascript的一个重要的知识点,而且在后面的生成器函数实现异步,async/await实现异步中会有广泛应用,希望通过刚刚的讲解,读者能对Promise对象有个基本的认识。
4.Generator(生成器)函数
Generator(生成器)函数,在python中就是创建迭代对象的,在Javascript中也是如此,不过无论在python还是JavaScript中Generator函数都还有另一个功能,即实现异步。
Generator函数是ES6提供的一种异步编程解决方案,其行为类似于状态机。
首先看一个简单的Generator例子
function *gen(){//声明一个生成器
let t1 = yield "hello"; //yield 表示 产出的意思 用yield来生成东西
console.log(t1);
let t2 = yield "world";
console.log(t2);
}
let g = gen();
/*next()返回一个{value,done}对象,value为yield表达式后面的值,done取值为true/false,表示是否 *生成结束*/
let x = g.next();//{value:"hello",done:false} 启动生成器
/**
* 通过给next()函数里传值 这里的值会传递到第一个yield表达式里 即相当于gen函数里 let t1 = "aaaa" */
let y = g.next("aaaa");//{value:"world",done:false}
g.next("bbbb");//{value:undefined,done:true}
console.log(x.value,y.value);
/*
输出
aaaa
bbbb
hello world
*/
上面的例子中,如果把gen函数当成一个状态机,则通过调用next()方法来跳到下一个状态,即下一个yield表达式,给next()函数传值来把值传入上一个状态中,即上一个yield表达式的结果。
在介绍Generator函数的异步时,先简单介绍一下Generator函数的几个方法
1.next()方法:
生成器函数里面的yield表达式并没有值,或者说总返回undefined,next()函数可以接受一个参数,该参数就会被当作yield表达式的值。
2.throw()方法:
在函数体外抛出一个错误,然后在函数体内捕获。例如
function *gen1(){
try{
yield;
}catch(e){
console.log('内部捕获')
}
}
let g1 = gen1();
g1.next();
g1.throw(new Error());
/*
输出
内部捕获
*/
3.return()方法:
返回给定值,并终结生成器。例如
function *gen2(){
yield 1;
yield 2;
yield 3;
}
let g2 = gen1();
g2.next();//{value:1,done:false}
g2.return();//{value:undefined,done:true}
g2.next();//{value:undefined.done:true}
4.yield*表达式(类似于python的yield from):
在生成器函数中调用另一个生成器函数。例如
function *gen3(){
yield 1;
yield 2;
yield 3;
}
function *gen4(){
yield 4;
yield * gen3();
yield 5;
}
//等价于
function *gen4(){
yield 4;
yield 1;
yield 2;
yield 3;
yield 5;
}
在使用Generator(生成器)函数做异步时,先引入协程(来自python的概念)这个概念,可以理解为 "协作的函数",一个协程本质就是子函数,不过这个子函数可以执行到一半,可以暂停执行,将执行权交给其他子函数,等稍后回收执行权的时候,还可以继续执行,跟线程非常像,在c++/python/java中一个线程的单位也是一个子函数(java的run方法),线程之间的切换,就相当于函数的切换,不过这个切换代价非常大,得保存很多跟线程相关东西,而协程则没那么复杂,所以协程又被称为纤程,或轻量级线程。
协程的执行流程大致如下:
1.协程A开始执行。
2.协程A执行到一半,进入暂停,执行权转移给协程B。
3.(一段时间后)协程B交还执行权。
4.协程A恢复执行
其中协程A就是异步任务,因为其分多段执行。
接下来将介绍使用Generator函数来实现协程,并做到异步。
首先来看一个简单的例子
const fs = require('fs');
function* gen(){//生成器函数
let data = yield asyncReadFile(__dirname+'/ajax_promise.js');
console.log(data); //文件读取成功 则输出
let data2 = yield timer(1000);
console.log(data2); //过1s后输出 hello world
}
let it = gen();
it.next();
function timer(time){//异步任务
setTimeout(() => it.next('hello world'),time)
}
function asyncReadFile(url) {//异步任务 读取文件
fs.readFile(url,(err,data) => {
it.next(data.toString())
})
}
可以看出通过暂缓it.next()方法的执行,来实现异步的功能,如果仅看gen的函数里面内部,比如
let data = yield asyncReadFile(__dirname+'/ajax_promise.js'); 这一段,可以理解为data等待异步读取文件asyncReadFile的结果,如果有了结果,则输出,gen继续向下执行,不过每一个异步函数,比如asyncReadFile的实现却变麻烦了,这个时候就要借助Promise对象,例子如下
const promisify = require('./promisify');
function timer(time,callback){
setTimeout(() => callback(), time)
}
const asyncReadFile = promisify(require('fs').readFile);//借用之前的promisify方法,将callback形式转换为Promise
const asyncTimer = promisify(timer);
function *gen(){
let [data] = yield asyncReadFile('./a.mjs');//生成一个Promise对象
console.log(data);
yield asyncTimer(1000);
console.log('hello world');
}
let g = gen();
let {value} = g.next(); //{value:asyncReadFile('./a.mjs'),done:false}
value.then(data => {//相当于asyncReadFile('./a.mjs').then(data => {})
let {value} = g.next(data);//{value:asyncTimer(1000),done:false}
value.then(data => {//相当于asyncTimer(1000).then(data => {})
g.next(data);//{value:undefined,done:true}
})
});
可以看出上面的借助Promise对象例子,在异步处理上可以有更通用的实现,即生成器执行器,
//run.js
function run(gen){//传入一个生成器函数
let g = gen();
function next(data){
let result = g.next(data);
let {value,done} = result;
if(done) return value;//done为true时结束递归
if (Array.isArray(value)) value = Promise.all(value);//如果yield表达式后面跟的是一个数组,可以将其转换为Promise.all
if(!value instanceof Promise) value = Promise.resolve(value)//不是Promise对象,则转成Promise对象
value.then((data) => {
next(data);//递归调用
});
}
next();//启动生成器
}
借助run执行器函数,运行上面的gen只需要run(gen)即可
最后让我们来继续改写之前的ajax例子,这次使用Generator函数,代码如下
//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
run(function*(){
let [[data1],[data2]] = yield [ajax.get('/api/test1', {name: 'dpf', age: 19}),ajax.post('/api/test2', {name: 'youname', age: 19})];//相当于Promise.all
console.log(data1,data2)
});
使用Generator函数无疑是解决异步的优于callback(回调),及Promise对象的好方法,没有callback回调地狱,Promise对象的过长then链,异步代码看起来跟同步代码一样,可读性,和维护性都较好。
5.async/await(javascript异步的终极解决方案)
es6中使用Generator函数来做异步,在ES2017中,提供了async/await两个关键字来实现异步,让异步变得更加方便。
async/await本质上还是基于Generator函数,可以说是Generator函数的语法糖,async就相当于之前写的run函数(执行Generator函数的函数),而await就相当于yield,只不过await表达式后面只能跟着Promise对象,如果不是Promise对象的话,会通过Promise.resolve方法使之变成Promise对象。async修饰function,其返回一个Promise对象。await必须放在async修饰的函数里面,就相当于yield只能放在Generator生成器函数里一样。一个简单的例子
//封装一个定时器,返回一个Promise对象
const timer = time => new Promise((resolve,reject) => {
setTimeout(() => resolve('hello world'),time)
});
async function main() {//async函数
let start = Date.now();
let data = await timer(1000);//可以把await理解为 async wait 即异步等待(虽然是yield的变体),当Promise对象有值的时候将值返回,即Promise对象里resolve(data)里面的data,作为await表达式的结果
console.log(data,'time = ',Date.now() - start,'ms')//将会输出 hello world time = 1002 ms
}
main();
可以看到async/await使用起来非常方便,其实async/await的原理也非常简单,就是把Generator函数和执行器包装在一起,其实现如下
//spawn.js
//之前的run函数的变体,只不过多了错误处理,然后返回的是Promise对象
function spawn(genF){
return new Promise((resolve,reject) => {
let g = genf();
function next(nextF){
let next;
try{
next = nextF();
}catch(e){
reject(e)
}
if(next.done) return resolve(next.value);
Promise.resolve(next.value)
.then(data => next(() => g.next(data)))
.catch(err => next(() => g.throw(err)));
}
next(() => g.next(undefined))
})
}
所以之前的async function main() {} 就等价于 function main() { return spawn(function *() {}) },了解async的内部原理可以有助于理解和使用async。
接下来看使用async/await来改进之前的ajax的例子
//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
(async function() {
let [data1,data2] = await Promise.all([ajax.get('/api/test1', {name: 'dpf', age: 19}),ajax.post('/api/test2', {name: 'youname', age: 19})]);
console.log(data1,data2)
})()
到此,这篇文章已经接近尾声,总结一下JavaScript实现异步的这五种方式的优缺点
1.callback(回调函数):写起来方便,不过过多的回调会产生回调地狱,代码横向扩展,过多的回调不易于维护和理解
2.发布订阅模式:通过实现个事件管理器,方便管理和修改事件,不同的事件对应不同的回调,通触发事件来实现异步,不过会产生一些命名冲突的问题(在原来的Event.js基础上加个命名空间,防止命名冲突即可),事件到处触发,可能代码可读性不好。
3.Promise对象:本质是用来解决回调产生的代码横向扩展,及可读性不强的问题,通过.then方法来替代掉回调,而且then方法接的参数也有限制,所以解决了,回调产生的参数不容易确定的问题,缺点的话,个人觉得,写起来可能不那么容易,不过写好了,用起来就就方便多了。
4.Generator(生成器)函数:记得第一次接触Generator函数是在python中,而且协程的概念,以及使用生成器函数来实现异步,也是在python中学到的,感觉javascript有点是借鉴到python语言中的,不过确实很好的解决了JavaScript中异步的问题,不过得依赖执行器函数。
5.async/await:这种方式可能是javascript中,解决异步的最好的方式了,让异步代码写起来跟同步代码一样,可读性和维护性都上来了。
最后文章中的所有代码,均在我的github上
[https://github.com/sundial-dreams/javascript_async]()
,希望这篇文章能让你对JavaScript异步有一定的认识。