JavaScript事件循环机制

86 阅读4分钟

1. 如何理解JS的异步

Javascript运行在浏览器的渲染主线程中,而渲染主线程只有一个,所以Javascript是单线程语言。

而主线程要渲染页面,执行JS等任务,如果使用同步的方式,就极有可能导致主线程阻塞,给用户造成卡死的现象。

所以浏览器采用异步的方式来避免。

2. Javascript为什么不设计成多线程语言?

浏览器中主要实现的是用户与浏览器的交互比如操作DOM,而这些操作必须在单一线程上执行,才能保证页面的安全和稳定。假如是多线程的,那么可能存在多个线程同时访问和修改同一个DOM元素,导致一些不可预料的问题。

3. 单线程的好处

好处是代码简单,易于调试。也可以降低浏览器的内存占用和CPU开销。虽然js是单线程,但是可以通过事件循环机制来处理异步任务,使得程序可以同时执行多个任务,而不阻塞主线程。

4.事件循环基础

javascript是一门单线程语言,分为同步任务和异步任务。

同步任务:是指在主线程上排队执行的任务,只有当前一个任务执行完成,才能继续执行下一个任务。(即不需要等待即可看到执行结果的)

异步任务:是指不进入主线程,而进入“任务队列”的任务;只有等主县城任务全部执行完,“任务队列”的任务才会进入主线程执行。(即需要等待一定的时候才能看到结果的)

异步任务分为:宏任务微任务

宏任务由宿主(Node、浏览器)发起。 微任务由JS引擎发起。

new Promise()、console.log()都属于同步任务。

  • 宏任务(macrotask):是指在主线程上执行的任务,包括:script(可理解为外层同步代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering
  • 微任务:是指在当前宏任务执行结束后立即执行的任务,包括:promise、Object.observe(已废弃,Proxy对象代替)、MutationObserve、process.nextTick(Node.js)

5. 浏览器执行顺序

同步任务---〉微任务---〉宏任务

  1. 首先执行所有的同步任务,
  2. 碰到宏任务放到宏任务队列中,
  3. 碰到微任务放到微任务队列中,
  4. 当所有的同步代码执行完毕后
  5. 再将异步微任务从队列中调入主线程执行,
  6. 微任务执行完后再将异步宏任务从队列调入主线程执行,
  7. 一直循环直到所有任务完成。

微任务的执行优先级高于宏任务,即微任务中的所有任务都会在下一个宏任务之前执行完毕。 当一个宏任务执行完毕后,会优先执行微任务中的所有任务,直到微任务队列为空,然后再从宏任务队列中取出一个任务,以此类推。

在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

6. 面试题

(1)请说出下面代码的执行顺序

console.log('a');
setTimeout(()=>{
    console.log('c');
},0)
new Promise((resolve)=>{
    console.log('resolve');
    resolve();
}).then(()=>{
    console.log('then');
}).then(()=>{
    console.log('catch');
})
console.log('b');

正确答案:a,resolve,b,then,catch,c。

(2)在看一道加深印象

console.log('a');
setTimeout(()=>{
    console.log('c');
},0)
new Promise((resolve)=>{
    console.log('resolve');
    resolve();
    new Promise((res)=>{
        console.log(1);
        res();
    }).then(()=>{
        console.log(2);
    })
}).then(()=>{
    console.log('then')
})
setTimeout(()=>{
    console.log('d');
    new Promise((resolve,reject)=>{
        console.log('e');
        resolve();
    }).then(()=>{
        console.log('f')
    })
})
console.log('b')

正确答案:a,resolve,1,b,2,then,c,d,e,f

(3)看一道包含async的

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(()=>{
    console.log('promise2');
})
console.log('script end');

正确答案: script start,async1 start,async2,promise1,script end,async1 end,promise2,setTimeout

注意⚠️

  1. async和await也属于异步任务中的微任务。
  2. promise内部遇到resolve和reject调用的时候,会继续执行后面的代码。
  3. 但是async内部遇到await的时候,只有当await右边的代码执行完成,才会执行后面的代码。此时后面的代码也可以算作微任务。

解析:

async function async1(){
    console.log('async1 start');  // 4. 执行过程中这里出现同步代码,所以直接执行【2】
    await async2();  // 5. 遇到await,执行await后面的代码即async2函数,await下面这行就放入微任务队列
    console.log('async1 end'); // 9.这行代码在微任务队列排第1,先进先出原则所以先执行【6】 
}
async function async2(){
    console.log('async2');  // 6. 这里是同步代码,直接执行【3】
}
console.log('script start');  // 1.这里是同步代码,直接执行【1】
setTimeout(function(){   // 2. 这里是异步宏任务,放入宏任务队列暂时不执行
    console.log('setTimeout'); // 11. 宏任务队列调取执行【8】
},0)
async1()    // 3. 这里调用了async1函数,去执行async1
new Promise(function(resolve){
    console.log('promise1') // 7. new一个对象是直接执行的,所以这里是【4】
    resolve()
}).then(()=>{
    console.log('promise2'); // 10.这个在微任务队列排第2,第1已经执行了,现在执行它【7】,发现微任务队列空了,再去执行宏任务
})
console.log('script end'); // 8. 这里是同步代码直接执行【5】,到此同步任务执行完成,去执行微任务

所以执行顺序为:

  1. script end
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. async1 end
  7. promise2
  8. setTimeout

event loop.png