不太正确的总结:
ES5
ES5事件轮询较为简单。
- 主线程执行栈在初次页面后首先会渲染页面;
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack);
- 异步任务有了运行结果,会通过在"事件队列"之中注册一个事件;
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行(setTimeOut事件还会检查定时器);
- 主线程不停重复以上三个步骤。
ES6
ES6中事件轮询有一些细微的差别(多了promise之后)。
注意点:
- 宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面
- 所有微任务也按顺序执行,且在以下场景会立即执行所有微任务
- 每个回调之后且js执行栈中为空。
- 每个宏任务结束后。
宏任务和微任务细节可以看这篇文章 Tasks, microtasks, queues and schedules,或者中文译文JS事件循环机制(event loop)之宏任务、微任务。英文原文中有执行动画,可以配合译文阅读。
以上总结不是很严谨。我查了一些资料想要弄清楚“宏任务”和“微任务”的概念是什么时候出现的,然而并没有找到。但是查找过程中发现了一些有趣的事情。
- 事件轮询(Event Loop)是Html标准,而不是JavaScript标准;
- 关于事件轮询详细标准是在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,重点)
一个事件循环存在时必须不停地执行以下步骤:
- 从event loop的一个事件队列中取出最先进入(原文oldest)的任务。如果有任务,则忽略以下操作。某些情况下,document还没有完全激活,那么用户代理可能会选择一个空的任务队列,如果是这样,跳转步骤6。
- 将event loop的当前运行任务设置为上步骤选中的任务。
- Run:执行选中的任务。
- 将当前运行任务设为null。
- 将该任务从任务队列中移除。
- Microtasks:执行微任务。详细见下方。
- 更新渲染。如果这个event loop是一个浏览器上下文event loop,那么执行以下子步骤:
- 更新Performance对象的now()方法的返回值为现在(now)
- Document B嵌套于Document A,那么列表中B必须在A后面。
- 如果A和B同时嵌套于C,那么A和B在列表中顺序必须满足他们在C中树顺序(tree order)。
- 如果一个顶层的浏览器上下文B在更新渲染中没有变化,那么会将他从document列表中移除。(操作没有影响到他,就移除他,防止渲染性能影响,比如在iframe中操作,不会触发父页面的重新渲染)
- 如果一个嵌套的浏览器上下文B在更新渲染中没有变化,那么会将他从document列表中移除
- 对于列表中每一个完全激活的页面,执行resize(执行resize相关事件)步骤,更新now时间
- 对于列表中每一个完全激活的页面,执行scroll(执行scroll相关事件)步骤,更新now时间
- 对于列表中每一个完全激活的页面,执行媒体查询和修改(执行media query和onchange,change相关事件)步骤,更新now时间
- 对于列表中每一个完全激活的页面,执行css动画并发送事件步骤,更新now时间
- 对于列表中每一个完全激活的页面,执行全屏渲染步骤,更新now时间
- 对于列表中每一个完全激活的页面,执行动画框架回调步骤,更新now时间
- 对于列表中每一个完全激活的页面,更新渲染或者用户交互
- 如果这是一个worker 事件循环,在事件循环中的任务队列没有任务而且WorkerGlobalScope 的关闭标识为true,那么销毁改事件循环,终止所有步骤,恢复到运行一个worker的步骤。
- 返回事件循环的第一步。
更新和当前事件循环相关Document对象列表,一般不需要排序,除了以下两种情况:
以下的步骤必须根据列表中Document的顺序重复执行
每个事件循环都有一个微任务队列(microtask queue),一个微任务是一个一开始就放在微任务队列的任务(不在任务队列中)。总共有两种微任务:单独的回调微任务(solitary callback microtasks)和复合微任务(compound microtasks)。
Microtasks处理(perform a microtask checkpoint):
- 将正在执行微任务标识(performing a microtask checkpoint flag)设为true
- 微任务队列处理:如果微任务队列为空,跳转到 完成 节点
- 选择 微任务队列最先进入(原文oldest)的微任务
- 事件循环的当前执行任务设置为上一步选择的微任务
- 执行:执行该微任务
- 将当前执行任务设为null
- 从微任务队列中移除该任务,返回到微任务队列处理步骤
- 完成:通知每一个受此事件循环的影响环境对象(environment settings object)他们的rejected promises
- 清除索引数据库事务
- 将正在执行微任务标识设为false
这可能会涉及到脚本回调,可能会再次执行微任务处理(perform a microtask checkpoint),所以使用“正在执行微任务标识“来避免函数重入。
任务源
- Dom操作任务源
- 用户交互任务源
- 网络任务源
- 历史返回操作任务源(调用
history.back()
等类似api)
总结
- 整体运行过程和其他文章没有太大差别,不过没有宏任务队列的概念,只有任务队列和微任务队列概念。
- 每执行完任务队列中一个任务,或者任务队列为空时,会立即执行微任务队列中所有任务。
- 英文真差(还不如机翻)。。。
原文地址:https://www.cnblogs.com/shaunyang/p/10342240.html