深入了解浏览器异步原理

  1. 什么是进程与线程(包括进程与线程的区别)
  2. 浏览器中的进程,各个进程的职责
  3. 浏览器中渲染进程的线程,各个线程的职责
  4. js单线程的执行机制
    1. js单线程存在的问题,解决方案
    2. js线程和GUI线程的关系
    3. js线程解析执行js时,与其他线程的关系 ***
    4. 宏任务与微任务

1. 什么是进程和线程

进程:

进程是系统中正在运行的一个程序,程序一旦运行就是进程。进程可以看成程序执行的一个实例,可以视作为浏览器打开了多个tab页,每个tab页相当于独立的进程。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。

一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。线程与进程的一个主要区别是,统一进程内的一个主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存(一个进程无法直接访问另一进程的内存)。同时,每个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。

线程:

线程是进程的一个实体,是进程的一条执行路径。

线程是进程的一个特定执行路径。当一个线程修改了进程的资源,它的兄弟线程可以立即看到这种变化。

线程和进程的区别:

1.地址空间和其他资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程内不可见。

2.通信:进程间通信IPC(管道,信号量,共享内存,消息队列),线程间可以直接独写进程数据段(如全局变量)来进程通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

3.调度和切换:线程上下文切换比进程上下文切换快得多。

4.在多线程OS中,进程不是一个可执行的实体。(看不懂,忽略不计)


2. 浏览器中的进程

1.浏览器进程(Browser进程)

浏览器的主进程,负责协调、主控,只有一个(无论打开几个tab或几个弹窗),主要作用:

  • 负责浏览器界面显示,与用户交互,如前进,后退等;
  • 负责各个页面的管理,创建和销毁其他进程;
  • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上;
  • 网络资源的管理,下载等;

2.CPU进程

用于3D绘制等,可禁用掉,只有一个。

3.第三方插件进程

每种类型的插件对应一个进程,仅当使用该插件时才创建。

4.浏览器渲染进程(浏览器内核)

浏览器渲染进程(render进程),即通常说的浏览器内核,主要作用是:页面渲染、脚本执行、事件处理等。每一个标签页的打开都会创建一个Render进程,并且互不影响。默认的话一个标签页对应一个Render进程,但是,有时候浏览器会将多个进程合并,如打开了多个空白标签页。

知识点:浏览器为什么是多进程的?

如果多个页面共用一个进程,在某个tab页崩溃时,将导致同进程中的其他页面崩溃,极其印象用户体验;而且进程之间是不会共享资源和地址空间的,所以不会存在太多的安全问题。当然,多进程相对于单进程而言,对内存等资源的消耗更大。


3.浏览器渲染进程中的线程

浏览器渲染进程(render进程,也成为浏览器内核),是我们前端开发人员最需要关注的。它主要的作用:页面渲染、脚本执行、事件处理等。它包含一下5种线程:

1.GUI渲染线程(有且只有一个)

  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
  • 当界面需要重绘(repain)时或者回流(reflow),该线程就会执行
  • 注意:GUI线程和JS引擎线程互斥!GUI线程和JS引擎线程互斥!GUI线程和JS引擎线程互斥!当JS引擎执行时,GUI线程就会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时,立即被执行。--- 这个地方会和后面连接起来叙述!!

小知识点:为什么JS引擎执行的时候GUI线程要被挂起?

因为JS是可以操作DOM的,而如果在修改这些元素的同时渲染界面,即当这两个线程不是互斥的时候,那么GUI渲染线程前后获得的元素数据就可能不一致。所以JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎线程空闲时立即被执行

2.JS引擎线程(有且只有一个)

  • JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序;
  • 注意:GUI线程和JS引擎线程互斥!GUI线程和JS引擎线程互斥!GUI线程和JS引擎线程互斥!所以当JS加载事件过长时,会造成页面渲染不连贯,导致阻塞页面加载。这就是为什么建议将<script>标签写在body的最末端。

3.事件触发线程

  • 事件触发线程归属于浏览器,而不是属于JS引擎,JS引擎处理的事务过多,需要浏览器另开线程来进行协助
  • JS是采用事件驱动(event-driven)机制,来响应用户操作的,事件触发线程通过维护事件循环和事件队列等的方式,来响应和处理事件
  • 当处理一些不能立即执行的函数或者其他的代码时,会将对应的任务在其可以触发的时机,添加到事件队列的末端
  • 事件循环机制会在JS引擎线程空闲时,循环访问事件队列的头部,如果有函数,则会将该函数推到执行栈中并立即执行。

4.定时器触发线程(多个)

  • setIntervalsetTimeout所在线程;
  • 浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确性;
  • 因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;
  • 注意:W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms。

5.异步http请求线程(多个)

  • XMLHttpRequest连接后通过浏览器新开一个线程请求;
  • 检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行;
浏览器进程和线程关系图

4.重新梳理一下线程之间的关系

1.GUI渲染引擎和JS执行引擎互斥,当GUI渲染引擎解析html时,发现了script标签时,会立即挂起解析html的任务,然后开始解析js代码,直到所有js代码执行完成后,继续执行html解析渲染。

2.JS是单线程的,所以在处理某些任务如请求服务端数据,定时任务等,自顾不暇,需要其他兄弟线程的协助。

3.JS是基于事件驱动的,其需要一种事件循环机制和事件队列的夹持,即需要事件触发线程的配合,和其他线程之间亲密合作。

为什么JS是单线程的:

JS执行引擎是单线程的,就是说,每一个浏览器内核(Render进程中),有且只有一个JS执行引擎(web worker另谈),为什么只能有一个?这和JS的用途有关,JS作为浏览器的脚本语言,主要用处是与用户进行交互,以及操作DOM的,如果JS是多线程,当在不同线程进行不同的DOM操作,会造成更复杂的一些问题,所以从一开始,JS就是单线程的。

什么是同步任务:

JS单线程就意味着,我的代码必须是从上到下,一行一行执行。一个代码块执行完毕之后,才能进行另一个代码块的执行,这就是同步执行代码。(定义:同步执行:代码从上到下,一行一行执行,下方代码块的执行必须是等待其前面代码全部执行完成之后才能执行,若代码执行中报错,则其下方代码不再执行

存在什么问题:

那么此时,我们想要从服务端获取某一些数据然后渲染到页面上,如果仍然是同步执行,我们需要等待数据全部加载完成之后,才能执行接下来的代码;如果这个数据的请求特别慢,那么页面就极其不友好。

如何解决:(异步回调)

异步回调是两个词,异步指的是,JS执行引擎会将某些任务挂起,交由他的兄弟线程进行处理,等到兄弟线程处理完成之后,JS执行引擎再回去执行其回调函数,他不会阻塞代码的执行,异步代码执行错误,不会影响其他同步代码的执行。而回调函数指的是,我先定了一个函数,但是目前我不想调用,当在某个事情发生或者到了某个特定的时间,我会去调用这个函数,这个就称为回调函数。

异步执行基于回调函数。异步执行基于回调函数。异步执行基于回调函数。

什么任务会触发异步:

  1. 网络请求,如ajax请求(交由异步http线程处理)
  2. 定时任务,setTimeout, setInterval(交友定时器线程)

回调函数的应用场景:

  1. 网络请求成功或者失败的回调函数
  2. 定时任务在到达指定事件后触发的函数
  3. 用户界面交互时发出的事件的处理函数

JS执行引擎,会召唤其兄弟线程来帮助他来进行一些操作,在这些操作执行完成之后,将对这些操作结果进行处理。如下图所示:

第一次理解:

Browser进程请求服务端,服务端返回了html资源

  1. GUI渲染线程对html进行解析
  2. 当解析到script标签时,停止GUI渲染线程,并将之后的任务,存在一个队列中(等待执行
  3. JS执行线程接管,解析并执行js代码,遇到下面三种情况,会请求兄弟线程协助
    1. 为dom元素,添加事件 => 通过事件触发线程,生成事件监听器(待确认)=> 监听器监听到了用户触发事件的行为之后,将回调函数,推入事件队列中
    2. 解析到setTimeout或者setInterval定时器代码时 => 通过定时器线程,开启定时任务 => 定时器时间到达之后,会将回调函数推入事件队列
    3. 遇到ajax请求 => 通过异步http请求线程,发送http请求 => 服务端返回响应后,会将成功或者失败的回调函数推入事件队列
  4. 当js执行线程中的代码执行完成之后,即同步执行的代码全部执行完成,(异步代码不会阻塞同步代码执行)。JS执行线程为空时,GUI渲染线程会重新接管,将剩下的等待执行的解析任务完成。
  5. 然后事件触发线程会开启事件循环机制,该机制会不断得访问事件队列的头部
  6. 如果事件队列的头部不为空,则将其取出,推入到JS执行引擎的执行栈中,并立即开始执行。然后从4-6再次循环(这个循环就叫做Event Loop),直至JS执行线程和事件队列中都为空时结束。

线程切换的顺序:

                        =>  事件触发线程 
GUI渲染线程 => JS执行线程  =>  异步http线程 => 推到事件队列(事件触发线程) => GUI渲染线程(如果仍有等待的任务) => JS执行线程
                        =>  定时器线程

事情走到这一步基本上是破案了,但是仍有一个前端面试经常碰到的问题,什么是宏任务,什么是微任务。

宏任务与微任务

概念一:宿主

JS运行的环境。一般为浏览器或者Node。

概念二宏任务和微任务

在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

ES6 规范中,microtask 称为 jobs,macrotask 称为 task
即 微任务是ES对异步的定义;而宏任务是浏览器对异步的定义。
jobs 包括 Promise, async/await
task 包括 script,定时任务,ajax请求,事件回调
宏任务是由宿主发起的,而微任务由JavaScript自身发起。

第二次理解:

  1. GUI渲染线程对html进行解析
  2. 当解析到script标签时,JS引擎解析拿到的js代码,解析script标签就是最先进入执行栈的共任务
  3. 同上,但是事件队列就被区分为宏任务事件队列task queue,微任务事件队列microtask queue
    1. 为dom元素,添加事件 => 通过事件触发线程,生成事件监听器(待确认)=> 监听器监听到了用户触发事件的行为之后,将回调函数,推入task queue中
    2. 解析到setTimeout或者setInterval定时器代码时 => 通过定时器线程,开启定时任务 => 定时器时间到达之后,会将回调函数推入task queue中
    3. 遇到ajax请求 => 通过异步http请求线程,发送http请求 => 服务端返回响应后,会将成功或者失败的回调函数推入task queue中
    4. 代码中使用了Promise或者async/await来处理异步 => 处理完成后 => 推入microtask queue中
  4. 当js执行线程中的代码执行完成之后,首先检查微任务队列头部是否有值,如果存在则将其推入到JS执行栈中执行,直到微任务队列头部为空。
  5. 如果宿主是浏览器,GUI渲染线程可能会重新渲染页面
  6. 然后检查宏任务队列头部是否有值,如果存在则将其推入到JS执行栈中执行,直到宏任务队列头部为空。
  7. 当JS执行线程中的执行栈为空时,事件轮询机制会一直重复4-6这个循环。

至此,完结。






  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值