01月22, 2018

JavaScript中的异步

本篇文章,主要总结一下,JavaScript中异步的处理方式。JavaScript异步处理的方式主要包括以下几种形式。 回调函数、发布订阅、Promise、Generator、async&await、Thunk。

本章我们分别来讲一下不同处理异步的方式是什么样的,以及它们之间的联系。

回调函数

回调函数比较简单,它也是最开始我们常用的一种处理异步的方法。简单的例子如下:

// 假设有如下异步函数
function asy(url, callback){
    $.ajax({
        url: url,
        success: function(data){
            callback(data)
        }
    })
}

如果我们有多个异步操作,需要串联执行,那我们的代码可能变成了这个样子。

    asy('https://example.com', function(){
        // 一些处理
        asy('https://example2.com', function(){
             // 一些处理
            asy('https://example3.com', function(){
                // 一些处理
                。。。
            })
        })
    })

上面的例子把所有的回调写在一起,还算能看清基本的处理顺序,如果每个回调都单独定义一个方法,调试的时候恶心程度可想而知。并且代码的耦合嵌套太深,不利于维护,同时错误处理能力也很弱。这也就是我们常说的回调地狱。

发布订阅

为了减少代码之间的耦合。于是出现了发布订阅的模式。

首先来写一个最简单的发布订阅对象:

class Observer{
    constructor(){
        if(Observer.instance instanceof Observer){
            return Observer.instance;
        }
        Observer.instance = this;
        this.cbs = {}
        return this;
    }
    subscribe(name, cb){
        this.cbs[name] = (this.cbs[name] || []).concat([cb])
    }
    publish(name, ...args){
        this.cbs[name].forEach((cb)=>{
            cb.apply(this, args);
        })
    }
}

还是上面的例子,改写一下:

var OB = new Observer();
OB.subscribe('step1', function(){
    ayc('https://example2.com', function(data){
        // 一些处理
        OB.publish('step2',data)
    })
});
OB.subscribe('step2', function(){
    ayc('https://example3.com', function(data){
        // 一些处理
    })
});
ayc('https://example1.com', function(data){
    // 一些处理
    OB.publish('step1', data);
});

发布订阅模式有了一个中心调度的控制对象。我们首先给控制对象中添加了“step1”和”step2“两个信号以及对应的回调方法。"https://example1.com"的请求成功后,发送"step1"信号并执行相关的回调,同理”https://example2.com"的请求成功后,发送"step2"信号并执行相关的回调。

相对于回调地狱,这种形式稍有改进,但多次异步操作的流程直观上看起来还是不够清晰。

Promise

关于Promise,之前我写过五篇文章,详细的介绍了Promise的原理和用法等。这里不过多的讲述它的用法。

上面的例子,用Promise来写的话,应该怎么写:

// 首先是异步函数
function asy(url){
    return new Promise((resolve, reject)=>{
         $.ajax({
            url: url,
            success: function(data){
                resolve(data)
            }
        })
    })
}

asy('https://example.com').then((data)=>{
    // 一些操作
    return asy('https://example2.com')
}).then((data)=>{
    // 一些操作
    return asy('https://example3.com')
}).then((data)=>{
    // 一些操作

}).catch((err)=>{
    // 错误处理
})

我们可以看到,和之前回调的形式相比,Promise结构更加清晰,流程步骤也更加清晰。Promise把之前一层层嵌套的回调拉平了。同时前面流程中出现的错误,可以通过最后一个catch来处理。

Promise还是有一定的问题。我们希望的可能是这样的:

let data = asy('https://example.com');
// 一些操作
let data2 = asy('https://example2.com');
// 一些操作
let data3 = asy('https://example3.com');
// 一些操作

Promise其实就是把之前的异步函数做了一层包裹,然后多了许多then等额外的代码。于是,就有了awaitasync。在讲awaitasync之前,先来说一下Generator

Generator

关于Generator的详细具体用法,我这里也不过多介绍,之前写过一个ppt,介绍了迭代器和生成器的相关内容。更加详细的内容,可以去看阮老师的文章

这里我们用简单介绍一下Generator的用法。

Generator有两个特征,一是function关键词和函数名之间有一个星号,二是函数内部可以用yield暂停函数的执行。Generator函数调用后,并不会立即执行,而是会返回一个迭代器对象,通过迭代器对象来一步一步控制它的执行。

还是上边的例子,用Generator改写是什么样呢?

function asy(url){
    return new Promise((resolve, reject)=>{
         $.ajax({
            url: url,
            success: function(data){
                resolve(data)
            }
        })
    })
}

function *gen(){
    var data = yield asy('https://example.com');
    // 一些操作
    var data1 = yield asy('https://example2.com');
    // 一些操作
    var data2 = yield asy('https://example3.com');
    // 一些操作
}

var it = gen();

it.next().value.then((data)=>{
    return it.next(data).value;
}).then((data)=>{
    return it.next(data).value;
}).then((data)=>{
    return it.next(data).value;
})

我们把串行的异步操作封装到一个生成器函数中,然后每一个异步操作前添加yield表达式来控制流程。只看生成器里面的代码,基本上和我们刚才所说的目标一致了。但是我们需要在外部用一系列代码控制它的执行。

从上面的代码中我们也可以看到,控制流程的代码其实很相似,我们可以写一个函数来控制生成器函数自动自行。

function run(gen){
    var it = gen();
    function next(data){
        var result = it.next(data);
        if(result.done){
            return result.value
        }
        result.value.then((data)=>{
            next(data)
        })
    }
    next();
}

这样,我们就只需要在最后执行一个run(gen)就可以了,而不需要手写一堆控制执行的代码。当然,这个自动执行的函数有一个前提是,所有异步操作返回的都是Promise

上面自己写的run只是一个简单实现的示例,TJ大神写的co模块,就是用来自动执行生成器函数的。感兴趣的童鞋可以自行查看源码

await && async

既然我们需要多此一举的写一个自动执行生成器的函数,那有没有更好的方法呢?答案当然是有了,这就是我们的awaitasync

function asy(url){
    return new Promise((resolve, reject)=>{
         $.ajax({
            url: url,
            success: function(data){
                resolve(data)
            }
        })
    })
}

async function main(){
    var data = await asy('https://example.com');
    // 一些操作
    var data1 = await asy('https://example2.com');
    // 一些操作
    var data2 = await asy('https://example3.com');
    // 一些操作
}
main()

这样,异步操作的写法和同步操作的写法就很相似了。基本上达到了我们想要的效果。

如果用babelawaitasync转换为ES2015,可以发现最终转换的结果就是生成器加一个自动执行生成器的函数。

Thunk

前面我们提到了自动执行生成器函数时,异步操作函数返回的必须是Promiseco模块还支持另一种返回形式的异步函数,那就是Thunk函数。

Thunk函数是把多参数函数替换成一个只接受回调函数作为参数的单参数函数。

例子如下:

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

Thunk函数和Promise做一个类比。Promise在成功时,调用then添加的回调,而Thunk函数内部还是成功时调用回调函数,只不过是回调函数单独提取出来作为它的参数。这样做的目的,就是为了自动执行生成器。

还是之前的例子,用Thunk函数来执行:

const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

function asy(url, callback){
    $.ajax({
        url: url,
        success: function(data){
            callback(data)
        }
    })
}

function *gen(){
    var data = yield Thunk(asy)('https://example.com');
    // 一些操作
    var data1 = yield Thunk(asy)('https://example2.com');
    // 一些操作
    var data2 = yield Thunk(asy)('https://example3.com');
    // 一些操作
}

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

run(gen)

总结

本篇文章要介绍的内容也就这么多,主要是讲了一下同样的一系列串行异步操作,随着js的发展,每个时期是如何处理和改进的。每一种方式都有许多自己的细节,需要各位自己去查阅资料学习。

本文链接:https://www.imliutao.com/post/asynchronous.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。