异步之一:现在与将来

65 阅读4分钟

分块的程序

我们可以把JavaScript代码写在单个js文件中,但是这个程序一定是多个块构成的,这些块中只有一个是现在执行的,其余在将来执行。最常见的块单位是函数

var data = ajax('http://soneurl')
console.log(data)

这里data通常不会包含ajax的结果,因为标准的ajax请求不是同步完成的,意味着可能要过几秒ajax才能获得数据。如果ajax能够阻塞到响应返回,那么data就会正常工作。事实上,如何精准地堵塞到响应返回,是JavaScript一直在探寻的问题

我们一般使用异步ajax请求,然后在将来得到返回的结果时,执行相应函数,这个函数就叫做回调函数

ajax('www.aaaa.com',function myCallbackFunction(data){
    cosole.log(data)
}

当然,我们也可以发送同步ajax请求。尽管技术上是这样说,但是在任何情况下都不应该通过这种方式,因为他会锁定浏览器UI(按钮、菜单、滚动条),并阻塞所有用户交互,这是一个可怕的想法

function now(){
    return 21
}

function later(){
    answer = answer * 2
    console.log('Meaning of life',answer);
}
var answer = now()
setTimeout(() => {
    later
}, 1000);

这段代码有两个块,现在执行的部分和将来执行的部分
现在

function now(){
    return 21
}

function later(){
 
}
var answer = now()
setTimeout(() => {
    later
}, 1000);

将来

 answer = answer * 2
 console.log('Meaning of life',answer);

现在这一块代码在程序运行后会立刻执行,但是setTimeout还设置了一个事件将来执行,所以函数later()的内容会在之后的某个时间(1s)执行

异步控制台

并没有什么规范指定console.*家族应该如何工作————他们并不是JavaScript正式的一员,而是由宿主环境添加到JavaScript的,即这些方法是浏览器实现,然后提供给JavaScript用的。因此不同浏览器和JavaScript环境可以按照自己的意愿来实现,有时候会混淆

有些浏览器的console.log()并不会将传入的内容立刻输出。出现这种情况的原因是:在许多程序中(不止JavaScript),I/O是非常低速的阻塞部分。所以,(从页面UI的角度来说),浏览器在后台异步处理控制台I /O能够提高性能。

下面这种情况不是很常见,但是也可能发生:

var a = {
    index:1
}
console.log(a);
a.index++

大部分情况下是输出{index:1}。但是也有一种可能,浏览器可能认为需要将控制台I/O延迟到后台,这样会导致控制台输出a时,a.index++已经执行完毕,这种情况下可能输出的是{index:2}。

优化的方法是在JavaScript中使用断点,而不要依赖控制台输出。次优的方案是将对象序列化成一个字符串,以强制执行一个快照,比如通过JSON.stringify

事件循环

JavaScript引擎并不是独立运行的,他运行在宿主环境中,这个宿主环境一般是web浏览器。

无论是什么样的宿主环境,这些环境都有一个共同点,即他们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用JavaScript引擎,这种机制被称为‘事件循环’。举例来说,如果JavaScript程序发出一个ajax请求,从服务器获取一些数据,那你就在一个函数(回调函数)中设置好响应代码,然后JavaScript引擎会通知宿主环境(web浏览器):“嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个函数”

那么,什么是事件循环呢?先通过一段伪代码了解一下这个概念

//eventLoop是一个用作队列的数组(先进先出)
var eventLoop = []
var event

// 永远执行
while (true) {
    // 一次tick
    if(eventLoop.length > 0){
        event = eventLoop.shift()
        try {
            event()
        } catch (error) {
            reportError
        }
    }
}

可以看到,有一个用while循环实现的持续运行的循环,循环的每一轮称为一个tick。

一定要清楚,setTimeout()并没有将回调函数挂在了事件循环队列中,是要当定时器到时后,环境会将你的回调函数放在事件队列中,这样,在将来的某个时刻的tick才会摘下并执行这个回调

如果当定时结束,你的回调放进了队列,但是这个时候事件循环队列已经有20个项目了怎么办?你的回调就会等待,并不能直接将其放在队头,这也解释了为什么setTimeout()的精度可能不高,有可能在这个时候执行,有可能在那个时候执行,要根据事件队列的状态而定

任务

在ES6中,有一个新概念是建立在事件循环队列之上的,叫做任务队列。

在事件循环的每一个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而是会在当前tick的任务队列末尾添加一个任务,更详细的运作,就需要学到promise啦