⚡ 30 秒速记
- 当前任务里的同步代码先完整运行
- 异步等待由宿主推进,回调再回到执行线程
- 检查点清空微任务,新增微任务也会继续处理
- 渲染只在合适机会发生,不保证每任务一帧
- 持续长任务或微任务都会影响响应
单线程不等于所有事情都要干等,而是同一执行线程一次只运行一段脚本。 网络和定时等待由宿主处理,完成后再安排回调。当前任务结束等检查点会清空微任务,随后才有机会继续其他任务和渲染。把计算塞进承诺回调并不会自动移到别的线程,持续追加微任务同样可能让页面无法响应,真正的并行计算需要额外执行环境。
版本校准: 原理文章中的代码代表特定实现与写作时间。应用到当前项目时,应先确认浏览器、框架或工具的主版本,再区分稳定的规范语义、可变化的内部实现和项目自身约束。
要怎么理解 JavaScript 是单线程这个概念呢?大概需要从浏览器来说起。
JavaScript 最初被设计为浏览器脚本语言,主要用途包括对页面的操作、与浏览器的交互、与用户的交互、页面逻辑处理等。如果将 JavaScript 设计为多线程,那当多个线程同时对同一个 DOM 节点进行操作时,线程间的同步问题会变得很复杂。
因此,为了避免复杂性,JavaScript 被设计为单线程。
这样一个单线程的 JavaScript,意味着任务需要一个接一个地处理。如果有一个任务是等待用户输入,那在用户进行操作前,所有其他任务都处于等待状态,页面会进入假死状态,用户体验会很糟糕。
那么,为了高效进行页面的交互和渲染处理,我们围绕着任务执行是否阻塞 JavaScript 主线程,将 JavaScript 中的任务分为同步任务和异步任务
# 同步任务与异步任务
- 同步任务:在主线程上排队执行的任务,前一个任务完整地执行完成后,后一个任务才会被执行
- 异步任务:不会阻塞主线程,在其任务执行完成之后,会再根据一定的规则去执行相关的回调。
我们先来看一下同步任务在浏览器中的是怎样执行的。
# 同步任务与函数调用栈
JavaScript 在执行过程中每进入一个不同的运行环境时,都会创建一个相应的执行上下文。那么,当我们执行一段 JavaScript 代码时,通常会创建多个执行上下文。
而 JavaScript 解释器会以栈的方式管理这些执行上下文、以及函数之间的调用关系,形成函数调用栈(call stack)(调用栈可理解为一个存储函数调用的栈结构,遵循 FILO(先进后出)的原则)
我们来看一下 JavaScript 中代码执行的过程:
- 首先进入全局环境,全局执行上下文被创建并添加进栈中;
- 每调用一个函数,该函数执行上下文会被添加进调用栈,并开始执行;
- 如果正在调用栈中执行的 A 函数还调用了 B 函数,那么 B 函数也将会被添加进调用栈;
- 一旦 B 函数被调用,便会立即执行;
- 当前函数执行完毕后,JavaScript 解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。