理解 JS 异步
同步和异步
同步: 调用之后得到结果, 再依次执行其他的任务
异步: 调用之后可以不等待结果, 继续做其他的事
众所周知, JavaScript 是单线程的, 代码只能一行一行通过 JS 引擎的主线程执行. 但是这种模式存在一个问题: 如果有一个任务耗时很长, 后面的任务都必须排队等着, 会导致整个程序卡在一个地方, 其他任务无法执行, 造成页面长时间无响应, 甚至卡死, 用户体验很糟糕.
如此,"异步" 模式就显得很重要了, 耗时很长的操作都使用异步执行, 避免浏览器失去响应, 让用户能够流畅的访问网页. 那么单线程的 JS 是怎么实现异步的呢?
JS 异步原理
JS 引擎本身是单线程的, 异步其实是借助浏览器内核多线程来实现的. 现代浏览器使用的都是多进程架构(如下图), 而一个进程可以包含一个或多个线程. JS 引擎处于渲染进程, 异步也是通过此进程中的各个线程的协调来完成的.
浏览器进程
GUI 线程: 主要用于渲染布局
JS 引擎线程: 用于解析, 执行 JS 代码; 它与 GUI 线程是互斥的, 因为 JS 里可以操作 DOM, 如果与 GUI 同时执行, 可能会引起页面渲染混乱
定时触发器线程: 用于执行定时任务, setTimeout,setInterval
事件触发线程: 将满足条件的事件加入任务队列
异步 HTTP 请求线程: XHR 所在线程, 用于处理 Ajax 请求
多线程之间的配合实现异步:
定时器, 异步请求线程可以独立于 JS 引擎主线程同时运行
定时器任务定时完成后, 会通知事件触发线程, 将定时器的回调任务加入任务队列
异步 HTTP 请求完成时, 如果有回调函数, 就会通知事件触发线程往任务队里添加事件
通过 Event Loop 机制, 浏览器依次执行完任务队列里的所有任务
理解 Event Loop 机制
JS 异步的实现, Event Loop 是关键的一步. Event Loop 其实是一个处理模型, 在不同的地方有不同的实现. 浏览器和 Node.JS 基于不同的技术实现了各自的 Event Loop. 这里我们主要来看看浏览器端的实现.
在 Event Loop 模型中, 我们将任务分为宏任务 (macrotask) 和微任务(microtask).
以下任务会加入宏任务队列:
script, 执行主线程中的 script, 是全局任务, 也可以看成是一个宏任务
- setTimeout/setInterval
- I/O
- UI rendering
以下任务会加入微任务队列:
- Promise
- Object.observe
- MutationObserver
postMessage, 主要用于 Windows 对象之间的通信
首先我们先来看看浏览器执行一个 JavaScript 代码的具体流程:
JS 代码执行流程
首先执行全局 script,script 可以包含同步任务和异步任务, 同步任务执行完就出栈, 异步任务则通过异步处理机制加入任务队列
当所有同步任务执行完成, 首先检查微任务队列, 一次执行完所有微任务
当所有微任务执行完以后, 从任务队列中取出最早的宏任务, 宏任务执行前, 再次检查微任务队列, 如果有新的微任务, 先清空微任务, 再执行宏任务, 执行完成后, 一次从队列中取出后面的宏任务, 重复此步骤, 直到执行完所有宏任务, Stack 清空
步骤 3, 可以解释为以下 Event Loop 模型:
Event Loop 处理模型
我们看一段示例代码, 进一步加深理解:
- console.log("1");
- setTimeout(function() {
- console.log("2");
- }, 0);
- Promise.resolve().then(function() {
- console.log("3");
- });
- console.log("4");
- console.log("start");
- setTimeout(() => {
- console.log("setTimeout");
- new Promise(resolve => {
- console.log("promise inner1");
- resolve();
- }).then(() => {
- console.log("promise then1");
- });
- }, 0);
- new Promise(resolve => {
- console.log("promise inner2");
- resolve();
- }).then(() => {
- console.log("promise then2");
- });
正确的打印结果如下:
- 1
- 4
- start
- promise inner2
- 3
- promise then2
- 2
- setTimeout
- promise inner1
- promise then1
我们来解析一下执行过程:
首先依次执行同步任务: console.log("1"),console.log("4"),console.log("start")
需要注意的是 console.log("promise inner2")是在 new Promise 定义中中声明的, 也是同步执行的, then 方法里面的才是异步任务
同步任务执行完, Stack 清空, 依次执行定义在外层的 Promise 微任务
外层微任务清空, 依次执行宏任务 setTimeout
参考链接:
浏览器进程? 线程? 傻傻分不清楚!
[译]官方图解: Chrome 快是有原因的, 现代浏览器的多进程架构!
Event Loops 标准
JS 中的栈内存堆内存
来源: http://www.jianshu.com/p/b95d47921ebb