这篇借助于同事准备的技术分享,其他技术文章,书本知识,自己的理解梳理而成
高级程序设计第三版:
js 是一门单线程的语言,运行于单线程的环境中,例如定时器等并不是线程,定时器仅仅只是计划代码在未来的某个时间执行,浏览器负责排序,指派某段代码在某个时间点运行
的优先级
1.为什么规定浏览器必须是单线程?
JS主要用途之一是操作DOM,如果JS同时有两个线程,同时对同一个dom进行操作,一个需要删除dom,一个需要添加dom,这时浏览器应该听哪个线程的,如何判断优先级,所以为了简化操作,规定js是一门单线程的语言。
2.有关于js是单线程的理解
所谓的"JS是单线程的"是指解释和执行JS代码的线程,只有一个,一般称之为“主线程”,而浏览器并不是单线程的,是多线程并且是多进程的,而对于前端最关心的还是渲染进程.
- GUI渲染线程
● 负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制
● 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
● GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中,等到JS引擎空闲时立即被执行。 - JS引擎线程
● 也称JS内核,负责处理JS脚本程序。例如V8引擎
● JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(Renderer进程)中无论什么时候都只有一个JS引擎线程在运行JS程序
● GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,页面渲染就不连贯。 - 定时触发器线程
● 传说中的setInterval和setTimeout所在的线程
● 定时器线程其实只是一个计时的作用,他并不会真正执行时间到了的回调,真正执行这个回调的还是JS主线程。当时间到了,定时器线程就通知事件触发线程,让事件触发线程将setTimeout的回调事件添加到待处理任务队列的尾部,等待JS引擎的处理。
● W3C在HTML5标准中规定,要求setTimeout中低于4ms的时间间隔算4ms - 事件触发线程
● 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解为:JS引擎自己都忙不过来,需要浏览器另开线程协助)
● 当JS引擎执行setTimeout时(或者是来自浏览器内核的其他线程,如鼠标点击、ajax异步请求等),当这些事件满足触发条件被触发时,该线程就会将对应回调事件添加到添加到待处理任务队列的尾部,等待JS引擎的处理
● 由于JS是单线程关系,所以这些待处理任务队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行) - 异步http请求线程
● 这个线程负责处理异步的ajax请求,当请求完成后如果设置有回调函数,他也会通知事件触发线程,然后事件触发线程将这个回调再放入任务队列中尾部,等待JS引擎执行
3.单线程如何实现异步?
大家都知道JS是单线程的脚本语言,在同一时间,只能做同一件事,为了协调事件、用户交互、脚本、UI渲染和网络处理等行为,防止主线程阻塞,设计者给JS加了一个事件循环(Event Loop)的机制
3.1理解什么是执行上下文?
可以看这篇文章https://amberzqx.com/2020/02/04/JavaScript%E7%B3%BB%E5%88%97%E4%B9%8B%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E5%92%8C%E6%89%A7%E8%A1%8C%E6%A0%88/
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。执行上下文(可执行代码段)总共有三种类型:
全局执行上下文(全局代码):不在任何函数中的代码都位于全局执行上下文中,只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
函数执行上下文(函数体):只有调用函数时,才会为该函数创建一个新的执行上下文,可以存在无数个,每当一个新的执行上下文被创-建,它都会按照特定的顺序执行一系列步骤。
Eval 函数执行上下文(eval 代码): 指的是运行在 eval 函数中的代码,很少用而且不建议使用
执行上下文又包括三个生命周期阶段:创建阶段 → 执行阶段 → 回收阶段
JS引擎创建了执行上下文栈(执行栈)来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出,后进先出的原则,就像下面的汉诺塔,第一个最大的先进去,当拿出来的时候肯定是最后一个出来的,最小的那个后进去,拿出来的时候是最先拿出来的~因为JS执行中最先进入全局环境,所以处于"栈底的永远是全局执行上下文"。而处于"栈顶的是当前正在执行函数的执行上下文"
举个例子:
const firstFunction = () => {
console.log('1');
secondFunction();
console.log('2');
}
const secondFunction = () => {
console.log('3');
}
firstFunction();
console.log(4)
// 1324
//从上到下的执行
//图一:从上到下执行,先是全局作用域,那就是栈底第一个
//图二: firstFunction的调用,打印出1,现在栈顶是secondFunction,因为函数里面还没有执行完,所以还没有被销毁
//图三: secondFunction的调用,打印3,secondFunction,因为函数里面执行完,所以要被销毁到图四
//再执行栈顶firstFunction里面的2到图5
看上面的图是不是对应汉诺塔放进去,拿出来的一个过程
3.2理解同步任务,异步任务,任务队列
JavaScript 是一个单线程序的解释器,因此一定时间内只能执行一段代码。为了控制要执行的代码,就有一个 JavaScript 任务队列。这些任务会按照将它们添加到队列的顺序执行如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行.同步任务指的是,在主线程上,排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程,而进入“任务队列”(task queue)的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
3.3 js的事件循环机制
具体来说,异步运行机制如下:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)述过程会不断重复,也就是常说的Event Loop(事件循环)
3.4宏任务微任务
js的任务又分为宏任务和微任务(btw,不要归类为同步任务异步任务和宏任务微任务扯上联系,抛开同步异步的概念去理解宏任务微任务)
前端常见的宏任务,微任务分类:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval,setImmediate(node或者小众浏览器支持)
micro-task(微任务):Promise,process.nextTick(node 环境支持)
注意:
Promise 新建后就会立即执行,也就是说new Promise构造函数是同步任务,但Promise的注册的then回调和catch回调才是微任务
宏任务:可以理解是每次执行栈执行的代码就是一个宏任务,所有宏任务都是添加到任务队列,所以”任务队列又叫宏任务队列”,这个任务队列由事件触发线程来单独维护的
微任务
可以理解是在当前宏任务执行结束后立即执行的任务
每一次事件循环,是先执行宏任务,再执行宏任务里面的微任务,看到里面两个字了吗?????每一次事件循环只执行一个宏任务
注意:
1.微任务队列里边的优先级process.nextTick()>Promise.then()
2.setInterval,setImmediate的执行顺序后续补充,目前前端几乎用不到setImmediate,不要慌
总结:
每一次循环称为 tick, 每一次tick的任务如下:
1、执行一个宏任务(执行栈中没有就从任务队列中获取)
2、宏任务执行过程中如果遇到微任务,就将它添加到微任务队列中
3、宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
4、重复1到3步骤
宏任务 > 所有微任务 > 宏任务>微任务,也就是每一次事件循环,是先执行宏任务,再执行宏任务里面的微任务,下一轮还是先执行宏任务,再执行下一轮的微任务
练习题目:
example1
console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('3')
}).then(() => {
console.log('4')
})
console.log('5')
// 1 5 3 4 2
//第一轮循环,执行宏任务script里面的同步任务,遇到微任务挂起,先执行15
// 执行完宏任务,执行微任务 3, 4 微任务执行完毕了就是下一轮事件循环的开始
// 第二轮循环 执行 settimout里面的2
example2
es6那本书---Promise 新建后就会立即执行
console.log('1')
new Promise((resolve) => {
console.log('2')
setTimeout(()=>{
console.log('4')
resolve()
}, 0)
}).then(() => {
console.log('3')
})
// 1,2,4,3
// 第一轮: 宏 script 打印出:12
// 第二轮: 宏 setTimeout 打印出: 4,3
// 这里要注意Promise的注册的then回调和catch回调才是微任务,所以resolve()是在setTimeout里面调的
// 属于第二轮的微任务
example3
new Promise((resolve) => {
console.log('1')
setTimeout(() => {
console.log('2')
}, 1000)
resolve()
}).then(() => {
console.log('3')
})
// 132
// 基于example2,可以明白,resolve()在setTimeout之外调用,还是属于第一轮宏任务里面的微任务
example4
console.log('1')
new Promise((resolve) => {
console.log('2')
resolve()
console.log('3')
}).then(() => {
console.log('4')
})
console.log('5')
// 第一轮 宏: script 打印: 1235 微:4
// 12354
// 要注意的是3不用等resolve回调完再执行哦,因为并没有还可以继续执行,await可以阻塞下面的执行
example5
console.log('1')
setTimeout(() => {
console.log('2')
new Promise((resolve) => {
console.log('3')
resolve()
}).then(() => {
console.log('4')
})
})
new Promise((resolve) => {
console.log('5')
resolve()
}).then(() => {
console.log('6')
})
console.log('7')
setTimeout(() => {
console.log('8')
new Promise((resolve) => {
console.log('9')
resolve()
}).then(() => {
console.log('10')
})
})
// 第一轮循环 宏:script 打印: 157 微:resolve() 打印:6
// 第二轮循环 宏:第一个setTimeout 打印:23 微:resolve() 打印:4
// 第三轮: 宏:第二个setTimeout 打印:89 微:resolve() 打印:10
// 15762348910
example6 在node下面执行,这个例子一般,不值得一看
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 第一轮 宏:script 打印:17 微: process.nextTick以及Promise.then 打印: 6 8
// 第二轮:宏: 第一个settimeout 打印: 24 微:process.nextTick以及Promise.then 打印:35
// 第三轮: 宏: 第二个settimeout 打印: 9 11 微: process.nextTick以及Promise.then 打印: 10 12
// 1768 2435 9 11 10 12
example6
async function async1(){
console.log('1')
await async2()
console.log('2')
}
async function async2(){
console.log('3')
}
console.log('4')
setTimeout(function(){
console.log('5')
},0)
async1();
new Promise(function(resolve){
console.log('6')
resolve();
}).then(function(){
console.log('7')
})
console.log('8')
// 第一轮事件循环 宏:script 打印:4 1 3 6 8 微: await之后的结果 以及resolve() 打印: 2 7
// 第二轮事件循环 宏:setTimeout 打印: 5
// 4 1 3 6 8 2 7 5
//注意 await xx的时候,相当于xx这里直接创建了一个new promise,所以async2函数是new promise,会立即执行, await的结果是promise.then的结果,并且没有成功finish会阻塞下面的执行,所以2会在微任务拿到结果之后执行
还有一个例子,前端目前用不到哈,后续更新:
console.log('1')
setTimeout(() => {
console.log('2')
process.nextTick(() => {
console.log('3')
})
new Promise((resolve) => {
console.log('4')
resolve()
}).then(() => {
console.log('5')
})
})
new Promise((resolve) => {
console.log('7')
resolve()
}).then(() => {
console.log('8')
})
console.log('9')
process.nextTick(() => {
console.log('10')
})
setImmediate(() => {
console.log('15')
process.nextTick(() => {
console.log('16')
})
new Promise((resolve) => {
console.log('17')
resolve()
}).then(() => {
console.log('18')
})
})
setTimeout(() => {
console.log('11')
new Promise((resolve) => {
console.log('12')
resolve()
}).then(() => {
console.log('13')
})
process.nextTick(() => {
console.log('14')
})
})
1,7,9,10,8,
2,4,3,5
11 12 14 13
15 17 16 18