JavaScript进阶之执行机制

Posted shaunyang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript进阶之执行机制相关的知识,希望对你有一定的参考价值。

不太正确的总结:

ES5

技术分享图片

ES5事件轮询较为简单。

  1. 主线程执行栈在初次页面后首先会渲染页面;
  2. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack);
  3. 异步任务有了运行结果,会通过在"事件队列"之中注册一个事件;
  4. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行(setTimeOut事件还会检查定时器);
  5. 主线程不停重复以上三个步骤。

ES6

ES6中事件轮询有一些细微的差别(多了promise之后)。

 技术分享图片

 

注意点:

  • 宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面
  • 所有微任务也按顺序执行,且在以下场景会立即执行所有微任务

    • 每个回调之后且js执行栈中为空。
    • 每个宏任务结束后。

 宏任务和微任务细节可以看这篇文章 Tasks, microtasks, queues and schedules,或者中文译文JS事件循环机制(event loop)之宏任务、微任务。英文原文中有执行动画,可以配合译文阅读。

 

以上总结不是很严谨。我查了一些资料想要弄清楚“宏任务”和“微任务”的概念是什么时候出现的,然而并没有找到。但是查找过程中发现了一些有趣的事情。

  1. 事件轮询(Event Loop)是html标准,而不是javascript标准;
  2. 关于事件轮询详细标准是在Html5中出现的;

event loop 的详细处理模型就在html标准中,看完就明白了。https://www.w3.org/TR/2018/WD-html53-20181018/webappapis.html#event-loops。这里做一个简单翻译。

译文:

定义

为了协调事件,用户交互,脚本,渲染,网络等,用户代理(user agents,可以理解为一个浏览器客户端)必须使用该节定义的事件循环(event loops)。总共有两种类型的event loops:浏览器上下文(browser context,理解为一个页面,如标签,窗口,包括frames,iframe)和workers。

一个用户代理至少有一个浏览器上下文,一个浏览器上下文必须包含一个event loop。浏览器上下文结束,那么event loop也跟着结束。

一个event loop有一个或多个任务队列( task queues,任务队列是一系列排好序的任务组成,这些任务有:

  • Events(事件),常见的事件
  • Pasring(解析),html解析
  • Callbacks(回调),回调函数
  • Using a resource(使用某个资源),以非阻塞方式获取资源(页面加载图片等)
  • Reacting to DOM manipulation(响应DOM操作),响应点击等DOM操作

当用户代理向任务队列添加一个任务时,同一个任务源的任务必须被添加进同一个任务队列中。比如一个用户代理有两个任务队列,第一个任务队列来处理鼠标和键盘事件(user interaction task source),第二个任务队列来处理其他事件,那么所有鼠标和键盘产生的事件就必须添加到第一个任务队列中。

每一个任务队列都有一个当前运行任务(currently running task),用来处理重入(reentrancy,函数重复调用),初始化为null。每个事件循环还有一个防止微任务重复调用的标志(performing a microtask checkpoint flag,正在执行微任务标识),初始化为false。

处理模型(Processing model,重点

一个事件循环存在时必须不停地执行以下步骤:

  1. 从event loop的一个事件队列中取出最先进入(原文oldest)的任务。如果有任务,则忽略以下操作。某些情况下,document还没有完全激活,那么用户代理可能会选择一个空的任务队列,如果是这样,跳转步骤6。
  2. 将event loop的当前运行任务设置为上步骤选中的任务
  3. Run:执行选中的任务。
  4. 当前运行任务设为null。
  5. 将该任务从任务队列中移除。
  6. Microtasks:执行微任务。详细见下方。
  7. 更新渲染。如果这个event loop是一个浏览器上下文event loop,那么执行以下子步骤:
    1. 更新Performance对象的now()方法的返回值为现在(now)
    2. 更新和当前事件循环相关Document对象列表,一般不需要排序,除了以下两种情况:
      • Document B嵌套于Document A,那么列表中B必须在A后面。
      • 如果A和B同时嵌套于C,那么A和B在列表中顺序必须满足他们在C中树顺序(tree order)。
      以下的步骤必须根据列表中Document的顺序重复执行 
    3. 如果一个顶层的浏览器上下文B在更新渲染中没有变化,那么会将他从document列表中移除。(操作没有影响到他,就移除他,防止渲染性能影响,比如在iframe中操作,不会触发父页面的重新渲染)
    4. 如果一个嵌套的浏览器上下文B在更新渲染中没有变化,那么会将他从document列表中移除
    5. 对于列表中每一个完全激活的页面,执行resize(执行resize相关事件)步骤,更新now时间
    6. 对于列表中每一个完全激活的页面,执行scroll(执行scroll相关事件)步骤,更新now时间
    7. 对于列表中每一个完全激活的页面,执行媒体查询和修改(执行media query和onchange,change相关事件)步骤,更新now时间
    8. 对于列表中每一个完全激活的页面,执行css动画并发送事件步骤,更新now时间
    9. 对于列表中每一个完全激活的页面,执行全屏渲染步骤,更新now时间
    10. 对于列表中每一个完全激活的页面,执行动画框架回调步骤,更新now时间
    11. 对于列表中每一个完全激活的页面,更新渲染或者用户交互
  8. 如果这是一个worker 事件循环,在事件循环中的任务队列没有任务而且WorkerGlobalScope 的关闭标识为true,那么销毁改事件循环,终止所有步骤,恢复到运行一个worker的步骤。
  9. 返回事件循环的第一步。

 

每个事件循环都有一个微任务队列(microtask queue),一个微任务是一个一开始就放在微任务队列的任务(不在任务队列中)。总共有两种微任务:单独的回调微任务(solitary callback microtasks)复合微任务(compound microtasks)。

Microtasks处理(perform a microtask checkpoint):

  1. 正在执行微任务标识performing a microtask checkpoint flag)设为true
  2. 微任务队列处理:如果微任务队列为空,跳转到 完成 节点
  3. 选择 微任务队列最先进入(原文oldest)的微任务
  4. 事件循环的当前执行任务设置为上一步选择的微任务
  5. 执行:执行该微任务
  6. 这可能会涉及到脚本回调,可能会再次执行微任务处理(perform a microtask checkpoint),所以使用“正在执行微任务标识“来避免函数重入。
  7. 当前执行任务设为null
  8. 从微任务队列中移除该任务,返回到微任务队列处理步骤
  9. 完成:通知每一个受此事件循环的影响环境对象(environment settings object)他们的rejected promises 
  10. 清除索引数据库事务
  11. 正在执行微任务标识设为false

任务源

  • Dom操作任务源
  • 用户交互任务源
  • 网络任务源
  • 历史返回操作任务源(调用history.back() 等类似api)

总结

  1. 整体运行过程和其他文章没有太大差别,不过没有宏任务队列的概念,只有任务队列和微任务队列概念。
  2. 每执行完任务队列中一个任务,或者任务队列为空时,会立即执行微任务队列中所有任务。
  3. 英文真差(还不如机翻)。。。

 



以上是关于JavaScript进阶之执行机制的主要内容,如果未能解决你的问题,请参考以下文章

web前端进阶知识之JavaScript内存机制讲解

javascript运行机制之执行顺序详解

javascript运行机制之执行顺序详解

我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印出C++的函数耗时以及代码片段耗时详情

JavaScript进阶之DOM

JavaScript进阶之DOM