头部背景图片
吉水于人的笔记 |
吉水于人的笔记 |

js异步编程总结

前言

Javascript是单线程的,而实现异步编程就显得及其重要。在ES6以前,我们要实现异步编程可通过:(1)回调事件;(2)事件监听(订阅/发布);(3)Promise对象。ES6中提供了Generator函数(协程coroutine)。ES7中使用了async和await实现。

Javascript异步编程方法

  • 回调函数
    回调函数实现异步应该是用的比较频繁的,它是在一个耗时操作之后执行某个操作。
//callback hell

doSomethingAsync1(function(){
    doSomethingAsync2(function(){
        doSomethingAsync3(function(){
            doSomethingAsync4(function(){
                doSomethingAsync5(function(){
                    // code...
                });
            });
        });
    });
});

上面的代码,在业务逻辑逐渐复杂的情况下,层层叠加,这就是典型的”回调地狱”,这种代码维护起来特别痛苦,同时,我们回调函数之外是无法捕获到回调内的异常。

  • 事件监听

事件监听也是比较常用的方法,它使用逻辑分离方式,对代码解耦很有用处。我们将不变的部分封装在组件内部,提供外部调用,将变化的部分暴露在外部使用。相当于事件的设计是组件的接口设计。

//发布和订阅事件

var events = require('events');
var emitter = new events.EventEmitter();

emitter.on('event1', function(message){
    console.log(message);
});

emitter.emit('event1', "message for you");

事件监听的实现方式是,一个订阅方订阅某个事件,在需要触发该事件时让发布方直接使用emit发出一个事件并携带相应的参数,订阅方收到该事件信息并处理。

  • Promise

Promise对象通过同步的写法实现异步的操作,避免层层嵌套的回调,使得代码逻辑清晰,易于维护。

(1)Promise的链式写法
Promise.prototype.then()方法返回一个新的Promise对象,采用这种链式的写法就是,一个then后面可以跟着另一个then。如果前面的回调返回的是一个Promise对象,那么后面的回调会等待前一个Promise对象有了返回结果之后再进一步调用。

//原生Primose顺序嵌套回调示例
var fs = require('fs')

var read = function (filename){
    var promise = new Promise(function(resolve, reject){
        fs.readFile(filename, 'utf8', function(err, data){
            if (err){
                reject(err);
            }
            resolve(data);
        })
    });
    return promise;
}

read('./text1.txt')
.then(function(data){
    console.log(data);
return read('./text2.txt');
})
.then(function(data){
    console.log(data);
})
.catch(function(err){
    console.log("error caught: " + err);
})

在上述代码中,第一个then先输出text1.txt的内容后返回read(‘./text2.txt’),这里返回了一个新的Promise实例,第二个then里面指定了异步读取text2.txt文件的回调函数。与Promise.prototype.then()对应的还有一个Promise.prototype.catch方法,用于指定发生错误时的回调函数。

(2)Promise异步并发
异步并发是,我们如果不需要某些事件按顺序执行,而是同时执行,就可以使用异步并发执行。Promise.all()用于将多个Promise包装成一个新的Promise实例(var p = Promise.all([p1,p2,p3]))。Promise.all可接受一个数组作为参数,数组中的每个值是Promise对象实例。执行结果的状态由p1,p2,p3决定,可分为两种情况:

a. 只有p1,p2,p3的状态都变为fullfilled,P的状态才会变成fullfilled,此时p1,p2,p3的返回值组成一个数组,返回给p的回调函数;
b. 只要p1,p2,p3中的任何一个被rejected,p的状态就会变成rejected,此时第一个被rejected的实例的返回值,会返回给p的回调函数。

var fs = require('fs')

var read = function (filename){
    var promise = new Promise(function(resolve, reject){
        fs.readFile(filename, 'utf8', function(err, data){
            if (err){
                reject(err);
            }
            resolve(data);
        })
    });
    return promise;
}

var promises = [1, 2].map(function(fileno){
    return read('./text' + fileno + '.txt');
});

Promise.all(promises)
.then(function(contents){
    console.log(contents);
})
.catch(function(err){
    console.log("error caught: " + err);
})

上述代码中会并发执行读取text1.txt和text2.txt的操作,如果两个文件都读取成功的话,输出他们的内容,组成数组(contents)。

Tips:
(1)Promise.race()也是将多个Promise实例包装成一个新的Promise实例(var p = Promise.race([p1,p2,p3])),上述代码中,p1、p2、p3只要有一个实例率先改变状态,p的状态就会跟着改变,那个率先改变的Promise实例的返回值,就传递给p的返回值。

(2)有时候需将现有对象转换成Promise对象,可以使用Promise.resolve()。

(3)Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

  • Generator函数

Generator函数的函数名前面有一个*,Generator函数封装了一个异步任务,需要异步操作的地方,使用yield标注。yield语句表示,执行到此处执行权就交给其他协程,也就是说,yield是两个阶段的分界线。协程遇到yield语句就暂停执行,直到执行权返回,再从暂停处继续执行。这种写法的优点是,可以把异步代码写得像同步一样。

function* gen(x){
    var y = yield x + 2;
    return y;
}

var g = gen(1);
var r1 = g.next(); // { value: 3, done: false }
console.log(r1);
var r2 = g.next() // { value: undefined, done: true }
console.log(r2);
  • ES 7中的async和await
    async和await是ES 7中的新语法,新到连ES 6都不支持,但是可以通过Babel一类的预编译器处理成ES 5的代码。目前比较一致的看法是async和await是js对异步的终极解决方案。
var fs = require('fs');

var readFile = function (fileName){
    return new Promise(function (resolve, reject){
        fs.readFile(fileName, function(error, data){
            if (error){
                reject(error);
            }
            else {
                resolve(data);
            }
        });
    });
};

var asyncReadFile = async function (){
    var f1 = await readFile('./text1.txt');
    var f2 = await readFile('./text2.txt');
    console.log(f1.toString());
    console.log(f2.toString());
};

asyncReadFile();