本文已发布在西瓜君的个人博客, 原文传送门
前言
写这一篇的时候, 西瓜君查阅了很多资料和文章, 但是相当多的文章写的都很简单, 甚至互相之间有矛盾, 这让我很困扰; 同时也让我坚定了要写出一篇好的关于 JS 异步, 单线程, 事件循环的文章, 下面, 让我们一起来学习本文吧, 冲鸭~~
单线程
- ### 1. 什么是单线程
- // 栗子 1
- console.log(1)
- console.log(2)
- console.log(3)
- // 输出顺序 1 2 3
单线程即同一时间只做一件事
2. JavaScript 为什么是单线程
首先是历史原因, 在创建 JavaScript 这门语言时, 多进程多线程的架构并不流行, 硬件支持并不好.
其次是因为多线程的复杂性, 多线程操作需要加锁, 编码的复杂性会增高.
而且, 如果同时操作 DOM , 在多线程不加锁的情况下, 最终会导致 DOM 渲染的结果不可预期
为了利用多核 CPU 的计算能力, html5 提出 web Worker 标准, 允许 JavaScript 脚本创建多个线程, 但是子线程完全受主线程控制, 且不得操作 DOM. 所以, 这个新标准并没有改变 JavaScript 单线程的本质.
异步
1.JS 的 同步任务 / 异步任务
同步任务: 在主线程上排队执行的任务, 只有前一个任务执行完毕, 才能执行后一个任务
异步: 不进入主线程, 而进入 "任务队列"(task queue)的任务, 只有 "任务队列" 通知主线程, 某个异步任务可以执行了, 该任务才会进入主线程执行
- // 异步的栗子
- console.log(1)
- setTimeout(()=>{
- console.log(2)
- },100)
- console.log(3)
- // 输出顺序 1 3 2
2. JavaScript 为什么需要异步
如果在 JS 代码执行过程中, 某段代码执行过久, 后面的代码迟迟不能执行, 产生阻塞(即卡死), 会影响用户体验.
JavaScript 怎么实现异步
JS 实现异步时通过 事件循环(Event Loop), 下面我们来了解一下
1. 执行栈与任务队列
先理解几个概念
JS 任务 分为同步任务 (synchronous) 和异步任务(asynchronous)
同步任务都在 JS 引擎线程(主线程) 上执行, 形成一个执行栈(call stack)
事件触发线程 管理一个 任务队列(Task Queue)
异步任务 触发条件达成, 将 回调事件 放到任务队列 (Task Queue) 中
执行栈中所有同步任务执行完毕, 此时 JS 引擎线程空闲, 系统会读取任务队列, 将可运行的异步任务回调事件添加到执行栈中, 开始执行
当一个 JS 文件第一次执行的时候, JS 引擎会 解析这段代码, 并将其中的同步代码 按照执行顺序加入执行栈中, 然后从头开始执行. 如果当前执行的是一个方法, 那么 JS 会向执行栈中添加这个方法的执行环境, 然后进入这个执行环境继续执行其中的代码. 当这个执行环境中的代码 执行完毕并返回结果后, JS 会退出这个执行环境并把这个执行环境销毁, 回到上一个方法的执行环境. 这个过程反复进行, 直到执行栈中的代码全部执行完毕.
举个栗子:
- //Event loop
- //(1)
- console.log(1)
- //(2)
- setTimeout(()=>{
- console.log(2)
- },100)
- //(3)
- console.log(3)
先解析整段代码, 按照顺序加入到执行栈中, 从头开始执行
先执行(1), 是同步的, 所以直接打印 1
执行 (2), 发现是 setTimeout, 于是调用浏览器的方法(webApi) 执行, 在 100ms 后将 console.log(2) 加入到任务队列
执行(3), 同步的, 直接打印 3
执行栈已经清空了, 现在检查任务队列,(执行太快的话可能此时任务队列还是空的, 没到 100ms, 还没有将 (2) 的打印加到任务队列, 于是不停的检测, 直到队列中有任务), 发现有 console.log(2), 于是添加到执行栈, 执行 console.log(2), 同步代码, 直接打印 2 (如果这里是异步任务, 同样会再走一遍循环:-->任务队列 ->执行栈)
所以结果是 1 3 2
注: setTimeout/Promise 等我们称之为任务源. 而进入任务队列的是他们指定的回调
2. 宏任务 (macro task) 与微任务(micro task)
上面的循环只是一个宏观的表述, 实际上异步任务之间也是有不同的, 分为 宏任务(macro task) 与 微任务(micro task), 最新的标准中, 他们被称为 task 与 jobs
宏任务有哪些: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering(渲染)
微任务有哪些: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5 新特性)
下面我们再详细讲解一下执行过程
执行栈在执行的时候, 会把宏任务放在一个宏任务的任务队列, 把微任务放在一个微任务的任务队列, 在当前执行栈为空的时候, 主线程会 查看微任务队列是否有事件存在. 如果微任务队列不存在, 那么会去宏任务队列中 取出一个任务 加入当前执行栈; 如果微任务队列存在, 则会依次执行微任务队列中的所有任务, 直到微任务队列为空(同样, 是吧队列中的事件加到执行栈执行), 然后去宏任务队列中取出最前面的一个事件加入当前执行栈... 如此反复, 进入循环.
注:
宏任务和微任务的任务队列都可以有多个
当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个事件. 同一次事件循环中, 微任务永远在宏任务之前执行.
不同的运行环境 循环策略可能有不同, 这里探讨 Chrome,node 环境
举个栗子:
- //Event loop
- //(1)
- setTimeout(()=>{
- console.log(1)
- },100)
- //(2)
- setTimeout(()=>{
- console.log(2)
- },100)
- //(3)
- new Promise(function(resolve,reject){
- //(4)
- console.log(3)
- resolve(4)
- }).then(function(val){
- //(5)
- console.log(val);
- })
- //(6)
- new Promise(function(resolve,reject){
- //(7)
- console.log(5)
- resolve(6)
- }).then(function(val){
- //(8)
- console.log(val);
- })
- //(9)
- console.log(7)
- //(10)
- setTimeout(()=>{
- console.log(8)
- },50)
* 上面的代码在 node 和 Chrome 环境的正确打印顺序是 3 5 7 4 6 8 1 2
下面分析一下执行过程:
全部代码在解析后加入执行栈
执行(1), 宏任务, 调用 webapi setTimeout, 这个方法会在 100ms 后将回调函数放入宏任务的任务队列
执行 (2), 同(1), 但是会比(1) 稍后一点
执行 (3), 同步执行 new Promise, 然后执行(4), 直接打印 3 , 然后 resolve(4), 然后. then(), 把(5) 放入微任务的任务队列
执行 (6), 同上, 先打印 5 , 再执行 resolve(6), 然后. then() 里面的内容 (8) 加入到微任务的任务队列
执行(9), 同步代码, 直接打印 7
执行 (10), 同(1) 和(2), 只是时间更短, 会在 50ms 后将回调 console.log(8) 加入宏任务的任务队列
现在执行栈清空了, 开始检查微任务队列, 发现(5), 加入到执行栈执行, 是同步代码, 直接打印 4
任务队列又执行完了, 又检查微任务队列, 发现(8), 打印 6
任务队列又执行完了, 检查微任务队列, 没有任务, 再检查宏任务队列, 此时如果超过了 50ms 的话, 会发现 console.log(8) 在宏任务队列中, 于是执行 打印 8
依次打印 1 2
注: 因为渲染也是宏任务, 需要在一次执行栈执行完后才会执行渲染, 所以如果执行栈中同时有几个同步的改变同一个样式的代码, 在渲染时只会渲染最后一个
结语
写到这里, 仍然觉得还有很多知识点没有写出来, 但是想写又不知道从哪里入手. 于是决定今天就写到这里, 日后再做补充.
到这篇, JS 三座大山系列就暂时完结了, 在这其中自己也学到了很多, 希望能继续输出一些有意义的东西, 加油, 西瓜君~~
参考文章:
- https://www.jianshu.com/p/12b9f73c5a4f
- https://www.cnblogs.com/cangqinglang/p/8967268.html
如有错误, 请斧正
以上
来源: https://www.cnblogs.com/bloglixin/p/11959219.html