• [更新]单线程的JS引擎与 Event Loop


    先来思考一个问题,JS 是单线程的还是多线程的?如果是单线程,为什么JavaScript能让AJAX异步发送和回调请求,还有setTimeout也看起来像是多线程的?还有non-blocking IO, event loop等概念。


    目录:

    1. 单线程的 JS 引擎 
    2. 多线程的浏览器
    3. setTimeout(func, 0) 的应用场景
    4. setTimeout与setInterval
    5. [更新]参考资料
    6. [更新]深入:JS并发模型与 Event Loop

     


     

    1. 单线程的 JS 引擎

    浏览器无论在什么时候都只有一个线程在运行JavaScript程序。单线程,即在某个特定的时刻只有特定代码能执行,并阻塞其他代码。

    而浏览器是事件驱动的(event-driven),可以将事件看做浏览器派给JS引擎的任务,

    这些任务可能来自JS引擎当前正在执行的代码块,比如说,调用 setTimeout() 添加一个任务;

    或者是来自浏览器内核的其他线程,比如说,界面元素鼠标点击事件、定时器时间到达通知、异步请求状态变更通知(a mouse click, a timer firing, or an XMLHttpRequest completing), 

    从代码角度看,任务实体就是各种回调函数。因为JS引擎是单线程的,这些任务得排队(加入JS引擎的处理队列),等待被JS引擎执行。

    • 好处:

    如果多线程,那么删除或者创建dom元素,都需要在线程之间通信。

    所以,单线程简单,不需要考虑线程同步;没有线程切换维护开销,省内存。

     


    2. 多线程的浏览器

    浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。

    浏览器内核可能有如下线程:

    常驻线程:

    • 界面渲染线程
    • 事件响应线程

    执行完就终止的线程:

    • Http请求线程
    • 定时器线程 

    一张图说明单线程的JS引擎如何与其他线程通信:

    • JS引擎线程 与 界面渲染线程:互斥

    界面渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.
    JavaScript脚本可操纵DOM元素,如果在修改这些元素属性的同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了。
    在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被”冻结”了.
    所以,在脚本中对界面进行更新操作,如添加结点、删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.

    •  事件触发线程

    用户点击鼠标 -》 被浏览器的事件触发线程捕获 -》 形成一个鼠标点击事件添加到 JS引擎的处理队列 末尾

    • 定时触发线程

    定时计数器并不是由JS引擎计数的,因为JS引擎单线程,如果处于线程阻塞状态就记不了时,需要依赖外部计数器。

    定时器仅仅只是计划代码在未来的某个时间执行,而执行时机是不能保证的。

    它的工作方式是:指定一个时间间隔,表示何时将定时器的代码插入到 JS引擎的处理队列而不是何时执行代码。其等待的时间基于队列里正在等待的消息数量。

    • ajax 异步请求

    浏览器新开一个线程请求,当请求状态改变时,若已设置回调(即 onreadystatechange 中设置的回调函数),该异步线程则将状态变更事件放入 JS引擎的处理队列 末尾等待处理。

     


    3. setTimeout(func, 0) 的应用场景

    告诉js引擎,在0ms以后把func放到主事件队列中,等待当前的代码执行完毕再执行,注意:重点是改变了代码流程,把func的执行放到了等待当前的代码执行完毕再执行。

    • 让浏览器渲染当前的变化(很多浏览器UI render和js执行是放在一个线程中,线程阻塞会导致界面无法更新渲染)【待理解】
    • 重新评估”script is running too long”警告【待理解】
    • 改变执行顺序

    下例中,点击按钮就会显示"calculating....",如果删除setTimeout就不会。因为reDraw事件被进入事件队列,到长时间操作的最后才能被执行,所以无法刷新。

     1 <button id='do'> Do long calc!</button>
     2 <div id='status'></div>
     3 <div id='result'></div>
     4  
     5  
     6 $('#do').on('click', function(){
     7   
     8   $('#status').text('calculating....'); //此处会触发redraw事件的fired,但会放到队列里执行,直到long()执行完。
     9   
    10   // without set timeout, user will never see "calculating...."
    11   //long();//执行长时间任务,阻塞
    12    
    13   // with set timeout, works as expected
    14   setTimeout(long,50);//用定时器,大约50ms以后执行长时间任务,放入执行队列,但在redraw之后了,根据先进先出原则
    15   
    16  })
    17   
    18   
    19   
    20 function long(){
    21   var result = 0
    22   for (var i = 0; i<1000; i++){
    23     for (var j = 0; j<1000; j++){
    24       for (var k = 0; k<1000; k++){
    25         result = result + i+j+k
    26       }
    27     } 
    28   }
    29   $('#status').text('calclation done') // has to be in here for this example. or else it will ALWAYS run instantly. This is the same as passing it a callback 
    30 }
    View Code

    4. setTimeout与setInterval

    1 setTimeout(function() {
    2         /* 代码块... */
    3         setTimeout(arguments.callee, 10);
    4 }, 10);
    5 
    6 setInterval(function(){
    7         /*代码块... */
    8 }, 10);
    View Code

    这两段代码看一起效果一样,其实非也,第一段中回调函数内的 setTimeout 是 JS引擎 执行后再设置新的setTimeout定时,假定上一个回调处理完到下一个回调开始处理为一个时间间隔,理论上两个setTimeout回调的执行时间间隔>=10ms。第二段自 setInterval 设置定时后,定时触发线程就会源源不断的每隔十秒产生异步定时事件,并放到任务队列尾,理论上两个setInterval回调执行时间间隔<=10。


    5. 参考资料

    深入理解JavaScript定时机制

    Javascript是单线程的深入分析

    How JavaScript Timers Work

    JavaScript 高级程序设计(第三版) 22.3 高级定时器

    [更新]

    MDN:并发模型与Event Loop

    JavaScript 运行机制详解:再谈Event Loop

    【赞!必看】Philip Roberts的演讲《Help, I'm stuck in an event-loop


    6. 深入:JS并发模型与 Event Loop

    JS中的并发模型基于"Event Loop"。

    先来看一个理论上的模型:(下图来自MDN:并发模型与Event Loop

    • 堆(Heap):内存中一块大的、未被组织的空间,对象被创建在堆中。
    • 栈(Stack):函数调用的空间。栈结构具有的特点:先进后出,从栈顶压入、栈顶弹出,恰好可以对应函数的调用过程。换句话说,函数的调用、返回,即压栈、弹栈的过程。
    • 消息队列(Queue):也称任务队列/ JS引擎的处理队列...(随你咯)。

    每个消息都与一个函数关联。

    当栈为空时,从队列中取出一个消息进行处理。处理过程包含:调用与消息相关联的函数,创建一个初始栈结构。

    事件循环(Event Loop):(下图来自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)

    JS引擎运行时需要用到堆和栈。当栈为空时,读取消息队列,若有消息,即调用与消息相关联的函数,并初始化栈结构,执行这个函数过程中涉及到的函数调用都通过压栈弹栈来实现。而栈中代码又可能调用外部API(DOM事件,ajax,定时器),于是消息队列中又添加了新的消息,等待栈空时被JS引擎读取执行。于是构成了 Event Loop.

    借助 MDN:并发模型与Event Loop 中对 Event Loop 中的阐述,个人理解是

    1 while( (stack is Empty) && (queue.hasMessage) ) {
    2     queue.processNextMessage;
    3 } 

    事件“循环”这个名字很形象~

     

    那么,关于非阻塞I/O(non-blocking I/O),应该很清楚了,以ajax为例,通过外部API建立一个新连接,当相应状态变化时,触发状态变化事件,加入到 JS引擎 的消息队列末尾,等待执行(栈空,且队列前无其他消息等待执行)。在等待连接响应的期间,JS引擎正常、欢快的执行着其他代码呢。

     

  • 相关阅读:
    Java反射之访问私有属性或方法
    java字符串中显示双引号
    什么导致spring事务失效
    ActiveMq性能优化
    JFrame关闭事件处理
    c3p0数据库连接池死锁问题
    Mongodb性能优化
    Spring事务配置的五种方式
    ActiveMq启动停止
    JScrollPane动态加载图片
  • 原文地址:https://www.cnblogs.com/xhz-dalalala/p/5661955.html
Copyright © 2020-2023  润新知