十四、JS同步异步知识点,重点(Node.js-fs模块补充篇)
(本片文章如果你能耐着性子看我,保证会对同步和异步有一个非常深刻的理解)
JavaScript是单线程执行,所谓的单线程呢就是指如果有多个任务就必须去排队,前面任务执行完成后,后面任务再执行。因为JavaScript是一门单线程语言,所以我们可以得出结论:
JavaScript是按照语句出现的顺序执行的
一、同步和异步
同步
如果在函数返回结果的时候,调用者能够拿到预期的结果(就是函数计算的结果),那么这个函数就是同步的.
console.log('hello');//执行后,获得了返回结果
如果函数是同步的,即使调用函数执行任务比较耗时,也会一致等待直到得到执行结果。(所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。)如下面的代码:
function wait(){ for(var i=0;i<9999999999;i++){ if (i==9999999999){ console.log('i需要慢慢的加1直到99999999999999'); return } } } wait(); console.log('要等到i增加到9999999999(也就是wait()函数执行完毕)才能打印出这句话,大概要等10秒左右');
上面代码中,函数wait是一个耗时程序,持续10秒左右,在它执行的这漫长的10秒中,下面的console.log()函数只能等待,这就是同步。所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。
异步
如果在函数返回的时候,调用者还不能够得到预期结果,而是将来通过一定的手段得到(例如回调函数),这就是异步。例如处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程等等。 如果函数是异步的,发出调用之后,马上返回,但是不会马上返回预期结果。调用者不必主动等待,当被调用者得到结果之后会通过回调函数主动通知调用者。
function wait(){//下面每句打印的话前面的数字是她们在控制台打印出来之后的先后顺序 console.log('1:wait()函数开始执行了'); var time1 = (new Date()).getTime();//获取当前的unix时间戳 setTimeout(function () { var time2=new Date().getTime(); while((time2-time1 > 5000)){ console.log('4:5秒过去了'); return } },5000)//等5秒然后获取当前的unix时间戳 console.log('2:wait()函数执行结束了'); } wait(); console.log('3:虽然js会先执行上面的wait函数,并且里面有个5秒的定时器,但是定时器函数是异步的,意思就是说wait()函数里面的定时器启动,并开始计时的时候并不影响继续执行定时器之后的代码');
上面的代码的打印结果:
例二:
//打印出什么 3 3 3 for(var i=0;i<3;i++){ setTimeout(function(){//凡是异步操作,一定是在所有同步操作执行完毕之后才能执行,也就是for循环执行完毕之后才能执行定时器里面的代码,此时i=3了 console.log(i); },100) }
二、单线程与多线程
了解完同步和异步之后,我们再来看看我们的问题:单线程又怎么会有异步呢?
JavaScript其实就是一门语言,说是单线程还是多线程得结合具体运行环境。众所周知,js的运行环境就是浏览器,具体由js引擎取解析和执行。下面我们来了解下浏览器。
浏览器
一个浏览器通常由以下几个常驻的线程:
- 渲染引擎线程,负责页面的渲染
- js引擎线程,负责js的解析和执行
- 定时触发器线程,处理setInterval和setTimeout
- 事件触发线程,处理DOM事件
- 异步http请求线程,处理http请求
要注意的是渲染引擎和js引擎线程是不能同时进行的。渲染线程在执行任务的时候,js引擎线程会被挂起。因为若是在渲染页面的时候,js处理了DOM,浏览器就不知道该听谁的了
JS引擎
通常讲到浏览器的时候,我们会说到两个引擎:渲染引擎和JS引擎。
1、渲染引擎:Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trdent引擎,FireFox用的是Gecko引擎。不同的引擎对同一个样式的实现不一致,就导致浏览器的兼容性问题。
2、JS引擎:js引擎可以说是js虚拟机,负责解析js代码的解析和执行。通常有以下步骤:
- 词法解析:将源代码分解位有意义的分词
- 语法分析:用语法分析器将分词解析成语法树
- 代码生成:生成机器能运行的代码
- 代码执行
不同浏览器的js引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。之所以说js是单线程就是因为浏览器运行时只开启一个js解释器,原因是若有两个线程操作DOM,浏览器就又晕了。
JavaScript是单线程的,但是浏览器不是单线程的。一些I/O操作,定时器的计时和事件监听是由其他线程完成的。
三、消息队列与事件循环
由上面浏览器一篇的介绍可以知道,浏览器中多个线程的合作完成了异步的操作,那么异步的回调函数又是怎样完成执行的呢?
这就需要了解消息队列和事件循环了。
如上图所示,左边的栈存储的是同步任务,就是那些能立即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不需要回调函数的操作都可归为这一类。右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每个异步任务都和回调函数相关联。
JS引擎线程用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。
JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环。
上图以AJAX异步请求为例,发起异步任务后,由AJAX线程执行耗时的异步操作,而JS引擎线程继续执行堆中的其他同步任务,直到堆中的所有异步任务执行完毕。然后,从消息队列中依次按照顺序取出消息作为一个同步任务在JS引擎线程中执行,那么AJAX的回调函数就会在某一时刻被调用执行。