【javascript】异步编年史,从“纯回调”到Promise
异步和分块——程序的分块执行
一开始学习javascript的时候, 我对异步的概念一脸懵逼, 因为当时百度了很多文章,但很多各种文章不负责任的把笼统的描述混杂在一起,让我对这个 JS中的重要概念难以理解, “异步是非阻塞的”, “Ajax执行是异步的”, "异步用来处理耗时操作"....
所有人都再说这个是异步的,那个是异步的,异步会怎么怎样,可我还是不知道:“异步到底是什么?”
后来我发现,其实理解异步最主要的一点,就是记住: 我们的程序是分块执行的。
分成两块, 同步执行的凑一块, 异步执行的凑一块,搞完同步,再搞异步
废话不多说, 直接上图:
图1
图2
异步和非阻塞
我对异步的另外一个难以理解的点是异步/同步和阻塞/非阻塞的关系
人们常说: “异步是非阻塞的” , 但为什么异步是非阻塞的, 或者说, 异步和非阻塞又有什么关系呢
非阻塞是对异步的要求, 异步是在“非阻塞”这一要求下的必然的解决方式
咱们看看一个简单的例子吧
ajax("http://XXX.", callback); doOtherThing()
你肯定知道ajax这个函数的调用是发出请求取得一些数据回来, 这可能需要相当长的一段时间(相比于其他同步函数的调用)
对啊,如果我们所有代码都是同步的,这就意味着, 在执行完ajax("http://XXX.", callback)这段代码前, doOtherThing这个函数是不会执行的,在外表看起来, 我们的程序不就“阻塞”在ajax("http://XXX.", callback);这个函数里了么? 这就是所谓的阻塞啊
让我们再想一想doOtherThing因为“同步”造成“阻塞”的话会有多少麻烦: doOtherThing()里面包含了这些东西: 这个简略的函数代表了它你接下来页面的所有的交互程序, 但你现在在ajax执行结束前,你都没有办法去doOtherThing,去做接下来所有的交互程序了。 在外观上看来, 页面将会处于一个“完全假死”的状态。
因为我们要保证在大量ajax(或类似的耗时操作)的情况下,交互能正常进行
所以同步是不行的
因为同步是不行的, 所以这一块的处理, 不就都是异步的嘛
如果这样还不太理解的话, 我们反方向思考一下, 假设一个有趣的乌托邦场景: 假设ajax的执行能像一个同步执行的foreach函数的执行那样迅速, javascript又何苦对它做一些异步处理呢? 就是因为它如此耗时, 所以javascript“审时度势”,
拿出了“异步”的这一把刷子,来解决问题
正因为有“非阻塞”的刚需, javascript才会对ajax等一概采用异步处理
“因为要非阻塞, 所以要异步”,这就是我个人对异步/同步和阻塞/非阻塞关系的理解
可能你没有注意到,回调其实是存在很多问题的
没错,接下来的画风是这样子的:
回调存在的问题
回调存在的问题可概括为两类:
信任问题和控制反转
可能你比较少意识到的一点是:我们是无法在主程序中掌控对回调的控制权的。
例如:
ajax( "..", function(..){ } );
我们对ajax的调用发生于现在,这在 JavaScript 主程序的直接控制之下。但ajax里的回调会延迟到将来发生,并且是在第三方(而不是我们的主程序)的控制下——在本例中就是函数 ajax(..) 。这种控制权的转移, 被叫做“控制反转”
1.调用函数过早
调用函数过早的最值得让人注意的问题, 是你不小心定义了一个函数,使得作为函数参数的回调可能延时调用,也可能立即调用。 也即你使用了一个可能同步调用, 也可能异步调用的回调。 这样一种难以预测的回调。
大多数时候,我们的函数总是同步的,或者总是异步的
例如foreach()函数总是同步的
array.foreach( x => console.log(x) ) console.log(array)
虽然foreach函数的调用需要一定的时间,但array数组的输出一定是在所有的数组元素都被输出之后才输出, 因为foreach是同步的
又如setTimeout总是异步的:
setTimeout( () => { console.log('我是异步的') },0 ) console.log('我是同步的')
有经验的JS老司机们一眼就能看出, 一定是输出
我是同步的
我是异步的
而不是
我是异步的
我是同步的
但有些时候,我们仍有可能会写出一个既可能同步, 又可能异步的函数,
例如下面这个极简的例子:
我试图用这段代码检查一个输入框内输入的账号是否为空, 如果不为空就用它发起请求。(注:callback无论账号是否为空都会被调用)
// 注: 这是一个相当乌托邦,且省略诸多内容的函数 function login (callback) { // 当取得的账号变量name的值为空时, 立即调用函数,此时callback同步调用) if(!name) { callback(); return // name为空时在这里结束函数 } // 当取得的账号变量name的值不为空时, 在请求成功后调用函数(此时callback异步调用) request('post', name, callback) }
相信各位机智的园友凭第六感就能知晓:这种函数绝B不是什么好东西。
的确,这种函数的编写是公认的需要杜绝的,在英语世界里, 这种可能同步也可能异步调用的回调以及包裹它的函数, 被称作是 “Zalgo” (一种都市传说中的魔鬼), 而编写这种函数的行为, 被称作是"release Zalgo" (将Zalgo释放了出来)
为什么它如此可怕? 因为函数的调用时间是不确定的,难以预料的。 我想没有人会喜欢这样难以掌控的代码。
例如:
var a =1 zalgoFunction () { // 这里还有很多其他代码,使得a = 2可能被异步调用也可能被同步调用 [ a = 2 ] } console.log(a)