面试题:请问执行结果是什么?
console.log(1);setTimeout(function(){ console.log(3); },0);console.log(2);复制代码
输出结果:
一、javascript是单线程异步执行的。
单线程:意味着代码在任务队列中会按照顺序一个接一个的执行。
异步:代表代码在任务队列中的顺序并不等同于代码的书写顺序。
既然JavaScript是单线程机制,那Ajax为什么是异步的?setTimeout()是怎样执行的?
在浏览器中,javascript引擎是单线程执行的。也就是说,在同一时间内,只能有一段代码被javascript引擎执行。页面加载时,javascript引擎会顺序执行页面上所有的javascript代码,优先执行同步代码。而异步代码由事件触发引擎按照“事件发生”的顺序添加到javascript引擎的异步队列中,等待所有同步代码执行完毕后,javascript引擎会按照异步队列中的顺序来执行异步代码。
二、理解js为什么是单线程的?
三、关于javascript引擎和浏览器内核
javascript引擎是单线程运行的,浏览器无论在什么时候,有且只有一个线程在运行javascript程序。
浏览器的内核是多线程的。他们在内核控制下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程、GUI渲染线程、浏览器事件触发线程。
1、javascript引擎:是单线程的。
2、GUI渲染引擎:负责渲染浏览器界面,当页面需要重绘(repaint)或者某种操作引发回流(reflow)时,该线程就会执行。但需要注意,GUI渲染线程与javascript引擎是互斥的。当Javascript引擎执行时,GUI线程会被挂起,GUI更新会被保持在一个队列中,等待javascript引擎空闲时立即执行。
3、事件触发线程:当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,当线程中没有任何同步代码的前提下才会执行这些异步代码。
四、理解任务队列(消息队列)
对同步任务和异步任务的理解
例1:
console.log("A");while(true){}console.log('B');复制代码
如图所示:输出结果为A,原因程序由上到下执行的过程中遇到了while死循环下面的内容将无法执行。
console.log("A");setTimeout(function(){ console.log('B');},0);while(true){}复制代码
输出结果:
输出结果还是A,原因:setTimeout()是一个异步任务,在所有的同步任务执行完前,任何异步任务是不会执行的。
五、理解Event Loop
异步运行机制:
1、所有的同步任务都在主线程上执行,形成一个执行栈。
2、循环队列之外,还有一个“任务队列”,当javascript引擎发起一个异步任务时,一方面继续执行后面的同步代码,一方面开始倒计时,把异步任务在xms之后添加到任务队列中去。
3、一旦任务栈中所有的同步代码执行完毕后,系统就会读取“任务队列”中的事件,使其结束等待状态,进入执行栈,开始执行。
4、主线程不断重复步骤3。
从setTimeout理解事件循环机制
例:
console.log('Hi');setTimeout(function cd(){ console.log('there');},5000);console.log('SJS');复制代码
1、首先main函数的执行上下文入栈
2、接着执行,遇到console.log('Hi');此时log('Hi')入栈,并立即执行,输出'Hi'。
3、当遇到setTimeout的时候,执行引擎将其添加到栈中。
4、调用栈发现setTimeout是之前提到的WebAPIs中的API,因此将其出栈,并将延时执行的函数交给浏览器的timer模块进行处理。
5、timer模块去处理延时执行的函数,此时执行引擎接着执行将log(‘SJS’)添加到栈中,此时输出’SJS’。
6、当timer模块中延时方法规定的时间到了之后就将其放入到任务队列之中,此时调用栈中的task已经全部执行完毕。
7、调用栈中的task执行完毕之后,执行引擎会接着看执行任务队列中是否有需要执行的回调函数。这里的cb函数被执行引擎添加到调用栈中,接着执行里面的代码,输出’there’。等到执行结束之后再出栈。
六、哪些语句会放入异步任务队列及放入时机
一般来说,有以下四种会放入异步任务队列:
- setTimeout和setlnterval
- DOM事件
- ES6中的Promise
- Ajax异步请求
放入时机:
例1:
for(var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); },1000)};复制代码
执行结果:
javascript引擎遇到setTimeout()时,并不是马上把setTimeout()拿到异步队列中,而是以便执行同步代码,一边在timer模块中之行倒计时,要等到一秒后,才将其放到任务队列里面,一旦"执行栈"中的所有同步任务执行完毕(即for循环结束,此时i已经为5),系统就会读取已经存放"任务队列"的setTimeout()(有5个),于是答案是输出5个5。
例2:
$.ajax({ url:"xxxxx", success:function (result){ console.log("a")}});setTimeout(function (){ console.log("b")},100);setTimeout(function (){ console.log("c")});console.log("d");复制代码
ajax、setTimeout都是异步任务,所以主线程先执行同步代码块,首先输出d;ajax和setTimout的执行顺序跟他们放入异步队列的先后顺序有关,c肯定是最先被放入异步队列的,a和b的先后顺序不确定,有两种情况:①d c b a ;②d c a b。
七、微任务和宏任务
异步任务分为宏任务和微任务,宏任务队列可以有多个,微任务队列只有一个。
宏任务包括:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering。微任务包括: new Promise().then(回调), process.nextTick, Object.observe(已废弃), MutationObserver(html5新特性)
宏任务和微任务的执行顺序
1、同步任务执行完毕时,会先执行微任务队列里的任务。
2、微任务执行完毕后,会读取宏任务队列中排在最前面的任务。
3、执行宏任务的过程中,遇到微任务,依次将其加入微任务队列。
4、栈空后,再次读取微任务队列里的任务。
5、微任务执行完毕后再去执行其他宏任务队列里的宏任务,依次类推。
Promise.resolve().then(()=>{ console.log('Promise1'); setTimeout(()=>{ console.log('setTimeout2') },0)});setTimeout(()=>{ console.log('setTimeout1'); Promise.resolve().then(()=>{ console.log('Promise2'); })},0);复制代码
执行结果:
解释:
1、执行栈的同步任务执行完毕后,会去查看是否有微任务,上题中有promise,所以先执行微任务中的所有任务,输出promise1,然后生成一个宏任务setTimeout2,将其放入另一个宏任务队列中。
2、然后去查看宏任务队列,宏任务setTimeout1在setTimeout2之前,所以先执行宏任务setTimeout1,输出 setTimeout1。
3、在宏任务setTimeout1执行的过程中,又会生成一个微任务promise2,将其放入微任务队列。这时setTimeout1所在的宏任务队列执行完毕。
4、执行微任务队列,输出promise2;然后再去执行另一个宏任务队列里的宏任务setTimeout2。
八、setTimeout()与setInterval()比较
1、setTimeout()
javscript引擎在执行setTimeout(fn,10)时,一方面继续执行setTimeout(fn,10)后面的同步代码,同时另一方面开始计时,在10ms后将fn插到任务队列中。待所有同步代码结束后,依次执行任务队列中的异步代码。所以setTimeout(fn,10)并不能准确的在10ms之后执行,而是大于等于10ms。
例1:
console.log(1);setTimeout(function () { console.log('a')}, 3);setTimeout(function () { console.log('b')}, 0);var sum = 0;for (var i = 0; i < 1000000; i ++) { sum += i;}console.log(sum);setTimeout(function () { console.log('c');}, 2);复制代码
执行结果:
例2:
console.log(1);setTimeout(function(){ console.log('a');},10);setTimeout(function(){ console.log('b');},0);var sum = 0;for(var i = 0; i < 100000; i++){ sum += i;}console.log(sum);setTimeout(function(){ console.log('c');},0);复制代码
总结:两段代码的区别在于for循环执行的时间不同,第一段代码for循环执行的时间是大于5ms的,所以console.log('a');先被插入任务队列,等for循环结束后console.log('c');才被插入任务队列。第二段代码的for循环执行时间小于10ms,所以console.log('c');先被插入任务队列。
2、setInterval()
setInterval()的执行方式和setTimeout()有不同。假如执行setInterval(fn,10),则每隔10ms,定时器的事件就会被触发。与setTimeout()相同的是,如果当前没有同步代码在执行(JavaScript引擎空闲),则定时器对应的方法fn会被立即执行,否则,fn就会被加入到任务队列中。由于定时器的事件是每隔10ms就触发一次,有可能某一次事件触发的时候,上一次事件的处理方法fn还没有机会得到执行,仍然在等待队列中,这个时候,这个新的定时器事件就被丢弃,继续开始下一次计时。需要注意的是,由于JavaScript引擎这种单线程异步的执行方式,有可能两次fn的实际执行时间间隔小于设定的时间间隔。比如上一个定时器事件的处理方法触发之后,等待了5ms才获得被执行的机会。而第二个定时器事件的处理方法被触发之后,马上就被执行了。那么这两者之间的时间间隔实际上只有5ms。因此,setInterval()并不适合实现精确的按固定间隔的调度操作。
例:
console.log(1);var interval = setInterval(function(){ var date = new Date(); console.log(date.getMinutes() + ":" + date.getSeconds() + ':' + date.getMilliseconds());},10);var sum = 0;for(var i = 0; i < 1000000; i++){ sum += i;}console.log(2);//清除定时器,避免卡死浏览器setTimeout(function(){ clearTimeout(interval);},100);复制代码
总结:setTimeout()和setInterval()都不能满足精确的时间间隔。假如设定的时间间隔为10ms,则setTimeout(fn, 10)中的fn执行的时间间隔可能大于10ms,而setInterval(fn, 10)中fn执行的时间间隔可能小于10ms。