今天在偶然在网上看到一个JavaScript的面试题,尝试着看了一下,很正常的就做错了,然后给我们前端做,哈哈,他居然也顺理成章做的错了,代码大概是这样的
/*1 下面代码会怎样执行?执行结果是什么*/
var i=true; setTimeout('stoploops()', 2000); loops(); function loops(){ alert('no hello world!'); while(i){ } } function stoploops(){ i=false; alert('hello world!'); }
粗略的看下上面的代码,你的第一感觉是浏览器在网页打开两秒后执行stoploops函数,将i的值变成false随后弹出hello world!,然后执行loops函数弹出no hello world!,由于i的值为false所以不会进入死循环,所以代码执行结束,但代码的实际运行情况并不是这样。
经过本人查阅资料仔细一研究后,发现事情并不简单,这个问题牵连出了JavaScript代码的在浏览器中的运行机制,而setTimeout和setInterval函数也算是两个彻头彻尾的“大骗子”,而相关关于介绍setTimeout() 的文档也是:方法用于在指定的毫秒数后调用函数或计算表达式。
但是在很多情况下setTimeout和setInterval函数调用的函数并不一定会在参数指定的时间内执行,比如上面的代码;上面的代码最终运行结果是:浏览器在网页打开的第一时间会先执行loops()函数,随即弹出no hello world!弹窗,然后由于i=true,所以会无限循环,直到停电,永远都不会执行stoploops函数。
如果你是普通撸码玩家,对于这种现实也许有点无法接受,因为常规的代码都是顺序执行的,这有点违背常理的味道在里面,但是事实就是这样,代码会按照我所说的第二种方式执行。
而且弄懂为什么会这样不安常规的执行,对于深入学习和理解JS都是有很大必要的。
下面就来揭开这个问题的神秘面纱,其执行顺序错位其本质就是由于:JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.
当然理解这句话的意思你首先得知道什么是线程:线程是程序执行流的最小单元。关于线程与进程相关细节的东西也比较复杂,一句话也说不清楚,当然在此处为了理解后面的内容,你可以简单的把线程理解为一个小型“cpu”,它在同一时间内只能做一件事情,就好比假如一个人,在同一时间内,他在吃饭的时候,就开启了一个负责吃饭的线程,这个线程就只能吃饭,不能同时做其他的事情,如果他想在吃饭的时候“边吃饭边看电视”,就要启用另一个线程,一个线程负责吃饭,一个线程负责看电视(实际的这两个线程执行是交替执行,并不是并行,所以边吃饭边看电视会有引号)。这么说我想你就能理解上面那句:JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序. 简单的说就是一个浏览器(浏览器就是一个进程)负责执行JS代码的线程只有一个,而setTimeout和setInterval使用了障眼法,制造了一种假象,好像用他俩执行的函数是单独启用了新的线程来执行的,其实并不会有新的线程被启用,从语言的设计角度来讲,JavaScript单线程运行时有它的优点的,JavaScript作为浏览器脚本语言,其主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题(想想要是真有一天,JS变成了多线程,要是把复杂线程冲突和数据同步问题都丢给JS,丢给前端,他会说我他M只是个画界面的,what fuck?Greet your family for me)。
所以JavaScript把代码任务分成了两种,一种是同步任务(synchronous)的代码,一种是异步任务(asynchronous)的代码(异步任务代码对应着一个事件队列,用于控制异步代码的执行顺序),JavaScript引擎会优先执行同步任务的代码,等所有同步任务的代码执行完成后,才会去顺序执行异步代码,如果在同步代码执行过程中出现CPU计算不过来,会消耗较长时间,那也是先等同步代码执行完成后,才会按顺序执行异步代码,我们一般常规书写的代码都是同步任务代码,而setTimeout和setInterval中执行的代码即为异步任务代码。所以现在回看上面的题目,由于loops函数是同步任务代码,JavaScript引擎会先执行loops函数,但是loops函数有一个死循环,所以JavaScript引擎线程会长期处于阻塞状态,无法执行后续的代码。。。终于真相大白!
扩展:
1,通过上面的学习,如果以后在遇到同步任务相对耗时时,我们可以将耗时的代码放到setTimeout中去执行,这样JavaScript引擎就会优先的去执行可快速执行的同步任务代码,现将可以渲染的部分先渲染出来,使用户体验更为良好
2,JavaScript引擎是单线程运行的!=整个网页都是单线程的,浏览器内核实现允许多个线程异步执行,除了javascript引擎线程外,还会有界面渲染线程,浏览器事件触发线程,相关请求线程等,
界面渲染线程:该线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行,该线程与JavaScript引擎线程是互斥的,所以就不难理解为什么当JavaScript引擎线程被阻塞后界面渲染线程就渲染HTML元素,导致界面一片空白了
事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
定时触发线程:浏览器模型定时计数器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的,如果处于阻塞线程状态就计不了时,它必须依赖外部来计时并触发定时
3,既然JS是单线程语言,XMLHttpRequest对象实现AJAX异步请求是如何做到的?
其实请求确实是异步的,不过这请求是由浏览器新开一个线程请求,当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行 onreadystatechange所设置的函数,但是如在执行AJAX异步请求之前同步任务中有可以阻塞JavaScript引擎线程代码,界面已久会卡死,无法进行其他操作,直到同步任务执行完成,解除阻塞,界面才可以做其他事情
参考文章:
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
http://www.iamued.com/qianduan/1645.html