Event Loop

156 阅读4分钟

进程和线程的区别

经常说js是单线程的执行的,那什么是单线程呢 ?
首先我们来看看什么是进程。本质上来说,两个名词都是CPU工作时间的描述。

进程描述了CPU在运行指令及加载和保存上下文所需要的时间,放在应用来说就表示了一个程序,线程是进程中更小的单位,描述了执行一段指令需要的时间。

把这些概念拿到浏览器上来说,当我们打开一个tab页面时,其实就创建了一个进程,一个进程中有多个线程,比如渲染引擎线程、JS引擎线程、HTTP请求线程等。当我们发送一个请求,就是创建了一个线程,请求完成后,这个线程就会销毁。

上段中说的渲染引擎线程和JS引擎线程,在我们平时开发中,肯定也发现了,JS的运行时会阻碍UI渲染,这说明两个线程是互斥的。这其中的原因JS可以修改DOM,如果JS执行时,UI线程还在工作,就可能导致不安全的UI渲染。JS单线程的另一个好处就是可以节省内存,节约上下文切换的时间,没有锁的问题。

执行栈

什么是执行栈?
可以把执行栈认为一个存储执行函数的栈结构,遵循先进后出的规则。

function foo(a, b) {
    return 2 * a + b;
}
function boo(a, b, c) {
    return foo(a, b) + c
}
console.log(boo(1, 2, 3));

这段代码执行时,首先把console.log(boo(1, 2, 3))放入执行栈中,然后继续把boo(1, 2, 3)放入执行栈中,然后执行到boo函数内部,发现调用了foo函数,就把foo(a, b)放入执行栈中,foo()函数内部无其他调用,执行返回值 4, foo(a, b)出栈。执行boo(1, 2, 3),返回值7,然后其出栈。最后执行console.log输出结果7,console.log出栈,执行完成。

浏览器中的Event Loop

异步代码的执行顺序?
当遇到异步代码的时候,会被挂起并在需要执行的时候加入到Task(有多种Task)队列中,一旦执行队列为空,Event Loop就会从Task队列中拿需要执行的代码并发入到执行栈中执行。所以从本质上来说JS中的异步行为还是同步行为。
不同的任务源会被分配到不同的Task队列中,任务源可以分为微任务(microtask)和宏任务(macrotask)。在es6规范中,microtask称为job,macrotask称为task

console.log('script start');
async function async1() {
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2 end');
}
async1();
setTimeout(() => {
    console.log('timeout end')
}, 0);
new Promise(resolve => {
    console.log('promise start');
    resolve();
}).then(res => {
    console.log('then1');
}).then(res => {
    console.log('then2');
})

console.log('script end');

输出:

  • script start
  • async2 end
  • promise start
  • sctipt end
  • async1 end
  • then1
  • then2
  • timeout end
    首先分析下asyncawait执行顺序,当我们调用async1的时候,会马上输出 async2 end,并且async2函数会返回一个Promise对象,接下来遇到await的时候,会让出线程执行函数async1外的函数,所以我们可以把await看作是让出线程的一个标示。

Event Loop执行顺序

  1. 首先执行同步代码,这属于宏任务。
  2. 执行完同步代码,执行栈为空,查询是否有异步代码需要执行。
  3. 执行所有的微任务
  4. 当执行完所有的宏任务,如果有必要会渲染也页面
  5. 然后开始下一轮的Event Loop,执行任务只的异步代码,也就是setTimeout中的回调函数

微任务包括:process.nextTickpromise.thenMutationObserver
宏任务包括:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

Event Loop 与 UI渲染关系

说说下面的代码,会看到几个颜色

// style 部分
<style>
.wrap {
    width: 500px;
    height: 500px;
    background-color: bisque;
}
</style>

// html部分
<div class="wrap"></div>


// js部分
document.querySelector('.wrap').style.backgroundColor = 'red';
setTimeout(() => {
    console.log('out setTimeout')
    document.querySelector('.wrap').style.backgroundColor = 'yellow';
}, 10000)

new Promise((resolve, reject) => {
    resolve(10000);
}).then(res => {
    for(let i = 0; i < 10000; i++) {}
    document.querySelector('.wrap').style.backgroundColor = 'pink';
    setTimeout(() => {
        document.querySelector('.wrap').style.backgroundColor = 'green';
    }, 5000);
    return '第一个'
}).then(res => {
    document.querySelector('.wrap').style.backgroundColor = 'blue';
});

会展示的颜色有三个:blue、green、yellow
解析:ui渲染需要等所有微任务执行完成再执行,微任务执行完成是设置的颜色是blue, 接下来就是两个宏任务,第一个setTimeout在10s后才注册回调,所以执行时间比第二个setTimeout完执行,所以先是green再是yellow