说清楚js的eventloop、宏任务和微任务

1,025 阅读6分钟

名词解释

JavaScript是单线程的,也就是说,同一个时刻,JavaScript只能执行一个任务,其他任务只能等待(浏览器是多线程的)

  • 同步任务:同步任务不需要进行等待可立即看到执行结果,比如console
  • 异步任务:异步任务需要等待一定的时候才能看到结果,比如setTimeout、网络请求

事件循环(Event Loop) :是一个在 JavaScript 引擎等待任务、执行任务、进入休眠状态、等待更多任务这几个状态之间转换的无限循环

任务队列(task queue)

  • 一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合,分别有宏任务队列和微任务队列
  • 每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列
  • 只要异步任务有了运行结果,就在任务队列之中放置一个事件(一个callback)

宏任务和微任务

我们这里把异步任务分为宏任务和微任务(从老外那里翻译过来的,业内面试都是这么分的,大家就这么记就行了)。要严格一点来说,其实应该是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行,微任务也属于宏任务的一部分,具体什么情况可以自行百度)

// 宏任务((macro)task)包括

script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)

// 微任务(microtask)包括
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

你可能会问为什么为什么我们有了宏任务还需要微任务,因为宏任务太耗费性能,而微任务的执行效率很高,所以平常在开发的时候有异步事务就优先考虑微任务

补充说一下单线程的事

  • JS 是单线程的,也就是说执行 JS 代码的线程只有一个( JS 引擎线程),我们也叫主线程,那为什么浏览器可以同时执行异步任务呢,因为浏览器的渲染进程是多线程的,当需要执行异步任务的时候,浏览器会帮我们启动另外一个线程来执行该任务!
  • 浏览器中还有定时器线程、HTTP 请求线程等,这些任务不是用来执行 JS 代码的,主要用来执行其他的一些任务!
  • 比如主线程(JS 引擎线程)中碰到 AJAX 请求,就把这个任务交给 HTTP 请求线程去真正的发送请求,等到请求回来了之后,再将 callback 里需要执行的 JS 回调函数,交个 JS 引擎线程去执行,也就是说浏览器才是真正执行发送请求这个任务的角色,而 JS 只是负责执行最后的回调处理!
  • 所以这里的异步不是 JS 自身实现的,其实是浏览器为其提供的能力。
  • V8 引擎不会将微任务交给浏览器的其他线程处理,而是存在自己的一个队列中。

补充说一句,想要实现多线程,可以借助web worker技术,具体自行百度

执行流程

  1. js代码开始执行后,主线程执行栈中会把任务分为两类.
  2. 一类是同步任务, 一类是异步任务; 主线程执行栈优先执行同步任务
  3. 异步任务会被放入特定的处理程序中,满足条件(有执行结果)后,被放到消息(任务/事件)队列中(队列满足FIFO先进先出规则)
  4. 主线程执行栈中所有的同步任务执行完毕之后,通过事件循环去消息(任务/事件)队列中挑选优先满足条件的程序,放入主线程执行栈中执行。事件循环,周而复始,一直执行

而异步任务又分为宏任务和微任务,主线程执行栈中执行过程中会产生微任务,微任务优先于宏任务之前执行,所以执行顺序是同步代码-->微任务-->宏任务

这里微任务执行完毕后通常会由GUI线程接管dom的渲染工作,然后再开启下一个宏任务,进行下一次的loop

几个执行顺序的小案例

这几个案例,涵盖了js事件执行机制的内容,同时也有很多js你未关注过的知识点,先提前说明一下,建议看完再去测试

  • 遇到同步代码就直接执行,而宏任务和微任务产生就先加到了各自的事件队列中(加入但并不一定执行)不管是同步代码,宏任务还是微任务的执行过程中又会产生这三者中的某一个某几个,记得同步立即执行,异步加入队列(没有同步了优先执行微任务,再执行宏任务即可,记得队列的FIFO先进先出)
  • const res = await fn()这句代码的执行流程
// 先是执行fn()方法,然后就开启一个微任务,
// res就是then方法回调里面的结果,所以res和下面的log都属于微任务里面的代码
const res = await fn()
console.log(1)
  • 关于定时器的参数问题,可以放一个code或者一个function
function func(num) {
  return function () {
        console.log(num)
    };
}

// 1. 第一种写法
setTimeout(func(1))

// 表示,这里的func(1)会立即执行,返回一个函数,开启一个宏任务
setTimeout(function () {
        console.log(1)
    })

// 2.第二种写法 也是我们的常见写法
setTimeout(func,1000) 
//也表示开启了一个宏任务(异步事务)
setTimeout(function (num) {
  return function () {
        console.log(num)
    };
})
  • then和catch方法执行完毕后会返回一个新的Promise对象并且状态是fulfilled(resolved)

测试一 (小试牛刀)

setTimeout(() => {
    console.log('1')
}, 0)
console.log('2');

new Promise((resolve) => {
    console.log('3');
    resolve()
}).then(() => {
    console.log('4');
}).then(()=>{
    console.log('5')
})

console.log('6')

//结果顺序是:2 3 6 4 5 1

测试二 (大展身手)

setTimeout(() => {
    // #1
    new Promise(resolve => {
        resolve();
    }).then(() => {
        // #7
        console.log('test');
    });
    // #8
    console.log(4);
});

new Promise(resolve => {
    resolve();
    console.log(1)
}).then(() => {
    // #2
    console.log(3);
    Promise.resolve().then(() => {
        // #4
        console.log('before timeout');
        // return Promise.resolve(undefined); // 默认会返回这么一句话
    }).then(() => {
        // #5
        Promise.resolve().then(() => {
            // #6
            console.log('also before timeout')
        })
    })
})
console.log(2);
// #3
/* 
result: 2 3 before timeout also before timeout 4 test

同
3 √ 
8 √

微
2 √
4 √
5 √
6 √
7 √

宏
1 √

*/

测试三(大结局)

setTimeout(function() {
  // #1
    console.log(0);
});


new Promise((resolve, reject) => {
  // #2
    console.log(1);
    resolve();
}).then(() => {
  // #3
    // 执行此微的时候又会产生两个微
    console.log(2);
    new Promise((resolve, reject) => {
      // #6
        console.log(3);
        resolve();
    }).then(() => {
      // #7
        console.log(4);
    }).then(() => {
      // #9
        console.log(5);
    });
}).then(() => {
  // #8
    console.log(6);
});

new Promise((resolve, reject) => {
  // #4
    console.log(7);
    resolve();
}).then(() => {
  // #5
    console.log(8);
});
/* result:1 7 2 3 8 4 6 5 0

同
2 √
4 √
6 √

微
3 √
5 √
7 √
8 √
9 √

宏
1 √

*/

测试四(大大结局)

function func(num) {
  return function () {
        console.log(num)
    };
}
// #1
setTimeout(func(1));

async function async3() {
    await async4();
    // #3
    console.log(8);
}
async function async4() {
    // #2
    console.log(5)
}
async3();
function func2() {
    // #10
    console.log(2);
    async function async1() {
        await async2();
        // #12
        console.log(9) 
    }
    async function async2() {
        // #11
        console.log(5)
    }
    async1();
    // #13
    setTimeout(func(4))
}
// #4
setTimeout(func2);

// #5
setTimeout(func(3));

new Promise(resolve => {
    // #6
    console.log('Promise');
    resolve()
})
    .then(
        // #7
    () => console.log(6)) 
    .then(
        // #9
    () => console.log(7)); 
    // #8
console.log(0);
/* result:5 Promise 0 8 6 7 1 2 5 9 3 4

同
2 √
6 √
8 √
10 √
11 √


微
3 √
7 √
9 √
12 √

宏
1 √
4 √
5 √
13 √

*/