关于JS单线程,异步执行的理解

前言

JS
在这几天的开发过程中,遇到了不少问题,阻塞最久的应该就是一段JS代码没有按照我以为的执行顺序去执行,想想好像也没有具体的了解JS的执行过程,所以花了一整天的时间来了解了一下JS的执行机制。

问题代码

for(var i=0;i<=3;i++){
    $(document).ready(function){
        console.log(i);
    });

出现问题的代码简化过后大概是这个样式,我的预期是输出1,2,3,但输出的结果却为3,3,3,我开始以为是JS单线程执行的缘故,所以Jquery代码被放在了callback queue(任务队列)的最后执行,但我查询资料后发现仅发现浏览器会为定时器,ajax等多开线程,jquery应该不算,正在我疑惑之时,我打开了Jquery的官方文档,看到了
$(document).ready(function(){});
的定义:文档准备完成后,内部的匿名函数作为document(ready)的回调函数进行执行,而此时for早已进行玩了。这就解释了为什么都是3,3,3

正文

从浏览器谈起

浏览器的进程与线程

浏览器是多进程的

  1. Browser进程:负责浏览器的主进程(协调,主控)。
  2. 第三方插件进程:每一个插件对应一个进程。
  3. GPU进程:用于绘制3D图形等。
  4. 浏览器渲染进程 (render进程,浏览器内核):每一个Tab页面一个进程,这个进程渲染我们所看到的每个页面。

浏览器渲染进程是多线程的

前面我们说到我们所看到的页面都是有渲染进程进行渲染的,而他又是多线程的:

  1. GUI渲染线程(浏览器内核)
  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
  • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
  • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  1. JS引擎线程
  • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
  • JS引擎线程负责解析Javascript脚本,运行代码。
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
  • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  1. 事件触发进程
  • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
  • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
  • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
  1. 定时触发线程
  • 传说中的setInterval与setTimeout所在线程
  • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
  • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
  • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  1. 异步http请求线程
  • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

线程数

进入正题,JS的执行过程

js的同步任务和异步任务
Pow2cQ.png

  1. 所有同步任务(能够立即被执行,不消耗时间的任务,如变量核函数的初始化,时间的绑定等等不需要回调函数的任务)都在主线程中执行,形成执行栈(stack),heap(堆)用来存储变量,对象等。
  2. 所有异步任务(一半会有操作,如点击事件,定时事件,具有回调函数的事件)运行在事件触发线程中,当异步任务有结果是,其回调函数(Callback Function)就会被放到任务队列中,等待执行。
  3. 当执行栈中同步任务执行完毕后, JS引擎就会从任务队列中(callback queue)中查找任务放入执行栈中,这个过程就被称之为事件循环event loop

从上面的解释中可以看到JS引擎始终是在执行栈中单线程执行任务,当执行栈空时,接下来的任务才回从任务队列中读取下一任务。

何为异步

所谓的异步,就是在其他线程(事件触发线程,异步http请求线程等)的辅助下,JS线程实现异步处理任务。举个栗子:

consloe.log("a")
setTimeout(function() {
console.log("b")
}, 0)
console.log("c")

模拟其运行过程:

1.consloe.log("a")// JS引擎发现这是一个同步任务,立即执行打印出a;
2.setTimeout(function() {
console.log("b")
}, 0)//JS引擎只想此处时发现这是一个异步任务,所以直接交由定时触发线程。
JS引擎继续向下执行,与此同时,定时触发线程接收到该事件,解析代码过后,
将在0毫秒实际是4毫秒,因为在上面我们讲到,W3C规定定时引擎的最低时间为4毫秒)后向JS引擎发送
回调函数,并将其推到任务队列中等待执行。
3.console.log("c")//再向定时引擎发送定时事件后立即执行该代码。打印出c。
4.定时引擎将console.log("b")推入任务队
列,在console.log("c")运行完成后,执行栈空,再将其推入执行栈,然后执console.log("c"),
打印出c,因此这段代码输出的结果为a,c,b,而不是a,b,c.

注意,即使setTimeout不是4毫秒后,而是0毫秒后返回回调函数,执行结果任然是a,c,b。因为这是一个排队的过程,console.log(“b”)任是排在console.log(“c”)之后的。

由此,一个异步任务就在JS引擎与其他线程的共同作用下完成了。

参考资料

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

js的单线程和异步

JavaScript单线程和异步机制

最后

这只是一个简单的理解,其中还有许多部分可能没有完善,等以后有更深的理解时再来完善吧。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :