kangkai124 / blog

开发笔记
https://kangkai124.github.io/blog/
MIT License
26 stars 4 forks source link

js event loop 事件循环 #24

Open kangkai124 opened 5 years ago

kangkai124 commented 5 years ago

image

参考:

  1. 第 10 题:常见异步笔试题
  2. 这一次,彻底弄懂 JavaScript 执行机制
kangkai124 commented 5 years ago

首先需要明白,JavaScript 是一门单线程语言,分同步任务和异步任务。

image

根据规范,事件循环是通过任务队列的机制来进行协调的。

一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。

setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。

宏任务

(macro)task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得 JS 内部 (macro)task 与 DOM 任务能够有序的执行,会在一个 (macro)task 执行结束后,在下一个 (macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

(macro)task 主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

微任务

microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

事件循环,宏任务,微任务的关系如图所示:

image

Promise和async中的立即执行

Promise 中的异步体现在thencatch中,所以写在Promise中的代码是被当做同步任务立即执行的。

await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

由于因为async await 本身就是 promise+generator 的语法糖。所以 await 后面的代码是 microtask。所以

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

等价于

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}
kangkai124 commented 5 years ago

举个栗子:

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')
    })
})
kangkai124 commented 5 years ago

首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:

image

然后进入第一轮时间循环:

image

上图是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。我们发现了process1then1两个微任务。

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。

那么第二轮时间循环从setTimeout1宏任务开始:

image

第二轮事件循环宏任务结束,我们发现有process2then2两个微任务可以执行。

第二轮事件循环结束,第二轮输出2,4,3,5。

第三轮事件循环开始,此时只剩 setTimeout2 了,执行。

image

第三轮事件循环宏任务执行结束,执行两个微任务process3then3

第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

kangkai124 commented 5 years ago

再举几个栗子吧

  1. 
    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');


2. 
```js
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');
  1. 
    async function async1() {
    console.log('async1 start');
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
    }
    async function async2() {
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout2')
    },0)
    }
    console.log('script start');

setTimeout(function() { console.log('setTimeout3'); }, 0) async1();

new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end');


4. 
```js
async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

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

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')
kangkai124 commented 5 years ago

image

注意 promise2async1 end 之前 image

image

image