前言
感觉知识就像网贷, 是个无底洞啊, 本来只是在犀牛书上看到定时器的内容, 只有一页而已, 然而我却花了几周的时间来整理它, 不过真的是学无止境, 还有很多细节无法深入, 大家一起学习进步吖~
简单的栗子
例 1:
- setTimeout(() => {
- console.log('hello world')
- }, 0)
- function printStr(str) {
- console.log(str)
- }
- printStr('hello Melody')
例 2:
- let startTime = Date.now();
- setTimeout(() => {
- console.log(Date.now()-startTime)
- }, 100)
- for (let i = 0; i <1000000000; i++) {}
这两个问题绝大部分人都能答的上来, 不过答案的扩展性极强, 我想面试官问你这个问题也只是以此为引子想看看你的基础是否够扎实.
最最基本也要知道以上两个问题的答案, 由此延伸其内部原理, 包括但不限于:
js 引擎为什么是单线程的?
什么叫阻塞? 异步是如何解决阻塞问题的?
线程和进程的区别
浏览器进程
什么是任务队列? 什么是执行栈?
事件循坏(Event Loop)
Microtasks 和 Macrotasks
......
js 引擎为什么是单线程的?
其实关于 js 引擎为什么是单线程的 这个问题我觉得就像 地球为什么是圆的花儿为什么这样红 一样无聊, 但是我还是问了这个无聊的问题,(我就是这么无聊(*︶))js 诞生之初本就没指望他干什么大事, 它不走 C 和 Java 的 "高端" 路线, 一开始就找准定位 -- 脚本语言, 作为脚本语言, 它不需要很快的速度, 很强的性能, 所以本着简单易学的原则, js 被设计成单线程语言.
还有一个说法是, js 会操作 dom, 而 dom 的修改会触发浏览器的渲染进程去渲染界面, 如果 js 是多线程的, 当它同时对同一个节点做了增加和删除操作, 渲染进程就不知道该怎么渲染这个节点了. 关于这个原因其实我觉得比较牵强, Python 不就是多线程的, 你们咋不说它也会造成这个问题哪?
所以单线程就单线程, 它也不见得就比多线程差多少啊.
什么叫阻塞? 异步是如何解决阻塞问题的?
js 里面的阻塞就是成群结队的函数啊网络请求啊排着队等 js 执行, 执行就得花时间, 某个任务花的时间长了, 占用 js 的时间多了, 就导致后面的任务等待执行的时间很长, 比较坏的情况是, js 引擎线程和 GUI 渲染线程是互斥的, 如果 js 久久执行不完, 就会导致窗口一直白屏, 甚至浏览器认为该窗口失去响应, 会询问用户是否要关闭该窗口.
阻塞不是单线程的专利, 事实上无论是单线程 or 多线程, 当并发量过高时都会造成阻塞, 只是单线程更容易阻塞, 不需要高并发量, 只需要来一个耗时的 I/O 读取操作就可以让线程无法继续往下处理, 只能干等着. 所以对于这类耗时的操作, 没必要等待他们执行完成, 只需要告诉他们: 我还有事要先去忙没空等你, 你结果返回了再来通知我, 我自会回去处理, 这就是异步和回调.
因为异步的用处实在太多, 一不小心就是异步接异步, 回调套回调, 然后就陷入了 "回调地狱", 不过近几年 js 的回调已经可以处理得非常优雅了, 包括有:
Promise 对象 http://es6.ruanyifeng.com/#docs/promise
Generator 函数 http://es6.ruanyifeng.com/#docs/generator
async 函数 http://es6.ruanyifeng.com/#docs/async (Generator 函数的语法糖, 用起来确实非常顺手)
线程和进程
我们说: xxx 语言是单 (多) 线程的, 浏览器是多进程的
我们说: 电脑好卡啊, 打开任务管理器看看哪些进程占用 CPU 过多
所以: 进程和线程到底是个啥?
官方解释进程是 cpu 资源分配的最小单位, 线程是 cpu 调度的最小单位
不懂, 有没有通俗一点的解释?
CPU 是计算机的 "大脑","身体" 的各个机能运转都依靠于 "大脑" 处理, 我们的大脑永远在工作, 除非我们挂掉了, CPU 也会一直运作, 除非切断了电源. 但是不管 CPU 有多忙, 它一次只能运行一个任务, 或者说: 它在一个时间段内只会运行一个进程.
比如说这一时刻我开启了一个浏览器窗口, 并且正在用键盘输入文字, 那么 CPU 要管理的进程就有浏览器和键盘, 当然除此之外显卡啊 RAM 啊等各种资源的进程也都是一直在运行的. 总之不管有多少个进程 (任务) 要执行, 都只能排着队等待 CPU 的 "临幸", 还有一点是, CPU"临幸" 某个进程的时间并不是根据这个进程执行完需要多久来确定的, 而是由 CPU 自己分配, 如果 CPU 分配的时间用完了, 那么 CPU 就会存储有关这个进程的执行环境, 我们称之为 "执行上下文", 等 CPU 下一次回来继续执行该进程时, 实际要做的:
加载执行上下文
执行进程
如果 CPU 分配时间用完, 跳到 4, 如果进程结束, 跳到 5
保存进程的执行上下文, 等待下一次 CPU 处理
回收该进程占用资源, 包括其执行上下文.
好了, 现在再来理解进程是 cpu 资源分配的最小单位是不是容易多了? 计算机运转的过程实际就是 CPU 在操控各个进程运转的过程, CPU 资源有限, 能容纳的进程数量有限, CPU 能力有限, 它一次始终只能运行一个进程.
而进程还很 "大", 为了将进程细分, 就引入了线程的概念, 这就像一段程序代码由函数和全局变量组成, 进程也是由很多个线程组成的, 这些线程共享进程的资源, 也就是 cpu 为该进程分配的上下文环境. 线程是 cpu 调度的最小单位这句话的通俗解释是 cpu 将自己的资源给进程, 但真正使用这些资源的是线程.
浏览器进程
浏览器是多进程的, 它包括:
浏览器进程(主进程): 管理浏览器的前进后退, 与用户交互, 同时负责处理所有和磁盘, 网络的通信, 不分析和渲染任何网页内容.
渲染进程(浏览器内核): 渲染进程是多线程的, 它负责解析 HTML,CSS 构建 DOM 树, 负责解析 js 和事件触发等等. 要注意的是它对磁盘和网络等都没有访问权限, 这些都是通过浏览器进程去访问的.
插件进程: 专为 Flash, Adobe reader 这类插件创建的进程.
GPU 进程: 目前绝大部分浏览器都有 GPU 进程(GPU 就是我们通常说的显卡的芯片)GPU 进程主要负责硬件加速, 即提高浏览器对视频和图像的渲染体验.
以此我们可以看到浏览器做的事情其实非常多, 它的各个进程相互配合, 可以实现浏览网页, 播放视频, 查看 pdf, excel 等各类文档 (只要安装了对应插件), 发起网络请求, 读取计算机磁盘等等功能. 不过由于浏览器内核是由不同厂商开发, 所以浏览器之间也有差别, 虽然它们都被要求遵守 W3C(万维网联盟) 的规范, 但也仅仅是遵守了部分规范而已.
渲染进程(浏览器内核)
渲染进程是我们最关注的进程, 它有多个线程, 主要包括:
GUI 渲染线程: 负责渲染界面, 即解析 HTML 和 CSS 构建 DOM 树
JS 引擎线程: 负责解析 JavaScript 脚本, 处理自己内部的任务(执行栈), 如果没有任务就去执行队列的栈顶取任务执行.
计时器线程: 等待延时时间到达就将计时器里的事件放到执行队列中.
http 请求线程: 等待网络请求返回结果就将其回调函数放到执行队列中.
事件触发线程: 等待用户做点击, 按下鼠标等操作时将该事件放到执行队列中. 另外, 该线程还负责控制事件循环.
......
现在让我们从这段简单的话里理出几条重要的信息.
GUI 渲染线程和 JS 引擎线程是互斥的
因为 js 脚本可以操作 DOM 树, 所以为了避免 GUI 渲染和 js 执行在操作 DOM 时发生冲突, 它们并不会一起发生, 当 GUI 渲染过程遇到 < script > 标签时就会停下来等待这段 js 代码执行完再继续渲染.
js 引擎是由 js 当前所在环境提供的
js 可以在浏览器里面运行是因为浏览器提供了解析 js 语法的引擎, Node 让 js 可以运行在服务端是也是因为 Node 给 js 提供了引擎, 并且, 各浏览器之间, Node 和浏览器之间实现的 js 引擎都是有差异的.
很多事情是浏览器做的而不是 js 引擎做的
我以前一直没深究过单线程的 js 是如何调配事件, 如何监听异步事件的回调函数的, 现在才知道这些都是浏览器在处理, 或者说是浏览器的渲染进程在处理, 而 js 引擎要做的就是依次处理执行栈中的任务, 当执行栈为空就去执行队列取出第一个任务接着处理.
浏览器会给异步任务开辟另外的线程
这里另外的线程就是指计时器线程, http 请求线程和事件触发线程等. 而 js 会在执行栈 (同步任务) 为空时才去处理任务队列 (异步任务) 的任务.
执行栈和任务队列
执行栈又叫主线程, 任务队列又叫消息队列
上面我们已经提到了这两个概念了, 现在让我们结合例子和图来加深理解.
例 3:
- setTimeout(() => {
- console.log('setTimeout 延时到了')
- }, 500)
- function excute(a, b) {
- let addRes = add(a, b);
- console.log(`add result: ${addRes}`)
- }
- function add(a, b) {
- return a+b
- }
- excute(2,3)
代码开始遇到了 setTimeout, 由计时器线程处理, 计时器线程会负责计时, 当到达 setTimeout 的延时时间即这里的 500ms 时会将其加入到任务队列中等待 js 执行.
计时器线程
接着执行 excute()函数, excute()是同步函数, 所以进入执行栈 (主线程) 中:
执行栈
在执行 excute()过程中发现它内部调用了 add()函数, 于是将 add()函数也加入执行栈中:
执行栈
add()函数执行完以后将结果返回并从执行栈中弹出:
执行栈
excute()函数打印结果, 并从执行栈中弹出, 此时执行栈中为空, js 开始去处理任务队列中的任务, 假如现在前面的定时器任务已经加入到任务队列中了:
执行队列
js 会去任务队列询问是否有待处理事件, 如果有就取第一条执行, 此时打印出
- console.log('setTimeout 延时到了')
- .
本篇文章开头的第一个例子 setTimeout 的延时是 0, 不是延时 0ms 执行, 而是延时 0ms 加入到任务队列中, 所以它会在所有的同步任务执行完以后再执行. 而在第二个例子中,
for (let i = 0; i <1000000000; i++) {}
是一个比较耗时的同步任务, 所以 setTimeout 打印出的时间差是大于 100ms 的.(当然, 即使是一个耗时很短的同步任务也会导致 setTimeout 打印出的值大于 100, 这里只是为了放大同步任务对其的影响而已)
事件循环(Event Loop)
经过前面的分析可以得出以下结论:
js 先顺序执行执行栈中的任务, 当执行栈为空时再去询问任务队列是否有任务, 而任务队列是一个先进先出的机构, js 引擎始终从任务队列的顶部取任务执行, js 引擎从任务队列取事件的过程是循环不断的, 所以这个过程又被称为 "事件循环(Event Loop)"
整个过程大致是这样:
2018-03-28_161542.jpg
但是! 但是! 不仅仅是这么简单, 如果仅仅是同步任务和异步任务这种区分方式, 那么看下面这个例子:
例 4:
- setTimeout(() => {
- console.log('定时器 1 开始了~')
- }, 0)
- Promise.resolve().then(() => {
- console.log('promise1 开始了~')
- })
- setTimeout(() => {
- console.log('定时器 2 开始了~')
- Promise.resolve().then(() => {
- console.log('promise2 开始了~')
- })
- }, 0)
- console.log("---end---");
这段代码的输出结果是什么? js 引擎是如何处理不同类型的异步任务的?
答案是 Microtasks 和 Macrotasks.
Microtasks 和 Macrotasks
Macrotasks 也称 Tasks, 后文我就直接写 Tasks, 也方便大家区分.
我在学习这里的时候就被误导过, 当时以为 Tasks 和 Microtasks 是针对异步任务而言的, 而其实不是, 应该说这才是区分任务最准确的方式.
Tasks: 所有的同步任务(执行栈),setTimeout,setInterval 等
Microtasks:Promise,process.nextTick 等
一个简单的结论是: 先执行 Tasks,Tasks 执行完以后再执行 Microtasks
当我看到这个结论时, 心中已经有了答案, 我觉得例 4 代码的打印结果是:
---end---(因为这是在执行栈中的任务, 会最先执行)
定时器 1 开始了~ (setTimeout 属于 Tasks, 先于 Promise 执行)
定时器 2 开始了~
promise1 开始了~ (Promise 属于 Microtasks, 会在 Tasks 执行完以后才开始执行)
promise2 开始了~
然而正确的结果是:
---end---
promise1 开始了~
定时器 1 开始了~
定时器 2 开始了~
promise2 开始了~
咦? 说好的 Tasks 先于 Microtasks 执行呢? 怎么反倒是 Promise 先执行了? 然后我又仔细读了这段话:
js 开始执行 Tasks, 执行过程中如果遇到 Microtasks 就将其加入任务队列中, 当 Tasks 执行完毕以后就去执行 Microtasks. 然后触发 GUI 渲染线程重新渲染界面, 当 GUI 渲染完成以后再继续下一轮 Tasks, 如果下一轮又遇到了 Microtasks 则等这一轮 Tasks 执行完毕以后又继续执行 Microtasks......
所以, 准确的事件循环应该是: Tasks -> Microtasks -> GUI 渲染 -> Tasks....
前面的结论其实也没有问题, 确实是先执行 Tasks,Tasks 执行完以后再执行 Microtasks, 只是这句话有歧义, 先执行 Tasks 的意思是先执行当前这一个 Tasks, 所以!! 并不是说 Tasks 会先于所有的 Microtasks 执行, 而是在每一次的事件循环过程中, 当前的 Tasks 一定会先于当前的 Microtasks 执行
如果还不明白, 再看例 4 的代码(一部分):
- setTimeout(() => {
- console.log('定时器 1 开始了~')
- }, 0)
- Promise.resolve().then(() => {
- console.log('promise1 开始了~')
- })
- console.log("---end---");
setTimeout 和
console.log("---end---")
是两个 Tasks,Promise 是 Microtasks, 而 setTimeout 和 Promise 是异步任务会加入到任务队列中等待执行,
console.log("---end---")
会直接进入主线程 (执行栈) 执行, 现在重新画一个流程图就应该是这样的:
事件循环. png
(画图画到吐血啊~)
执行过程已经非常清楚了, 每一轮事件循环只会执行一个 Tasks 和多个 Microtasks, 而所有的同步任务一开始就在执行栈中了, 它们的执行优先级最高, 所以 setTimeout 或者 setInterval 这类 Tasks 会在第二轮以后才被执行.
现在再来看例 4 的全部代码:
- // 代码块 1
- setTimeout(() => {
- console.log('定时器 1 开始了~')
- }, 0)
- // 代码块 2
- Promise.resolve().then(() => {
- console.log('promise1 开始了~')
- })
- // 代码块 3
- setTimeout(() => {
- console.log('定时器 2 开始了~')
- // 代码块 3-1
- Promise.resolve().then(() => {
- console.log('promise2 开始了~')
- })
- }, 0)
- // 代码块 4
- console.log("---end---");
根据前面的分析, 第一轮事件循环包括:
主线程里的代码, 属于 Tasks 的代码块 4
任务队列里的代码, 属于 Microtasks 的代码块 2
第二轮事件循环包括:
任务队列里面的代码, 属于 Tasks 的代码块 1
第三轮事件循环包括:
任务队列里面的代码, 属于 Tasks 的代码块 3
任务队列里的代码, 属于 Microtasks 的代码块 3-1
注意: 不同的浏览器结果不一样, 但根据规范, 这确实才是正确的结果.
HTML5 的 Web Worker
Web Worker 是让 js 可以模拟多线程工作的技术, 即 Web Worker 里面的任务不会阻塞主线程执行和 GUI 渲染, 但是, 由于我们前面提到的原因, Web Worker 是不能处理与 DOM 相关的任务的, 具体来说, 在 Web Worker 里可以操作的对象有:
navigator 对象
location 对象(只读)
XMLHttpRequest 对象
setTimeout 和 setInterval 方法
应用缓存
不可操作的对象有:
DOM 对象
Window 对象
document 对象
因为我也还没用到过这个技术, 所以就不再展开它的详细用法了, 建议大家阅读 MDN 上的使用 Web Workers https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers , 讲的非常详细.
总之呢, Web Worker 并没有让 js 由单线程变成多线程, 它只是让 js 有了多线程的能力, 一般来说, 会放在 Web Worker 里的任务都是耗时或计算量很大的, 而大部分时候我们都不需要 js 来做计算量很大的工作, 所以目前用到它的地方还不多, 不过这也只是我的看法~
写在最后
感觉写在最后的话被我写在前言里面了, 所以好像也没啥好总结的了, 只是感觉自己很拖沓, 这篇文章前前后后拖了大半个月, 真的是很懒很拖延了~
来源: http://www.jianshu.com/p/fe7baef4a1f6