为什么js是单线程
js最大的特点就是单线程,即同一个时间只能做一件事。那么为啥js不能多线程呢?多线程后效率不是更高吗?
普遍性
在 GUI 编程里,单一线程控制 GUI,是一个非常普遍的做法。js 最初就是用在网页上的,早期设计了 js 只能单线程运行,沿袭普遍做法,也就显得非常顺理成章了。
用途
作为浏览器脚本语言,js的主要用途就是与用户互动、操作DOM,如果js同时有2个线程在跑,A线程在某个DOM上添加节点,B线程删除了这个DOM节点,这时浏览器应该以哪个线程为准?这就乱套了。为了简单起见,js生而单线程,以后至少可预见的以后也会保持这个特性。
其他
为了能够更好的利用CPU的计算能力,HTML5提出Web Worker标准,允许js脚本创建多个线程,但是子线程完全由主线程控制,且不能操作DOM,所以本质上还是单线程。
进程和线程
概念回顾
- 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位 。
- 线程是进程的一个具体表现形式,是CPU调度和分派的基本单位,它是比进程更小、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可以与同属一个进程的其他的线程共享进程所拥有的全部资源。
- 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
浏览器的多进程和其中的多线程
浏览器是多进程的,浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
- 浏览器主进程:负责协调、主控各个线程,只有一个,作用有:
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
- 插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程:最多一个,用于3D绘制等
- 渲染进程:排版引擎和V8引擎都运行在这个进程中,主要是将html、css、js转换为用户可与之交互的网页
- GUI 渲染线程
- 绘制页面,解析 HTML、CSS,构建 DOM 树和render树,布局和绘制等
- 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新**
- JS 引擎线程
- 负责 JS 脚本代码的执行
- 负责执行那些准备好待执行的事件
- 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染
- 事件触发线程
- 辅助 JS 引擎,用于控制事件循环,管理着一个任务队列
- 事件件触发线程会把符合条件的定时器、鼠标点击、ajax异步请求等对应的任务,添加到任务队列的末尾,等待 JS 引擎处理
- 定时触发器线程
- 定时器setInterval和setTimeout所在的线程
- 在到达时间后,事件触发线程再把回调函数添加到任务队列的队尾,等待JS引擎处理
- HTTP 请求线程
- XMLHttpRequest在连接后会通过浏览器新开一个线程请求,在发起一个异步请求时去,http请求线程负责去请求服务器,有了响应后,事件触发线程再把回调函数添加到任务队列的队尾,等待JS引擎处理
队列
单线程意味着所有的任务都需要排队,等待前一个任务完成后再执行下一个任务。如果前一个任务执行时间长,后面的任务就一直等着。如果计算量大CPU忙不过来就算了,但是很多时候CPU是闲着的,因为IO设备很慢(比如Ajax操作从网络读取数据),不得不等着结果出来再往下执行。
js的设计者意识到,这时主线程可以完全不管IO设备,挂起处于等待中的任务,先运行排在后面的任务,等到IO设备返回了结果,再回头继续执行挂起的任务。
于是任务就分为了两种:一种是同步任务,一种是异步任务。
数据在队列中的存储与使用方式类似于数据结构中的队列数据结构,遵循先进先出的原则。
同步任务
同步任务指的是在主线程上排队执行的任务,只有前一个任务执行完毕后,才能执行下一个任务。
异步任务
异步任务指的是不进入主线程,而进入“任务队列”(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程去执行。
任务队列里存放着各种异步操作所注册的回调,里面分为两种任务类型,宏任务(macroTask
)和微任务(microTask
)。
宏任务和微任务
常见的宏任务(MacroTask)
- setTimeout / setInterval
- I/O
- 用户交互操作
- UI渲染
- setImmediate(node环境)
常见的微任务(MicroTask)
- Promise.then()、catch()、finally()里面的回调
- process.nextTick(node环境)
事件循环(Event Loop)
基础知识准备的差不多了,下面开始重头戏。所有的同步任务都在主线程上执行,形成一个执行栈。主线程之外,事件触发线程还控制着一个任务队列,只要异步任务有了结果,就在任务队列中放一个事件回调,一旦执行栈中的所有同步任务执行完步,也就是**JS引擎线程 **空闲了,系统就会读取任务队列,将可运行的异步任务添加到栈中去执行。
执行步骤
- script代码入栈,遇到同步任务,直接执行。
- 如果是异步任务,则放入任务队列中:微任务放到微任务队列,宏任务放到宏任务队列。
- 检查微任务队列是否为空,如果为空执行步骤4,若不为空,则先把微任务队列中的队首取出来,放到执行栈中执行,然后检查微任务队列中是否还有其他的微任务,如果有则继续执行微任务,如果没有就 更新 UI 渲染,然后去执行宏任务。
- 执行宏任务队列中的队首元素,执行完之后,进入步骤3。
由于JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。也就是当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> …
小试牛刀
console.log('start');
// 记作 set1
setTimeout(function () {
console.log('2');
// set4
setTimeout(function() {
console.log('3');
});
// pro2
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
// 记作 pro1
new Promise(function (resolve) {
console.log('6');
resolve();
}).then(function () {
console.log('7');
// set3
setTimeout(function() {
console.log('8');
});
})
// 记作 set2
setTimeout(function () {
console.log('9');
// 记作 pro3
new Promise(function (resolve) {
console.log('10');
resolve();
}).then(function () {
console.log('11');
})
})
console.log('end');
第一轮:(start - 6 - end)
- 执行console.log(‘start’),输出start
- 把set1放到宏任务队列
- 执行new Promise()里的内容,输出6
- 把pro1.then()放到微任务队列
- 把set2放到宏任务队列
- 执行console.log(‘end’),输出end
第二轮:(7 - 2 - 4)
- 检查微任务队列是否为空,发现不为空,将pro1.then()拿出来放到执行栈中执行,输出7
- 将set3放到宏任务队列
- 检查微任务队列是否为空,发现为空,取宏任务队列中的队首元素set1到执行栈中执行,输出2
- 将set4放到宏任务队列中
- 执行new Promise()里的内容,输出4
- 将pro2.then()放到微任务队列
第三轮:(5 - 9 - 10)
- 检查微任务队列是否为空,发现不为空,将pro2.then()拿出来放到执行栈中执行,输出5
- 检查微任务队列是否为空,发现为空,取宏任务队列中的队首元素set2到执行栈中执行,输出9
- 执行new Promise()里的内容,输出10
- 将pro3.then()放到微任务队列
第四轮:(11 - 8)
- 检查微任务队列是否为空,发现不为空,将pro3.then()拿出来放到执行栈中执行,输出11
- 检查微任务队列是否为空,发现为空,取宏任务队列中的队首元素set3到执行栈中执行,输出8
第五轮:(3)
- 检查微任务队列是否为空,发现为空,取宏任务队列中的队首元素set4到执行栈中执行,输出3
第六轮:(start - 6 - end - 7 - 2 - 4 - 5 - 9 - 10 - 11 - 8 - 3)
- 执行完毕