JavaScript中异步代码的前世今生
JavaScript
右领军大都督
2人收藏 3139次学习

JavaScript中异步代码的前世今生

本文灵感扩展自javascript-async-history,如希望快速浏览的,也可以看这个。

由于本文仅关注于异步代码编写,对异步本身如何实现,不会多做介绍,如有兴趣,请各位同学自己google吧^^!

言归正传,我们这就上路!

javascript中,面对一个异步任务时,我们是如何自处的?以下几节,我们就来谈谈怎么编写异步代码,她们的风格又是怎么样一步步进化的。

回调故事

“上古”时代,谈异步,就不能不提回调(callback),那我们先来看看,一个舒服的回调函数,应该长什么样?

'use strict'

getSession(function(session){
    console.log('userId is', session.userId);//获取当前session信息,并输出session中的userId
});

 

可回调也有令人难过的时候,譬如:


'use strict'

getSession(function(session){//获取session

    getCurrentUser(session.userId, function(user){//根据session中的userId获取当前用户信息

        getUserInfo(user.bestFriendId, function(friend){//根据用户中的bestFriendId获取该用户信息

            getPosts(friend.postCode, function(posts){//根据该用户的postCode获取他的所有发帖信息

                console.log('posts are', posts);//层层嵌套,是不是有点绕了?
            });
        });
    });
});

以上例子,可能是个伪需求,我们不必执着于它存在的具体可能性,只谈它背后的隐患,想必各位在项目中都会遇到类似的嵌套场景,真实场景中可能更甚,这种现象,我们就称之为“回调地狱(callbackhell)”。

 

既然回调可能使我们落入深渊,那可有解法?

Thunk为何物?

讲完了回调地狱,我们先别急着往下走,既然说了是进化史,那就谈谈在处理异步任务的路上,我们又有什么其他发现吧!

Thunk的背景故事

那是很久很久以前,计算机科学家们还在争论哪种求值策略(Evaluation Strategy)更先进的时候,因为以下两种策略的争论,导致了Thunk的诞生,我们来看下,到底是哪两种策略:

 

  • 传值调用(call by value) - 在函数调用之前,先将要传入该函数的参数表达式计算完毕,然后将计算结果传入该函数
  • 传名调用(call by name) - 在函数调用时,将未计算的的参数表达式直接传入该函数,在合适的时机(通常就是被使用时)才进行计算

 

说了概念,我们来上点代码,有助于大家理解这两种求值策略:

传值调用:

'use strict'

var x = 1;
var sum = function(m){
    return m + 5;
};

//传值调用中,以下两个函数调用的,其实是一样的
sum(x + 2);

sum(3);

通常,人们会认为这种策略会造成一定程度的资源浪费,因为这个“传入前就计算好的参数,很可能在函数中因为某些流程控制,而根本没有使用”

 

传名调用:

'use strict'

var x = 1;
var sum = function(m){
    return m + 5;
};

//传名调用中,下面这个调用
sum(x + 2);

//相当于这个
(x + 2) + 5;

通常,如果只是将参数表达式,传入函数中,将相应位置的参数变量替换,人们也会认为这种策略会在内存中产生多个版本的函数副本,和多个参数表达式的副本,也是一种浪费

 

为了让众人皆喜,于是有了Thunk这个概念,先来看下针对上面的这个案例,Thunk是怎么处理的:

'use strict'

var x = 1;

var sum = function(exp){
    return exp() + 5;
};

//先将参数表达式写到一个临时函数之中,
//再将这个临时函数传入被调用函数。
//我们把这个“临时函数”就叫做 Thunk 函数
var thunky = function () {
  return x + 2;
};

sum(thunky);

 

这下各位可还精神状况良好?没事,我知道现在仍然和异步没有毛的关系,莫急、莫哭,我们再往下看看。

Thunk如何改变异步代码?

大多数异步的JavaScript APIs都遵循这样一个原则,即“可以接受多个参数,但回调函数永远放在最后一位,而且是可选项”。

关于这个原则,有兴趣的朋友可以看这里

 先来看个例子:

'use strict'

var fs = require('fs');

fs.readFile('/etc/hosts', 'utf-8', function(err, data){
    console.log('文件的内容是', data);
});

 

那对于Thunk的使用,如何改变这个readFile函数的调用过程呢?诸位看这里:

'use strict'

var fs = require('fs');

//我们管这个过程叫做"Thunkify"
var readFile = function(fileName, options){
    return function(callback){
        return fs.readFile(fileName, options, callback);
    };
};

var thunky = readFile('/etc/hosts', 'utf-8');

//过程已变,现在只接受一个回调函数了哦!
thunky(function(err, data){
    console.log('文件内容是', data);
});

 

这时,有朋友或许会说“这算哪门子的优化,代码变多了不说,好像还更复杂了!”,^^,还好,对于那个"Thunkify"的过程,我们tj大大已经写了一个工具thunkify帮我们做这个脏活儿,来我们试试吧:

'use strict'

var thunkify = require('thunkify');//引用大大的thunkify工具
var fs = require('fs');

var readFile = thunkify(fs.readFile);//thunkify一下fs.readFile函数

var thunky = readFile('/etc/hosts', 'utf-8');

thunky(function(err, data){
    console.log('文件内容是', data);
});

 

事已至此,想必朋友们又要问了“还是看不出来这有多先进啊!”。

 

是的,无论我多么想反驳你,但到目前为止还真是“然并卵”,不过好戏尚在后头,我们继续吧?

Promise能帮我们么?

由于本文不是专讲Promise的使用,亦或原理细节,请大家见谅我不会就每一个细节讲太多。

 

先来看看之前那个稍微有点儿“回调地狱(callback-hell)”的例子,如果用Promise改写,会是什么样子:

'use strict'

getSession()
.then(function(session){
    return getCurrentUser(session.userId);
})
.then(function(user){
    return getUserInfo(user.bestFriendId);
})
.then(function(friend){
    return getPosts(friend.postCode);
})
.then(function(posts){
    console.log('posts are', posts);
})
.catch(function(err){
    console.error('ERROR', err);
});

 

这样的写法,的确是好了许多。如果你想知道更多“Promise”是如何做到这样的,可以参看我另一篇一步步来手写一个Promise

Promiseify为何物?

很多时候,我们将要使用的APIs,是遵循回调流程的,并非Promise,譬如大多数node异步APIs。

 

想象一下,如果这些APIs可以Promise风格调用的话?

'use strict'

var fs = require('fs');

//想必那是极好的
fs.readFile('/etc/hosts', 'utf-8')
.then(function(data){
    console.log('文件内容是', data);
})
.catch(function(err){
    console.error('ERROR', err);
});

这个时候,如果使用promiseify的话,一切就都美好了:

'use strict'

var fs = require('fs');
var promiseify = require('just-promiseify');//引入promiseify库

var readFile = promiseify(fs.readFile);//和thunkify用法差不多

//这下可high?
readFile('/etc/hosts', 'utf-8')
.then(function(data){
    console.log('文件内容是', data);
})
.catch(function(err){
    console.error('ERROR', err);
});

一个回调风格的函数,就这样被转换成了Promise

Promise就是异步编码的终结了么?

其实,这里一定还要再说一个其实,generator能帮我们更进一步,试想一下,如果异步代码编写能和同步代码编写时有同样的感受,像下面这样是不是碉堡了?

'use strict'

try{
    var session = getSession();
    var user = getCurrentUser(session.userId);
    var friend = getUserInfo(user.bestFriendId);
    var posts = getPosts(friend.postCode);
    console.log('posts are', posts);
}catch(e){
    console.error('ERROR', err);
}

 

来吧,tj大大又提供了我们一个generator的自动执行器co,我们一起来看下generator是怎么帮我们把异步代码变漂亮的:

'use strict'

var co = require('co');//引入co函数

var execute = function*() {//*代表声明该函数为generator,可是光声明generator是“然并卵”喔

    try {//yield关键字后面的内容,我们称之为“yieldables”

        var session = yield getSession();
        var user = yield getCurrentUser(session.userId);
        var friend = yield getUserInfo(user.bestFriendId);
        var posts = yield getPosts(friend.postCode);

        console.log('posts are', posts);
    } catch (e) {
        //妈妈再也不用担心你的异常处理了,不用什么各种then,catch
        //只要和同步代码一样,try catch一下就好了
        console.error('ERROR', err);
    }
};

co(execute);//不过有了大大的执行器,这篇代码就high了

OK,总算可以绕回之前讲了半天的Thunk函数了,根据co文档的介绍:

The yieldable objects currently supported are:

  • promises
  • thunks (functions)
  • array (parallel execution)
  • objects (parallel execution)
  • generators (delegation)
  • generator functions (delegation)

事到如今,Thunk总算是和异步编码扯上了点关系!^^ 因为yield关键字后面,可以跟promise和thunk

ES7中的async + await能做什么?

async + await是generator的语法糖,就使用上而言,它们差别不大,不过有以下项几个优点:

  • 内置执行器,所以tj大大的co函数已无用武之地,难怪大大离开了JavaScript,奔go去了
  • 更高的可读性,async + await的表现力,想必比* + yield更容易理解吧
  • 更健壮,await后还可以跟原始类型,使用场景可以更广泛

 那我们看看,换成async + await后,之前的例子会改写成什么样儿?

'use strict'

var execute = async function() {//加async关键字后,该函数就成了异步函数

    try {//这里,await比yield更具表现力,也支持更多类型

        var session = await getSession();
        var user = await getCurrentUser(session.userId);
        var friend = await getUserInfo(user.bestFriendId);
        var posts = await getPosts(friend.postCode);

        console.log('posts are', posts);
    } catch (e) {
        console.error('ERROR', err);
    }
};

execute();//无需额外执行器,即可执行

 

当然,ES7本身目前尚未完全定稿,无论node还是浏览器环境,都不原生支持async,只能通过babel之类的工具来转义,所以诸位也不用哭,市面上已有不少方案了,自行Google吧!

写在最后

今日只谈异步编码的进化过程,并没有涉及其中某项技术的具体实现细节和使用要领,为的是给大家一个直观的感受,体会几年之间在javascript里异步编码的风格转变,以及这其中对我们软件工程可能产生的影响。

更多的内容,还请期待

加入1KE学习俱乐部

1KE学习俱乐部是只针对1KE学员开放的私人俱乐部
标签:
进阶 JavaScript