一, JS 为什么是单线程的
Javascript 语言的一大特点就是单线程, 同一时间只能做同一件事, 那么为什么 JS 不能多线程呢?
作为浏览器脚本语言, Javascript 的主要用途是与用户互动, 以及操作 DOM, 这决定了它只能是单线程, 否则会带来很复杂的同步问题. 比如: 假定 Javascript 同时有两个线程, 一个线程在某个 DOM 节点上添加内容, 另一个线程删除了找个节点, 这时浏览器应该以哪一个为准?
所以, 为了避免复杂性, 从一诞生, Javascript 就是单线程, 这已经成为了这门语言的核心特征, 将来也不会改变.
为了利用多核 CPU 的计算能力, html5 提出 web Woker 标准, 允许 Javascript 脚本创建多个线程, 但是子线程完全受主线程控制, 且不能操作 DOM. 所以, 这个新标准并没有改变 JavaScript 单线程的本质.
二, 同步和异步
假设存在一个函数 A:
A(args...){...}
同步: 如果在调用函数 A 的时候, 调用者立即能够得到预期的结果, 那么这个函数就是同步的.
异步: 如果在调用函数 A 的时候, 调用者无法立即得到预期的结果, 而是需要在将来通过一定的手段 (耗时, 延时, 事件触发) 得到, 那么这个函数就是异步的.
三, JS 是如何实现异步操作的
虽然 JS 是单线程的但是浏览器的内核是多线程的, 浏览器为一些耗时任务开辟了另外的线程, 不同的异步操作会由不同的浏览器内核模块调度执行, 例如 onlcik,setTimeout,ajax 处理的方式都不同, 分别是由浏览器内核中的 DOM Bingding,network,timer 模块执行, 当执行的任务得到运行结果时, 会将对应的回调函数放到任务队列中. 所以说, JS 是一直都是单线程的, 实现异步操作的其实是浏览器.
在上图中, 调用栈中遇到 DOM 请求, ajax 请求以及 setTimeout 等 WebAPIs 的时候就会交给浏览器内核的其他模块进行处理, webkit 内核在 Javascript 执行引擎之外, 有一个重要的模块是 webcoew 模块. 对于图中 WebAPIs 提到的三种 API,webcore 分别提供了 DOM Binding,network,timer 模块来处理底层实现. 等到这些模块处理完这些操作的时候将回调函数放入任务队列中, 之后等栈中的 task 执行完之后再去执行任务队列中的回调函数.
小结:
1. 所有的代码都要通过函数调用栈中调用执行
2. 当遇到前文中提到的 APIs 的时候, 会交给浏览器内核的其他模块进行处理
3. 任务队列中存放的是回调函数
4. 等到调用栈中的 task 执行完之后再回去执行任务队列中的 task
JS 的运行机制如下:
(1)所有的同步任务都在主线程上执行, 形成一个执行栈.
(2)主线程之外, 还存在一个 "任务队列". 只要异步任务有了运算结果, 就在 "任务队列" 之中放置一个事件(回调函数).
(3)一旦 "执行栈" 中的所有同步任务执行完毕, 系统就会读取 "任务队列", 看看里面有哪些事件. 那些对应的异步任务, 于是结束等待状态. 进入执行栈, 开始执行.
(4)主线程不断重复上面的第三步
任务队列
上文中一直都有提到任务队列, 那么任务队列到底是个什么东东呢? 举个
在 ES6 的标准中, 任务队列分为宏任务 (macro-task) 和微任务(micro-task)
1.macro-task 包括: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
2.micro-task 包括: process.nextTick, Promises, Object.observe, MutationObserver
事件循环的顺序是从 script 开始第一次循环, 随后全局上下文进入函数调用栈, 碰到 macro-task 就将其交给处理它的模块处理完之后将回调函数放进 macro-task 的队列之中, 碰到 micro-task 也是将其回调函数放进 micro-task 的队列之中. 直到函数调用栈清空只剩全局执行上下文, 然后开始执行所有的 micro-task. 当所有可执行的 micro-task 执行完毕之后. 循环再次执行 macro-task 中的一个任务队列, 执行完之后再执行所有的 micro-task, 就这样一直循环.
接下来我们分析一下执行过程:
- (function test() {
- setTimeout(function() {console.log(4)}, 0);
- new Promise(function executor(resolve) {
- console.log(1);
- for( var i=0 ; i<10000 ; i++ ) {
- i == 9999 && resolve();
- }
- console.log(2);
- }).then(function() {
- console.log(5);
- });
- console.log(3);
- })()
1. 全局上下文入栈, 开始执行其中的代码.
2. 执行到 setTimeout, 作为一个 macro-task 交给 timer 模块处理完成后将其回调函数放入自己的队列中.
3. 执行到 Promise 实例, 将 Promise 入栈, 第一个参数是在当前任务直接执行输出 1.
4. 执行循环体, 遇到 resolve 函数, 入栈执行后出栈, 改变 promise 状态为 Fulfilled, 随后输出 2
5. 遇到 then 方法, 作为 micro-task, 进入 Promise 的任务队列.
6. 继续执行代码, 输出 3.
7. 输出 3 之后第一个宏任务代码执行完毕, 开始执行所有在队列之中的微任务. then 的回调函数入栈执行后出栈, 输出 5.
8. 这时候所有的 micao-task 执行完毕, 第一轮循环结束. 第二轮循环从 setTimeout 的任务队列开始, setTimeout 的回调函数入栈执行完毕之后出栈, 此时输出 4.
小结:
不同的任务会放进不同的任务队列之中.
先执行 macro-task, 等到函数调用栈清空之后再执行所有在队列之中的 micro-task.
等到所有 micro-task 执行完之后再从 macro-task 中的一个任务队列开始执行, 就这样一直循环.
当有多个 macro-task(micro-task)队列时, 事件循环的顺序是按上文 macro-task(micro-task)的分类中书写的顺序执行的.
总结:
JS 引擎在解析 JS 代码的时候, 会创建一个主线程 (main thread) 和一个调用堆栈(call-stack), 在对一个调用堆栈中的 task 处理的时候, 其他的都要等着.
当执行到一些异步操作的时候, 会交给浏览器内核的其他模块处理 (以 webkit 为例, 是 webcore 模块), 处理完成将 task(回调函数) 放入任务队列中.
一般不同的异步任务的回调函数都会放入不同的任务队列中, 等到调用栈中所有的 task 执行完毕, 接着去执行任务队列中的 task(回调函数)
来源: https://www.cnblogs.com/chao-insist/p/9433774.html