最近看了深入理解JavaScript异步机制和再谈Event-Loop两篇博客文章,感觉收获颇多,所以想总结一下自己学到的东西,当然内容和两位巨佬差的太远。本文只是个人的一些理解,如果有不对的地方,还望指出.
Let’s start !
什么是异步
所谓异步,就是代码执行的顺序并不是按照从上到下的顺序一次性一次执行,而是在不同的时间段执行,一部分代码在“未来执行”。
程序中现在运行部分和未来运行部分的关系就是异步编程的核心 ——— ——《YDNJ》
setTimeout( ()={ console.log('1') },1000)
console.log("2")
//2
//1
如果没有异步的话,应该是按照顺序先执行,先输出1,再输出2,但是setTimeout是异步操作,在1000ms后的”将来执行“,所以先执行console.log(“2”)。
回调函数
回调函数我们都比较熟悉。如果有两个函数f1和f2,其中f1是异步函数,而f2是同步函数,如何让f2函数在f1之后执行?答案就是回调函数。
幸运的是,基本绝大多数的异步操作都会给我们提供一个加入回调函数的接口,我们可以传入一个函数,在异步操作结束后"立即”调用(为什么加引号在异步操作的机制中会探讨)。
我们之前所举的例子中 ,setTimeout( ()=>{ console.log(‘1’) },1000) 就是往异步函数setTimeout中传入回调函数,在计时1秒后调用。
再比如我们常用的ajax:
function f2(){
console.log("我是回调函数")
}
$.ajax("www.baidu.com") //在百度页面打开控制台运行,省的跨域
f2()
//我是回调函数 f2先执行
//undifined
$.ajax("www.baidu.com",{success:f2})
在ajax获取数据后调用f2。
异步操作的机制
异步操作时浏览器(或者其他环境)是如何工作的?回调函数在何时调用,如何调用?我们通过异步操作的机制的学习都能得到答案。
在主程序结束和回调函数调用期间,是没有JS代码运行的,这是否意味着此时我们的浏览器是停止工作的?当然不是。
其实,绝大多数异步操作的功能并不是完全由JS完成的,JS引擎不会计时,也不会发送和接受网络请求,这一切都是由浏览器(或者Node等工作环境)来完成的,js引擎只是通过调用浏览器给它的接口,"命令"浏览器完成这些功能。所以异步操作不代表不工作,只是该工作转交给浏览器处理 ,当浏览器处理完这部分操作后会"告诉"JS引擎来调用回调函数。
然而这个"告诉"并不是我们想象的那样是浏览器主动对JS引擎说"我已经处理好了",而是JS引擎每隔一段时间就问浏览器"你有没有完成操作",一旦完成,就调用回调函数。这个过程不断重复就叫做Event Loop,如下图所示:
解释一下这张图,当进程中的JavaSrcipt代码执行结束后,会检查任务队列中是否有任务,如果有任务,就立刻执行,如果没有任务,就过一段时间再次检查,循环往复。
任务队列又是什么呢?浏览器每次完成一个异步任务,就会在一个队列(先进先出)中插入回调函数, 一旦EventLoop中有内容,并且此时没有JS程序运行,JS引擎就会调用该队列的第一个回调函数 ;但是如果此时有JS程序正在运行,需要等该JS代码运行完,然后才会依次调用任务队列中的回调函数。
然后我们也就可以解释下面这个代码了:
setTimeout('console.log("1")',0)
console.log('2')
输一下,结果居然是先输出2,再输出1,setTimeout不是设时间为0么,为什么没有立刻执行?其实计时器虽然设为0,会立即在任务队列中加入console.log(“1”),但是此时还有代码在运行,所以不能执行任务队列中的任务,等console.log(‘2’)执行完毕,主程序执行完,才会调用任务队列中的console.log(1)。
接下来来个更加详细的介绍图:
我来解释一下,首先是左侧JS一栏,代码运行时,把代码加载到一个栈(stack)中,按照顺序执行,并且将数据存在堆(heap)中,要使用时调用。
然后:
1.在代码执行过程中,一旦遇到异步操作,会调用浏览器提供给JS引擎的接口(WebAPIS),让浏览器处理DOM、ajax、setTimeout等操作,然后JS引擎继续处理下面的JS,两者间的工作互不干扰。(接下来的2和3是同时进行的,部分先后)
2.JS引擎把JS代码加载完毕,就检查任务列表中是否有任务要处理,有的话就执行,没有的话,等待一会再检查。
3.浏览器一旦把异步操作处理完毕,就在任务队列中加入onClick,onLoad回调函数等 。 (注意这里不是具体传入的函数,而是onClick,onLoad,但是onClick、onLoad执行时会根据作用域等在堆中找到相应的函数值,然后执行)
.
4.重复上述过程
最后再来个例子热热身:下面两行代码运行结果是否有区别
var req = new XMLHttpRequest( );
req.open(‘GET’ ,url);
req. = function(){};
req. = function(){};;
req.send()
var req = new XMLHttpRequest( );
req.open('GET' ,url);
req..send();
req. = function(){};
req. = function(){};;
答案是肯定没有的。
因为req.send是异步操作,在操作完成后,不能立即调用和,而是要等到主程序执行完毕,此时在上面两端代码中,两个函数都完成赋值。
下一篇我们将继续讨论更多异步操作