JS 运行机制-EventLoop(事件循环)
javascript 是单线程的
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完
全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
主线程和任务队列
单线程就意味着,所有任务需要排队。所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说:
1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",根据顺序循环调用异步任务,进入执行栈,开始执行,直到所有任务队列异步任务执行完
4. 另外,任务队列中的每一个事件都是一个宏任务,执行栈执行的过程中也会有微任务,他们的执行顺序是:见下面总结
下图就是主线程和任务队列的示意图
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。
宏任务和微任务
了解完主线程,还要了解一下任务,任务有宏任务(MacroTask)和微任务(MicroTask)之分。
macro-task(宏任务):可以理解为每次执行栈执行的代码就是一个宏任务,包括每次从任务队列中获取一个事件并将其对应的回调放入到执行栈中执行。宏任务需要多次事件循环才能执行完,任务队列中的每一个事件都是一个宏任务。每次事件循环都会调入一个宏任务,浏览器为了能够使 JS 内部宏任务与 DOM 任务有序的执行,会在一个宏任务结束后,下一个宏任务开始前,对页面进行重新渲染。
micro-task(微任务):可以理解为在当前宏任务执行结束后立即执行的任务。微任务是一次性执行完的,在一个宏任务执行完毕后,就会将它执行期间产生的所有微任务都执行完毕。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务放到队列尾部,之后会依次执行。
宏任务主要有:script代码段、setTimeout、setInterval、I/O、setImmediate(node)等.
微任务主要有:process.nextTick(node),Promise的回调(Promist.then、catch、finally)、MutationObserver
总结:
执行一个 宏任务(栈中没有就从 事件队列中获取)
执行过程中如果遇到 微任务,就将它添加到 微任务的任务队列中
宏任务执行完毕后,立即执行当前 微任务队列中的所有 微任务(依次执行)
当前 宏任务执行完毕,开始检查渲染,然后 GUI线程接管渲染
渲染完毕后, JS线程继续接管,开始下一个 宏任务(从事件队列中获取)
好了概念讲清楚了,下面看几个例子
宏任务
浏览器为了能够使 宏任务和 DOM任务有序的进行,会在一个 宏任务执行结果后,在下一个 宏任务执行前, GUI渲染线程开始工作,对页面进行渲染。
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';
我们可以将这段代码放到浏览器的控制台执行以下,看一下效果:
我们会看到的结果是,页面背景会在瞬间变成白色,以上代码属于同一次 宏任务,所以全部执行完才触发 页面渲染,渲染时 GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。
第二个例子:
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black';
})
执行一下,再看效果:
我会看到,页面先显示成蓝色背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次 宏任务,第一次 宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。
微任务
document.body.style = 'background:blue';
console.log(1);
Promise.resolve().then()=>{
console.log(2);
document.body.style = 'background:black';
}
console.log(3)
执行一下,再看效果:
控制台输出 1 3 2 , 是因为 promise 对象的 then 方法的回调函数是异步执行,所以 2 最后输出
页面的背景色直接变成黑色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务,
在微任务中将背景变成了黑色,然后才执行的渲染
第二个例子:
setTimeout(()=>{
console.log(1);
Promise.resolve(3).then(data=> console.log(data));
},0)
setTimeout(() => {
console.log(2);
},0)
上面代码共包含两个 setTimeout ,也就是说除主代码块外,共有两个 宏任务,
其中第一个 宏任务执行中,输出 1 ,并且创建了 微任务队列,所以在下一个 宏任务队列执行前,
先执行 微任务,在 微任务执行中,输出 3 ,微任务执行后,执行下一次 宏任务,执行中输出 2
async/await
关于async/await 相关优点 :Async/Await替代Promise的6个理由
async
当我们在函数前使用async的时候,使得该函数返回的是一个Promise对象
async function test() {
return 1 // async的函数会在这里帮我们隐士使用Promise.resolve(1)
}
// 等价于下面的代码
function test() {
return new Promise(function(resolve, reject) {
resolve(1)
})
}
可见async只是一个语法糖,只是帮助我们返回一个Promise而已
await
await表示等待,是右侧「表达式」的结果,这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值(换句话说,就是没有特殊限定)。并且只能在带有async的内部使用
使用await时,会从右往左执行,当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码, 并且当await执行完毕之后,会先处理微任务队列的代码
下面来看一个栗子:
async function async1() {
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2() {
console.log( 'async2' )
}
console.log( 'script start' )
setTimeout( function () {
console.log( 'setTimeout' )
}, 0 )
async1();
new Promise( function ( resolve ) {
console.log( 'promise1' )
resolve();
} ).then( function () {
console.log( 'promise2' )
} )
console.log( 'script end' )
下面是在chrome浏览器上输出的结果
使用事件循环机制分析:
- 首先执行同步代码,console.log( ‘script start’ )
- 遇到setTimeout,会被推入宏任务队列
- 执行async1(), 它也是同步的,只是返回值是Promise,在内部首先执行console.log( ‘async1 start’ )
- 然后执行async2(), 然后会打印console.log( ‘async2’ )
- 从右到左会执行, 当遇到await的时候,阻塞后面的代码,去外部执行同步代码
- 进入 new Promise,打印console.log( ‘promise1’ )
- 将.then放入事件循环的微任务队列
- 继续执行,打印console.log( ‘script end’ )
- 外部同步代码执行完毕,接着回到async1()内部, 由于async2()其实是返回一个Promise, await async2()相当于获取它的值,其实就相当于这段代码Promise.resolve(undefined).then((undefined) => {}),所以.then会被推入微任务队列, 所以现在微任务队列会有两个任务。接下来处理微任务队列,打印console.log( ‘promise2’ ),后面一个.then不会有任何打印,但是会执行
- 行后面的代码, 打印console.log( ‘async1 end’ )
- 进入第二次事件循环,执行宏任务队列, 打印console.log( ‘setTimeout’ )
粗略大概,规范在改,promise, async await 比想象的还要复杂些
来几个常见的题目吧
console.log(1);
while(true){};
cosole.log(2);
// 1
结果:因为这是同步任务,程序由上到下执行,遇到while()死循环,下面语句就没办法执行
console.log(1);
setTimeout(()=>{
console.log(2);
})
while(true){};
// 1
依然是 1,因为setTimeout()就是个异步任务
console.log(1);
setTimeout(() => {
console.log('setTimeout');
}, 0);
let promise = new Promise(resolve => {
console.log(3);
resolve();
}).then(data => {
console.log(100);
}).then(data => {
console.log(200);
});
console.log(2);
有好玩的我就加一下
const first = () => (new Promise((resolve,reject)=>{
console.log(3);
let p = new Promise((resolve, reject)=>{
console.log(7);
setTimeout(()=>{
console.log(5);
resolve(6);
},0)
resolve(1);
});
resolve(2);
p.then((arg)=>{
console.log(arg);
});
}));
first().then((arg)=>{
console.log(arg);
});
console.log(4);
第一轮循环
先执行宏任务,主script ,new Promise立即执行,输出【3】,执行p这个new Promise 操作,输出【7】,发现setTimeout,将回调放入下一轮任务队列(Event Queue),p的then,姑且叫做then1,放入微任务队列,发现first的then,叫then2,放入微任务队列。执行console.log(4),输出【4】,宏任务执行结束。
再执行微任务,执行then1,输出【1】,执行then2,输出【2】。到此为止,第一轮事件循环结束。开始执行第二轮。
第二轮循环
先执行宏任务里面的,也就是setTimeout的回调,输出【5】。resovle不会生效,因为p这个Promise的状态一旦改变就不会在改变了。 所以最终的输出顺序是3、7、4、1、2、5。
掘金上有个题不错
https://juejin.im/post/5a04066351882517c416715d
再看一个
const fn = () => new Promise((resolve, reject) => {
console.log(3)
let p = new Promise((resolve, reject) => {
console.log(7)
setTimeout(() => {
console.log(5)
resolve(6)
})
resolve(1)
})
resolve(2)
p.then(arg => { console.log(arg) })
})
fn().then(arg => { console.log(arg) })
console.log(4)
显示同步代码: 3 7 4
接下来看异步代码:
宏队列 :【5】
微队列 :【1 2】
再来一个抽搐型的
setTimeout(() => {
console.log('0')
})
new Promise((resolve, reject) => {
console.log('1')
resolve()
}).then(() => {
console.log('2')
new Promise((resolve, reject) => {
console.log('3')
resolve()
}).then(() => {
console.log('4')
}).then(() => {
console.log('5')
})
}).then(() => {
console.log('6')
})
new Promise((resolve, reject) => {
console.log('7')
resolve()
}).then(() => {
console.log('8')
})
// 1 7 2 3 8 4 6 5 0