从一个面试题牵出的关于JavaScript运行机制的学习

今天在偶然在网上看到一个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

原文地址:https://www.cnblogs.com/ruanraun/p/google.html

时间: 2024-08-03 10:33:11

从一个面试题牵出的关于JavaScript运行机制的学习的相关文章

从一个面试题开始学习C++类基础

前几天一个朋友参加了一个C++的面试,在面试后,他给我发了一个面试题,然后本屌用仅有的一点C++基础,开始了我的关于类的基础学习.题目大致是以下这样的:有一个类A,在main函数调用完之后,会打印哪些信息. 代码如下: class A { public: static int m_iCount; A(){++m_iCount;} ~A(){--m_iCount; print();} void print() { cout << "m_iCount = " <<

【源码】用1,2,2,3,4,5这六个数字,写一个函数,打印出所有不同的排序,要求:4不能放在第三位,3与5不能相连(C语言实现)

帮朋友做的,好像是一个面试题.暴力方式. #include <stdio.h> #include <stdlib.h> #include <string.h> //判断这个数是不是由1.2.2.3.4.5几位数字组成 int func(int n) { int a[5] = {0}; for(int i = 0; i < 6; i++) { int bit = n % 10; n /= 10; switch(bit) { case 1: a[0]++; break

上星期IOS的一个面试题。

美丽说面试题 1,IOS是怎样进行内存管理的,什么是ARC. 2,声明Property时,assign,nonatomic,readonly,retain,copy(各什么意思,括号里没打印出来,我猜得) 3,delegate需要retain吗? 4,什么是designated initlalizer?执行[super init]后会发生什么事? 5,isKindOfClass和isMemberOfClass有什么区别? 6,简述UIViewController的生命周期,比较重要的几个方法分别

ASPNET服务端控件练习(一个机试题)

简单记录: 模糊查询的select语句的拼写 public List<Model.Student> GetWhereStudent(string name, string sub, string isG) { List<Web.Model.Student> lt = new List<Model.Student>(); string sql = "select * from SC_Student where studentName like @n and [e

给出两个单词(start和end)与一个字典,找出从start到end的最短转换序列

问题 给出两个单词(start和end)与一个字典,找出从start到end的最短转换序列.规则如下: 一次只能改变一个字母 中间单词必须在字典里存在 例如: 给出 start = "hit"end = "cog"dict = ["hot","dot","dog","lot","log"] 返回 [ ["hit","hot",&

一个文件中有40亿个整数,每个整数为四个字节,内存为1GB,写出一个算法:求出这个文件里的整数里不包含的一个整数

4个字节表示的整数,总共只有2^32约等于4G个可能.为了简单起见,可以假设都是无符号整数.分配500MB内存,每一bit代表一个整数,刚好可以表示完4个字节的整数,初始值为0.基本思想每读入一个数,就把它对应的bit位置为1,处理完40G个数后,对500M的内存遍历,找出一个bit为0的位,输出对应的整数就是未出现的.算法流程:1)分配500MB内存buf,初始化为02)unsigned int x=0x1;  for each int j in file  buf=buf|x<<j;  e

设计一个函数,找出整型数组元素的最大值

/* 设计一个函数,找出整型数组元素的最大值 */ #include <stdio.h> int maxOfArray(int array[], int length) { // 数组当做函数参数传递时,会当做指针变量来使用,指针变量在64bit编译器环境下,占据8个字节 //int size = sizeof(array); //printf("array=%d\n", size); //sizeof(array); // 1.定义一个变量存储最大值(默认就是首元素) i

一个输入流同时写出到多个输出流

在有一部分传输的时候,为了效率我们需要将一个输入流中的数据异步写出到多个输出流里面,Java 自带的IO/NIO并没有这个功能,自己实现的具体流程图如下: 可以想到,如果达到这个效果,我们需要将输入流中的读出的byte[] 保存到内存中,之后再由输出流读取,如果希望输入流输出流互不干涉完全异步,那么我们就需要每个输出流一个线程来实现,输入流不停地向缓冲区队列里面写数据,不用管输出流怎么读,直到所有数据读完. 输入流在第一次读数据之后输出流开始启动,输出流监控这个缓冲队列,如果有数据就读取,没有数

android如何使用DOM来解析XML+如果做一个表情的弹出框

效果图: 如何解析以下的xml: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <array> <string>(#大笑)</string