美文网首页javascript
js Event Loop 运行机制

js Event Loop 运行机制

作者: 大毛哥的大哥 | 来源:发表于2019-03-26 13:11 被阅读0次

    js Event Loop 运行机制

    一个进程可以有多个线程,线程之间可以相互通信

    概念

    进程和线程基本概念
    拿出在教科书里的概念:
    1、调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
    2、并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
    3、拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源;
    4、系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
    进程和线程的关系:

    一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
    资源分配给进程,同一进程的所有线程共享该进程的所有资源;
    处理机分给线程,即真正在处理机上运行的是线程;
    线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体

    浏览器内核的线程

    浏览器引擎(进程)中包含哪些线程

    UI渲染线程
    负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行

    注意:UI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),UI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

    js引擎线程(JS解析线程)
    也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
    JS引擎线程负责解析Javascript脚本,运行代码。
    JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序

    同样注意:UI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

    事件触发线程
    归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理

    注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

    定时触发器线程
    传说中的setInterval与setTimeout所在线程
    浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)

    注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

    异步http请求线程
    在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

    js渲染引擎的Event Loop

    以上线程,每个拿出来都可以详细的说上一篇。Event Loop涉及到的JS引擎的一些运行机制的分析。我们可以将这些线程理解为,

    一个主进程就是js引擎,其他均为辅助的线程。
    主进程存在一个执行栈,事件触发线程维护一个消息队列
    同步任务在执行栈中执行,异步任务在满足条件后加入到消息队列中,等待执行。
    先执行栈中的任务,执行完毕后,检查队列是否为空,不为空,将队列中的任务压入执行栈中执行。直到栈和队列均为空。

    题目

    1

    setTimeout(function(){
    console.log(0)
    },500)
    setTimeout(function(){
    console.log(1)
    },1000)
    setTimeout(function(){
    console.log(2)
    },2000)

    for(;;){

    }
    复制代码上面这段代码用于不会有输出,同步代码死循环阻塞了执行栈。虽然定时后回调加入执行队列,但是异永远不会执行。

    2

    setTimeout(function(){
    console.log('setTimeout1');
    Promise.resolve().then(()=>{
    console.log('then1');
    });
    setTimeout(function(){
    console.log('setTimeout3');
    },0)
    },0)
    Promise.resolve().then(()=>{
    console.log('then2');
    Promise.resolve().then(()=>{
    console.log('then3');
    })
    setTimeout(function(){
    console.log('setTimeout2');
    },0)
    })
    答案:then2 then3 setTimeout1 then1 setTimeout2
    首先在题目中出现了es6的promise,他的出现让原来我们理解的事件环产生了一些不同
    为什么呢?因为Promise里有了一个一个新的概念:microtask
    此时JS中分为两种任务类型:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task

    微任务和宏任务

    首先说明,是以浏览器为处理环境下的执行逻辑
    浏览器环境下的微任务和宏任务有哪些
    宏任务:setTimeout setImmediate MessageChannel
    微任务:Promise.then MutationObserver
    记住两点:

    微任务在宏任务之前的执行,先执行 执行栈中的内容 执行后 清空微任务
    每次取一个宏任务 就去清空微任务,之后再去取宏任务

    然后题目入手分析宏任务和微任务的执行

    解题思路:
    setTimeout1放入宏任务执行队列中,微任务then2放入微任务队列中,栈为空,优先执行微任务,则先执行then2。
    then2之后执行后,接下来存在微任务then3。将then3放入微任务队列中。
    接下来setTimeout2加入到宏任务队列中。
    此时执行栈为空,执行then3。
    微任务全部执行完毕后,执行宏任务setTimeout1,执行发现微任务then1,放置到微任务队列中。
    setTimeout1宏任务执行完,再次清空微任务队列,执行then1
    微任务全部执行完毕后,执行宏任务setTimeout2。程序结束。

    那么node作为后端服务,单线程有什么利弊?

    优点:

    避免频繁创建、切换进程的开销,使执行速度更加迅速。
    资源占用小
    线程安全,不用担心同一变量同时被多个线程进行读写而造成的程序崩溃。

    缺点:
    不适合大量的计算和压缩等cpu密集型的操作,会造成阻塞。

    分析一下node下的消息队列

    为微任务,定时器,io,setImmidiate分别分配消息队列
    先检查定时器队列,如果有内容,则全部清空
    从时间队列切换到io队列的过程中,检查微任务,如果有则情况微任务。
    io队列执行完成,如果有check队列的内容,则执行。否则继续检查定时器队列。
    完成闭环
    setTimeout(() => {
    console.log('timeout1');
    Promise.resolve().then(() => {
    console.log('promise');
    });

    }, 0)
    setTimeout(() => {
    console.log('timeout2');
    }, 0)
    复制代码浏览器下的结果:timeout1 promise timeout2
    node下的结果:timout1 timeout2 promise

    简单梳理下浏览器渲染流程

    浏览器的渲染流程(简单版本):
    浏览器输入url,浏览器主进程接管,开一个下载线程,
    然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,
    随后将内容通过RendererHost接口转交给Renderer进程

    浏览器渲染流程开始

    渲染大概可以划分成以下几个步骤:
    解析html建立dom树
    解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
    布局render树(Layout/reflow),负责各元素尺寸、位置的计算
    绘制render树(paint),绘制页面像素信息
    浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。
    所有详细步骤都已经略去,渲染完毕后就是load事件了,之后就是自己的JS逻辑处理了

    染完毕后会触发load事件,那么你能分清楚load事件与DOMContentLoaded事件的先后么?
    很简单,知道它们的定义就可以了:
    当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。 (譬如如果有async加载的脚本就不一定完成)
    当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。 (渲染完毕了)
    所以,顺序是:DOMContentLoaded -> load

    css加载是否会阻塞dom树渲染?
    这里说的是头部引入css的情况
    首先,我们都知道:css是由单独的下载线程异步下载的。
    然后再说下几个现象:
    css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
    但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)
    这可能也是浏览器的一种优化机制。

    因为你加载css的时候,可能会修改下面DOM节点的样式, 如果css加载不阻塞render树渲染的话,那么当css加载完之后, render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。 所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后, 在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。

    JS分为同步任务和异步任务

    同步任务都在主线程上执行,形成一个执行栈
    主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
    一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

    相关文章

      网友评论

        本文标题:js Event Loop 运行机制

        本文链接:https://www.haomeiwen.com/subject/anjgvqtx.html