• 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 {
    			 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

    Vue.nextTick的原理和用途

    为什么setState是异步的

    看一段代码:

    <!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(整体代码)开始第一次循环。然后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的微任务。当所有可执行的微任务执行完毕之后。循环再去从宏任务去找看看还有没有其它的宏任务队列,如果有的话就开始第二轮。

    上面这段代码的执行顺序就是:

    1. 事件循环从宏任务队列开始,宏任务队列中只有一个script(整体代码)任务。全局上下文入栈
    2. script宏任务执行时,首先遇到了 setTimeout, 就会在宏任务中添加一个setTimout队列。
    3. script执行时遇到Promise实例。Promise构造函数的第一个参数,是在new的时候执行,不会进入到任何其它的队列。而是直接在当前任务直接执行了。所以先打印2
    4. 再往下for循环也不会进入其它队列,所以继续打印2
    5. 接下来到then了。promise的 .then 会被分发到 微任务的 Promise队列中去
    6. script继续往下执行。打印5。到此,全局任务就执行完了。
    7. 第一个宏任务script执行完了之后,就开始执行所有的可执行的微任务。这时候,微任务中,只有一个promise队列的任务console.log(4)。就打印了4
    8. 当所有的微任务执行完了之后,表示第一轮的循环就结束了。这时候继续第二轮的循环。第二轮的循环依然从宏任务开始,它就找到了 setTimout队列中还要一个 console.log(1) 的任务要执行。所以就打印了1。这时候发现宏任务队列和微任务队列中都没有任务了,所以代码就不会再输出其它东西了。
  • 相关阅读:
    http请求消息体和响应消息体
    整型常量
    C语言中字符串后面的'\0'
    String类
    二进制转成十六进制
    http消息头
    NULL和NUL
    拷贝构造函数和赋值表达式
    awk中的FS
    之前给女性网增加的一个滚动展示
  • 原文地址:https://www.cnblogs.com/hezhi/p/10484884.html
Copyright © 2020-2023  润新知