JavaScript执行机制、单线程与并发

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014465934/article/details/89458581

1.先看一些结论

JavaScript 是以单线程的方式运行的。

进程和线程是不同的操作系统管理资源的方式,进程有自己独立的内存空间,线程没有独立地址空间,但是有自己堆栈和局部变量,线程是进程的一个实体,是CPU任务调度和分配的基本单位,线程不能独立执行,必须依存在进程之中,同一进程的多个线程共享同一内存空间,进程退出时该进程的所有线程会被清空。

作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。若以多线程的方式操作这些 DOM,则可能出现操作的冲突。当然,我们可以为浏览器引入“锁”的机制来解决这些冲突,但这会大大提高复杂性,所以 JavaScript 从诞生开始就选择了单线程执行。

另外,因为 JavaScript 是单线程的,在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行。那么对于类似 I/O 等耗时的任务,就没必要等待他们执行完后才继续后面的操作。在这些任务完成前,JavaScript 完全可以往下执行其他操作,当这些耗时的任务完成后则以回调的方式执行相应处理。这些就是 JavaScript 与生俱来的特性:异步与回调。

同步和异步

同步异步是指程序的行为。

同步(Synchronous)是程序发出调用的时候,一直等待直到返回结果,没有结果之前不会返回。也就是说,同步是调用者主动等待调用过程。

异步(Asynchronous)是发出调用之后,马上返回,但是不会马上返回结果。调用者不必主动等待,当被调用者得到结果之后会主动通知调用者。

阻塞和非阻塞

阻塞与非阻塞是指等待状态。

阻塞(Blocking)是指调用在等待的过程中线程被“挂起”(CPU资源被分配到其他地方去了)。

非阻塞(Non-blocking)是指等待的过程CPU资源还在该线程中,线程还能做其他的事情。

所以,同步可以阻塞也可以非阻塞,异步可以阻塞也可以非阻塞。

Web Worker

当然对于不可避免的耗时操作(如:繁重的运算,多重循环),HTML5 提出了Web Worker,它会在当前 JavaScript 的执行主线程中利用 Worker 类新开辟一个额外的线程来加载和运行特定的 JavaScript 文件,这个新的线程和 JavaScript 的主线程之间并不会互相影响和阻塞执行,而且在 Web Worker 中提供了这个新线程和 JavaScript 主线程之间数据交换的接口:postMessage 和 onMessage 事件。但在 HTML5 Web Worker 中是不能操作 DOM 的,任何需要操作 DOM 的任务都需要委托给 JavaScript 主线程来执行,所以虽然引入 HTML5 Web Worker,但仍然没有改线 JavaScript 单线程的本质。

单线程的JavaScript和浏览器的多线程

浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:
1.javascript引擎线程 javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。

2.GUI渲染线程 GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

3.浏览器事件触发线程 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。(当线程中没有执行任何同步代码的前提下才会执行异步代码)

在 Chrome 浏览器中,为了防止因一个标签页奔溃而影响整个浏览器,其每个标签页都是一个进程(Renderer Process)。当然,对于同一域名下的标签页是能够相互通讯的,具体可看 浏览器跨标签通讯(http://web.jobbole.com/82225/)。在 Chrome 设计中存在很多的进程,并利用进程间通讯来完成它们之间的同步,因此这也是 Chrome 快速的法宝之一。对于 Ajax 的请求也需要特殊线程来执行,当需要发送一个 Ajax 请求时,浏览器会开辟一个新的线程来执行 HTTP 的请求,它并不会阻塞 JavaScript 线程的执行,当 HTTP 请求状态变更时,相应事件会被作为回调放入到“任务队列”中等待被执行。

参考文章:https://leohxj.gitbooks.io/front-end-database/content/theory/single-thread.html

JS引擎

通常讲到浏览器的时候,我们会说到两个引擎:渲染引擎和JS引擎。

渲染引擎就是如何渲染页面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不同的引擎对同一个样式的实现不一致,就导致了经常被人诟病的浏览器样式兼容性问题。这里我们不做具体讨论。

JS引擎可以说是JS虚拟机,负责JS代码的解析和执行。通常包括以下几个步骤:
1.词法分析:将源代码分解为有意义的分词
2.语法分析:用语法分析器将分词解析成语法树
3.代码生成:生成机器能运行的代码
4.代码执行

不同浏览器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。

并发模型与事件循环(Event Loop)

JavaScript 有个基于“Event Loop”并发的模型。 JavaScript 是单线程,但是并发与并行是有区别的。 前者是逻辑上的同时发生,而后者是物理上的同时发生。所以,单核处理器也能实现并发。

并发和并行:
在这里插入图片描述
所谓“并发”是指两个或两个以上的事件在同一时间间隔(例如上面横坐标这个时间段)中发生。如上图的第一个表,由于计算机系统只有一个 CPU,故 ABC 三个程序从“微观”上是交替使用 CPU,但交替时间很短,用户察觉不到,形成了“宏观”意义上的并发操作。

定时器setTime

在到达指定时间时,定时器就会将相应回调函数插入“任务队列”尾部。这就是“定时器(timer)”功能。

定时器 包括 setTimeout 与 setInterval 两个方法。它们的第二个参数是指定其回调函数推迟\每隔多少毫秒数后执行。

对于第二个参数有以下需要注意的地方:
1.当第二个参数缺省时,默认为 0;
2.当指定的值小于 4 毫秒,则增加到 4ms(4ms 是 HTML5 标准指定的,对于 2010 年及之前的浏览器则是 10ms);

深入了解定时器:零延迟 setTimeout(func, 0)

零延迟并不是意味着回调函数立刻执行。它取决于主线程当前是否空闲与“任务队列”里其前面正在等待的任务。指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。

看一段代码:

//代码1
console.log('先执行这里');
setTimeout(() => {
    console.log('执行啦')
},0);

//输出结果
//先执行这里
//执行啦

//代码2
console.log('先执行这里');
setTimeout(() => {
    console.log('执行啦')
},3000);
//输出结果
//先执行这里
// ... 3s later
// 执行啦

看看以下代码:

(function () {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('this is a msg from callback');
  });

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('this is a msg from callback1');
  }, 0);

  console.log('this is the end');

})();

this is the start
this is just a message
this is the end
undefined
this is a msg from callback
this is a msg from callback1

//另一个例子
(function () {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('this is a msg from callback');
  },10);

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('this is a msg from callback1');
  }, 0);

  console.log('this is the end');

})();
this is the start
this is just a message
this is the end
undefined
this is a msg from callback1
this is a msg from callback

setTimeout(func, 0) 的作用:
1.让浏览器渲染当前的元素更改(浏览器将 UI render 和 JavaScript 的执行是放在一个线程中,线程阻塞会导致界面无法更新渲染)
2.重新评估“scriptis running too long”警告
3.改变执行顺序

JavaScript执行机制

先看一段代码:

setTimeout(function(){
    console.log('定时器开始啦')
});

new Promise(function(resolve){
    console.log('马上执行for循环啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('执行then函数啦')
});

console.log('代码执行结束');

//输出结果
马上执行for循环啦
代码执行结束
执行then函数啦
undefined
定时器开始啦

JavaScript事件循环

JavaScript任务:
1.同步任务
2.异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

在这里插入图片描述
导图要表达的内容用文字来表述的话:
1.同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
2.当指定的事情完成时,Event Table会将这个函数移入Event Queue。
3.主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
4.上述过程会不断重复,也就是常说的Event Loop(事件循环)。

我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

不如直接一段代码更直白:

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');

上面是一段简易的ajax请求代码:
1.ajax进入Event Table,注册回调函数success。
2.执行console.log(‘代码执行结束’)。
3.ajax事件完成,回调函数success进入Event Queue。
4.主线程从Event Queue读取回调函数success并执行。

再看一个实例:

setTimeout(() => {
    task()
},3000)

sleep(10000000)

这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的:
1.task()进入Event Table并注册,计时开始。
2.执行sleep函数,很慢,非常慢,计时仍在继续。
3.3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
4.sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

Promise与process.nextTick(callback)

我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:
1.macro-task(宏任务):包括整体代码script,setTimeout,setInterval
2.micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:

setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');

1.这段代码作为宏任务,进入主线程。
2.先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
3.接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
遇到console.log(),立即执行。
4.好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
5.ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
6.结束。

事件循环,宏任务,微任务的关系如图所示:

在这里插入图片描述

看一个复杂点实例:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

第一轮事件循环流程分析如下:
1.整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
2.遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
3.遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
4.遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
5.又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
在这里插入图片描述
6.上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
7.我们发现了process1和then1两个微任务。
8.执行process1,输出6。
9执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。

那么第二轮时间循环从setTimeout1宏任务开始:
1.首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
在这里插入图片描述
2.第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
3.输出3。
4.输出5。

第二轮事件循环结束,第二轮输出2,4,3,5。

第三轮事件循环开始,此时只剩setTimeout2了,执行。
1.直接输出9。
2.将process.nextTick()分发到微任务Event Queue中。记为process3。
3.直接执行new Promise,输出11。
4.将then分发到微任务Event Queue中,记为then3。

在这里插入图片描述
4.第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
5.输出10。
6.输出12。

第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

还可以对比参照这幅图:http://lynnelv.github.io/img/article/event-loop/browser-excute-animate.gif

Ajax异步请求是否真的异步?

很多童鞋搞不清楚,既然说JavaScript是单线程运行的,那么XMLHttpRequest在连接后是否真的异步?

其实请求确实是异步的,这请求是由浏览器新开一个线程请求(见前面的浏览器多线程)。当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到 JavaScript引擎的事件处理队列中等待处理。当浏览器空闲的时候出队列任务被处理,JavaScript引擎始终是单线程运行回调函数。javascript引擎确实是单线程处理它的任务队列,能理解成就是普通函数和回调函数构成的队列。

总结一下,Ajax请求确实是异步的,这请求是由浏览器新开一个线程请求,事件回调的时候是放入Event loop单线程事件队列等候处理。

参考文章:https://www.cnblogs.com/Mainz/p/3552717.html

非阻塞js的实现(non-blocking javascript)

要实现非阻塞js(non-blocking javascript)有两个方法:1. html5 2. 动态加载js

首先一种办法是HTML5的defer和async关键字:

//defer
<script type="text/javascript" defer src="foo.js"></script>

//async
<script type="text/javascript" async src="foo.js"></script>

然后第二种方法是动态加载js:

setTimeout(function(){
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "foo.js";
    var head = true; //加在头还是尾
    if(head)
      document.getElementsByTagName("head")[0].appendChild(script);
    else
      document.body.appendChild(script); 
}, 0);
//另外一个独立的动态加载js的函数
function loadJs(jsurl, head, callback){
    var script=document.createElement('script');
    script.setAttribute("type","text/javascript");
     
    if(callback){
        if (script.readyState){  //IE
            script.onreadystatechange = function(){
                if (script.readyState == "loaded" ||
                        script.readyState == "complete"){
                    script.onreadystatechange = null;
                    callback();
                }
            };
        } else {  //Others
            script.onload = function(){
                callback();
            };
        }
    }
    script.setAttribute("src", jsurl);
     
    if(head)
     document.getElementsByTagName('head')[0].appendChild(script); 
    else
      document.body.appendChild(script); 
 
}

最后

我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

事件循环是js实现异步的一种方法,也是js的执行机制。

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

参考文章:
https://github.com/JChehe/blog/blob/master/posts/关于JavaScript单线程的一些事.md
https://juejin.im/post/59e85eebf265da430d571f89#heading-10
https://zhuanlan.zhihu.com/p/27035708
https://imweb.io/topic/58e3bfa845e5c13468f567d5
http://www.cnblogs.com/whitewolf/p/javascript-single-thread-and-browser-event-loop.html
http://www.cnblogs.com/hustskyking/p/javascript-asynchronous-programming.html
https://blog.csdn.net/lin_credible/article/details/40143961
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
https://segmentfault.com/a/1190000004322358
https://imweb.io/topic/56642e21d91952db73b41f52
https://imweb.io/topic/5b6cf97093759a0e51c917c8
http://www.cnblogs.com/zhaodongyu/p/3922961.html
http://www.cnblogs.com/onepixel/p/7143769.html

猜你喜欢

转载自blog.csdn.net/u014465934/article/details/89458581