js异步梳理:1.从浏览器的多进程到JS的单线程,理解JS运行机制
大家很早就知道JS是一门单线程的语言。但是也时不时的会看到进程这个词。首先简单区分下线程和进程的概念
1. 简单理解进程
- 进程是一个工厂,工厂有它的独立资源
- 工厂之间相互独立
- 线程是工厂中的工人,多个工人协作完成任务
- 工厂内有一个或多个工人
- 工人之间共享空间
2. 简单理解线程
- 工厂的资源 -> 系统分配的内存(独立的一块内存)
- 工厂之间的相互独立 -> 进程之间相互独立
- 多个工人协作完成任务 -> 多个线程在进程中协作完成任务
- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
- 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
3. 浏览器是多进程的
上面的1.1和1.2可能还是有些抽象。接下来用与前端息息相关的浏览器为例展开。
当你打开浏览器开了好几个网页的时候,打开浏览器的任务管理器(比如谷歌浏览器-> 更多工具 -> 任务管理器)
这里就是查看进程的地方,而且可以看到每个进程的cpu占用率和内存资源信息。
简单用比较官方的术语总结下:
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 不同进程之间也可以通信。(比如网页是一个进程,qq是一个进程,在网页上使用快捷方式qq登录。网页怎么会知道你当前有没有登录qq的?这之间就涉及到了不同进程之间的通信)
- 一般讨论的单线程和多线程,都只是指在一个进程内的单和多。
4 浏览器是如何渲染进程的?与JS的单线程有什么联系?
在浏览器中打开一个网页相当于新起了一个进程,每个进程内又会有自己的多线程(当然,浏览器有自身的优化机制,当你开了很多空的标签页的时候,可能会发现多个空白标签页被合并成了一个进程)。比如页面的渲染,JS的执行,事件的循环,都会在这个进程内进行。(以下用比较官方的术语列举一些主要常驻线程)
扩散思考1:浏览器为什么要弄成多进程的?
优点:
- 避免单个标签页崩溃影响整个浏览器
- 避免第三方插件崩溃影响整个浏览器
- 多进程充分利用多核优势
- 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
缺点:
- 会占用更多的内存
4.1. GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行(扩展阅读:页面重绘和回流以及优化)
- 注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(想当与被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
.a {
width: 100px;
height: 100px;
background: #f60;
}
</style>
<script>
console.time('js执行')
for(var i = 0; i < 1000000000; i++) {
}
console.timeEnd('js执行')
</script>
</head>
<body>
<div class="a">a</div>
</body>
</html>
从这个例子中可以看到JS页面明显有一段空白期,也就证明了上面所说的当JS引擎执行时GUI线程会被挂起。
扩展思考:你可能以前听说并且一直是这么做的,JS调用不放在中,要放到网页底部前面来优化你的网站。但是修改这个例子可能会发现无论你是将这段script包含的代码放到head里还是body里,或者是另外新建一个文件引入,都要等到js加载并且执行才会在页面里渲染出a。尤其是jquery时代大家统一会将代码写在$(document).ready中,那样的话JS不管在顶部引入还是在底部引入,这样看起来它们的执行时机对页面的影响是一样的,那JS调用放在顶部和底部真的会有差别吗?
推荐阅读:网站为什么 JS 调用尽量放到网页底部?
4.2. JS引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)。
- JS引擎线程负责解析Javascript脚本,运行代码
- JS引擎椅子和等待着任务队列中任务的到来,然后加以处理,一个标签页中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程和JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
4.3. 事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其它线程,如鼠标点击、Ajax异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被出发时,该线程会把事件添加到待处理队列的末尾,等待JS引擎的处理
- 注意,由于JS的单线程的关系,所以这些处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
4.4 定时触发器线程
- setInterval 与 setTimeout所在的线程
- 浏览器定时计数器并不是由JavasScript引擎计数的。(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML表中中规定,规定要求setTimeout低于4ms的时间间隔算为4ms。
4.5 异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
看了上面的描述后,思考两个问题。
a. 平时前端写的事件,定时器,异步我们都会把它称为JS。那为什么这里把JS引擎线程单独拿出来讲?我们平时说的JS和这里的JS引擎有什么区别?
JS引擎包含两个部分
内存堆(Memory Heap): 和内存分配有关。(比如基本类型值存栈内存里,引用类型值存堆内存里)
调用栈(Call Stack): 代码执行时候的栈帧 (你可能看到过一些执行栈,执行上下文堆栈,函数调用栈这样的词,其实没必要太过咬文嚼字。简单理解就是每当一个函数被调用的时候,都会为这个函数创建一个新的上下文。而在一个javascript程序中,必定会有多个执行上下文。javascript以栈(先进后出,后进先出)的方式来处理它们。而调用栈就像一个高速摄影机,会把当前运行的代码的每一帧都给记录下来。)
推荐阅读:js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?
而日常开发中真实的JS运行环境可能包含更多的内容,比如DOM操作(onload, onclick...), Ajax, setTimeout等等。这些是宿主环境(浏览器)提供的Web API。而WebAPI本身是不能把执行代码放到调用栈中执行的,每个Web Api在执行完成以后会把回调放到事件队列中。而Event Loop(事件轮询机制)就是检查执行栈和任务队列,如果执行栈已经为空了,就会将事件队列中的第一个回调函数放到栈中执行。
b. 单从前端开发来讲,除了上面说的dom操作,定时器,ajax。还有哪些你觉得是异步操作的?
promise, Generator, async
看一段代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
setTimeout(function() {
console.log(1);
})
new Promise(function(resolve) {
console.log(2);
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
})
console.log(5);
</script>
</body>
</html>
打印顺序是 2 3 5 4 1。这里的promise它的执行顺序又是怎么定的
这里扯出来另一个概念,宏任务和微任务。前面事件轮询机制(Event-Loop)中说到任务队列,一些Web Api 产生的回调函数在条件达到的时候会被加到任务队列中。而任务队列又分为宏任务(macro-task)和微任务(micro-task)。最新的标准中,它们分别被称为task 和jobs。
常见的macro-task大概包括:script(整体代码), setTimeout,setInterval,setImmediate, I/O, UI rendering
常见的micro-task大概包括:process.nextTick, Promise, MutationObserver(html5新特性)
setTimeout/promise这些我们都称之为任务源,而进入任务队列的是它们指定的具体执行任务。比如setTimeout的第一个参数回调函数才是进入任务队列的任务。
不同任务源的任务会进入到不同的任务队列,其中,setTimeout和setInterval是同源的
事件循环的顺序,决定了Javascript代码的执行顺序。它从script(整体代码)开始第一次循环。然后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的微任务。当所有可执行的微任务执行完毕之后。循环再去从宏任务去找看看还有没有其它的宏任务队列,如果有的话就开始第二轮。
上面这段代码的执行顺序就是:
- 事件循环从宏任务队列开始,宏任务队列中只有一个script(整体代码)任务。全局上下文入栈
- script宏任务执行时,首先遇到了 setTimeout, 就会在宏任务中添加一个setTimout队列。
- script执行时遇到Promise实例。Promise构造函数的第一个参数,是在new的时候执行,不会进入到任何其它的队列。而是直接在当前任务直接执行了。所以先打印2
- 再往下for循环也不会进入其它队列,所以继续打印2
- 接下来到then了。promise的 .then 会被分发到 微任务的 Promise队列中去
- script继续往下执行。打印5。到此,全局任务就执行完了。
- 第一个宏任务script执行完了之后,就开始执行所有的可执行的微任务。这时候,微任务中,只有一个promise队列的任务console.log(4)。就打印了4
- 当所有的微任务执行完了之后,表示第一轮的循环就结束了。这时候继续第二轮的循环。第二轮的循环依然从宏任务开始,它就找到了 setTimout队列中还要一个 console.log(1) 的任务要执行。所以就打印了1。这时候发现宏任务队列和微任务队列中都没有任务了,所以代码就不会再输出其它东西了。
js异步梳理:1.从浏览器的多进程到JS的单线程,理解JS运行机制的更多相关文章
- 对JS闭包和函数作用域的问题的深入讨论,如何理解JS闭包和函数作用域链?
首先先引用<JavaScript权威指南>里面的一句话来开始我的博客:函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的. 因此,就出现了如下的几串代码: ...
- [JS]异步任务之事件循环
前言 常常会听到单线程和多线程这两个名词,单线程即一个时间段内程序从上到下执行任务,多线程即一个时间段内程序同时执行多个任务. 然而 JavaScript 是单线程的,它不像 Java 那样新开启一个 ...
- 如何更好的理解js中的this,分享2段有意思的代码
关于js中this的浅析,大家可以点击[彻底理解js中this的指向,不必硬背]这篇博客了解. 今天遇到2段比较有意思的代码. ----------------第一段----------------- ...
- 【学习笔记】深入理解js原型和闭包(0)——目录
文章转载:https://www.cnblogs.com/wangfupeng1988/p/4001284.html 说明: 本篇文章一共16篇章,外加两篇后补的和一篇自己后来添加的学习笔记,一共19 ...
- 从浏览器多进程到JS单线程,JS运行机制的一次系统梳理
前言 见解有限,如有描述不当之处,请帮忙及时指出,如有错误,会及时修正. ----------超长文+多图预警,需要花费不少时间.---------- 如果看完本文后,还对进程线程傻傻分不清,不清楚浏 ...
- 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
前言 来源:https://dailc.github.io/2018/01/21/js_singlethread_eventloop.html 见解有限,如有描述不当之处,请帮忙及时指出,如有错误,会 ...
- JS魔法堂:深究JS异步编程模型
前言 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...
- 深入理解JS异步编程五(脚本异步加载)
异步脚本加载 阻塞性脚本 JavaScript在浏览器中被解析和执行时具有阻塞的特性,也就是说,当JavaScript代码执行时,页面的解析.渲染以及其他资源的下载都要停下来等待脚本执行完毕 浏览器是 ...
- 纯js异步无刷新请求(只支持IE)
纯js异步无刷新请求 下载地址:http://pan.baidu.com/s/1slakL1F 所以因为非IE浏览器都禁止跨域请求,所以以只支持IE. <HTML> <!-- 乱码( ...
随机推荐
- 设置 Confluence 6 日志
Confluence 使用的是 Apache's log4j 日志服务.能够允许管理员通过编辑配置文件来控制日志的表现和日志输出文件.在系统中有 6 个日志输出级别,请参考 log4j logging ...
- Confluence 6 配置字符集编码
Confluence 和你的数据库必须配置使用相同的字符集.为了避免字符出现问题,请将所有的字符集设置为使用 UTF-8 编码(或者根据你配置的数据库来制定正确的 UTF-8 编码字符集,例如在 Or ...
- js 获取当前的网址
http://www.xcx.cc/index.php/home/index/ind?idf=12321var $cur_url=window.location.href; //获取全部的网址var ...
- python面试1-30题
1.一行代码实现1--100之和 利用sum()函数求和 2.如何在一个函数内部修改全局变量 利用global 修改全局变量 3.列出5个python标准库 os:提供了不少与操作系统相关联的函数 s ...
- ionic3 隐藏子页面tabs
看了几天ionic3 问题还挺多的,今天想把所有子页面tabs 给去掉,整了半天,发现app.Module 是可以配置的 修改 IonicModule.forRoot(MyApp) imports: ...
- poj2411 状态压缩-铺地板题型-轮廓线解法(最优)
解法参考博客https://blog.csdn.net/u013480600/article/details/19569291 一种做法是先打出所有的状态,即满足上下配对的所有可能方案,然后再逐行进行 ...
- getComputedStyle()用法详解
那如果元素即没有在style属性中设置宽高,也没有在样式表中设置宽高,还能用getComputedStyle或currentStyle获取吗?答案是getComputedStyle可以,current ...
- mongoDB通过_id删除doc
转载: 点击查看原文 做项目遇到一个需求,需要根据mongodb数据记录里面的_id字段删除相应的docs,实际使用时发现直接如下使用 db.collection('infochanges').rem ...
- this容易混淆的示例
[注]this 永远不会混乱,混乱的是我们而已. /* this永远指向当前函数的主人. this混乱: 1.添加了定时器/延时器 2.事件绑定 [注]函数如果发生了赋值,this就混乱了. */ 示 ...
- easyUI-datagrid带有工具栏和分页器的数据网格
<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>数据 ...