事件循环机制 Event Loop

事件循环机制 Event Loop

文章目录

    • 事件循环机制 Event Loop
      • 一.JavaScript是单线程
        • 1.为什么JavaScript是单线程
        • 2.让JavaScript拥有多线程
      • 二.任务队列
        • 1.同步任务(synchronous)
        • 2 异步任务(asynchronous)
          • 2.1 异步任务的执行机制
        • 3.Javascript中的异步任务
          • 3.1 setTimeout和 setInterval
          • 3.2 dom事件
          • 3.3 ajax
          • 3.4 promise
        • 4.调用栈 Call Stack(执行栈)
        • 5.事件队列(Task Queue)
      • 三.事件循环机制
      • 四.微任务和宏任务
      • 五.示例代码解读
        • 1.示例一
        • 2.示例二
        • 3.示例三
        • 4.示例四

一.JavaScript是单线程

Javascript语言是的一大特点是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会执行下一个任务.

1.为什么JavaScript是单线程

​ 为什么JavaScript是单线程的?根据阮一峰大神介绍,作为浏览器的脚本语言,JavaScript的主要用途是与用户互动,以及进行DOM操作.这决定了它只能是单线程.否则会带来很多复杂的同步问题.比如,JavaScript同时有2个线程,一个线程在DOM节点上添加内容,另一个线程删除了这个DOM节点,那么请问,这个时候浏览器应该以哪个线程为准呢?

2.让JavaScript拥有多线程

​ 为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制.并且还不能操作DOM,所以,这个新标准,并没有改变JavaScript是单线程的本质.

二.任务队列

单线程就意味着,所有的任务都需要排队,前面一个任务结束,才会执行后一个任务.如果前一个任务耗时很长,后一个任务就不得不一直等着

任务分为2种:

1.同步任务(synchronous)

同步任务是指:在主线程上排队执行的任务,只有当前一个任务执行完毕,才能执行后一个任务

var num = 0;
console.log('任务一');
for (let index = 0; index < 100000000; index++) {
    num += index;
}
console.log(num);
console.log('任务二');

以上代码是一个同步任务.当任务一执行之后,进入for循环去计算,但是这个for循环计算需要很长的时间,所以不得不等着,只有当for循环计算完成之后,才可以去执行任务二,这种形式的任务,就是同步任务.

2 异步任务(asynchronous)

异步任务是指:不进入主线程,而是进入任务队列,只有任务队列通知主线程,某个异步任务可以执行了,那么该任务才会进入主线程执行.

console.log('任务一');
setTimeout(() => {
    console.log('任务二');
}, 3000);
console.log('任务三');
//打印结果:任务一 > 任务三 > 任务二

以上代码是一个异步任务.不按顺序执行,同时执行多个任务.因为setTimeout是一个异步任务,所以会先执行同步任务,然后再执行异步任务

所以呢,可以得到一个结论:同步任务和异步任务同时存在时,一定先执行完同步任务再执行异步任务.

setTimeout(() => {
    console.log('任务一');
}, 0);
console.log('任务二');
var num = 0;
for (let index = 0; index < 100000000; index++) {
    num += index;
}
console.log(num);
console.log('任务三');

执行结果:
任务二
4999999950000000
任务三
任务一

从上面代码可以看出,setTimeout这个异步任务不管写在哪里,都会先执行同步任务,再执行异步任务

2.1 异步任务的执行机制

同步任务执行也可以这么认为,因为它可以被视为没有异步任务的异步执行

  • 所有同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,还存在一个"任务队列",只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件.哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
  • 主线程不断的重复上面这一步

3.Javascript中的异步任务

3.1 setTimeout和 setInterval
setTimeout(function() { console.log(‘b’); }, 10)
3.2 dom事件
console.log("1")
dom.onclick = function () { alert(123) }
console.log("2")

Javascript中的事件基本上都是异步的,上面代码,会先执行同步代码,打印结果1,2之后,等待触发点击事件后,才会执行,所以也是个异步任务

3.3 ajax

ajax请求,不用说了,都知道,它是异步请求数据

3.4 promise
var promise = new Promise(function(resolve, reject) {//这里是同步任务
    console.log(3);
    resolve();
})
promise.then(function() {//这里是异步任务
    console.log(4);
})

4.调用栈 Call Stack(执行栈)

调用栈是一种后进先出的数据结构,当一个脚本执行的时候,js引擎会解析这段代码,并将其中同步代码按照执行顺序加入调用栈中,然后从头开始执行.

在谷歌浏览器中,我们F12调试的时候,可以看到右边的 Call Stack,也就是调用栈,所有的代码都会进出于这个调用栈.

后进先出的意思是:就像子弹壳装弹,一粒一粒的进去,但是打出来的时候,是从上面打出来的,最先压进去的最后弹出来,也就是说进去的顺序的123,打出来的顺序是321,这就是后进先出.

5.事件队列(Task Queue)

​ js引擎遇到一个异步任务之后,并不会一直等待其返回结果,而是会将这个任务交给浏览器的其他模块进行处理(以谷歌浏览器的webkit为例,是webcore模块) 继续执行调用栈中的其他任务.当一个异步任务返回结果后,js引擎会将这个任务加入与当前调用栈不同的另一个队列,我们称之为事件队列也有叫"任务队列".

三.事件循环机制

事件循环机制 Event Loop_第1张图片

我们来解读下这个图:

call stack :当一个脚本执行的时候,js引擎会解析这段代码,并且将其中的同步代码按照执行顺序加入调用栈中,然后从头开始执行.

webcore module: js引擎遇到一个异步事件后并不会一直等待其返回结果,而是将这个事件挂起(其他模块进行处理),继续执行调用栈中的其他任务.一个异步事件返回结果后,js会将这个事件加入到事件队列.

task queue:被放入事件队列不会立刻执行其回调.而是等待当前执行栈中的所有任务都执行完毕,主线程处于闲置状态时,然后主线程会去查找事件队列中是否有任务,如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码,如此反复,这样就形成了一个无线的循环,这个过程被称为**事件循环(Event Loop)**

事件循环机制 Event Loop_第2张图片

  • 整体的script(作为第一个宏任务),开始执行的时候,会把所有代码分为两部分:同步任务和异步任务
  • 同步任务会直接进入主线程依次执行
  • 异步任务会再分为宏任务和微任务
  • 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移入大Event Queue中
  • 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
  • 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务.所以,先执行微任务在执行宏任务

上述过程会不断重复,这就是Event Loop事件循环

四.微任务和宏任务

上面说的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别.

不同的异步任务被分为两类:

微任务(micro task):
promise.then、promise.nextTick(node),MutationObserver(html5 新特性)
宏任务(macro task)
整体代码script、setTimeout、setInterval......

需要注意的是:new Promise是会进入到主线程中立刻执行,而promise.then则属于微任务

先执行整体的宏任务,再执行异步任务中的微任务,然后执行宏任务

五.示例代码解读

1.示例一

console.log(1);
var timer = setTimeout(function () {//异步任务的宏任务
    console.log(2);
}, 0)
console.log(timer);//延时器的id 值为1
var promise = new Promise(function (resolve, reject) {//同步任务
    console.log(3);
    resolve();
})
promise.then(function () { //异步任务的微任务
    console.log(4);
})
console.log(5);
//1,1,3,5,4,2

2.示例二

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

先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务

  1. 从上往下执行代码,先执行同步代码,输出 script start
  2. 遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
  3. 执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
  4. 接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数
  5. 接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
  6. 依次执行微任务中的代码,依次输出 async1 endpromise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout

最后的执行结果如下

  • script start
  • async1 start
  • async2
  • promise1
  • script end
  • async1 end
  • promise2
  • setTimeout

3.示例三

console.log('start');
setTimeout(() => {
    console.log('children2');
    Promise.resolve().then(() => {
        console.log('children3');
    })
}, 0);

new Promise(function(resolve, reject) {
    console.log('children4');
    setTimeout(function() {
        console.log('children5');
        resolve('children6')
    }, 0)
}).then((res) => {
    console.log('children7');
    setTimeout(() => {
        console.log(res);
    }, 0)
})

这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务

  1. 从上往下执行代码,先执行同步代码,输出 start
  2. 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列①中
  3. 接着往下执行,输出 children4, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的
  4. 代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出 children2,此时,会把 Promise.resolve().then放到微任务队列中。
  5. 宏任务①中的代码执行完成后,会查找微任务队列,于是输出 children3;然后开始执行宏任务②,即第二个 setTimeout,输出 children5,此时将.then放到微任务队列中。
  6. 宏任务②中的代码执行完成后,会查找微任务队列,于是输出 children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6;

最后的执行结果如下

  • start
  • children4
  • children2
  • children3
  • children5
  • children7
  • children6

4.示例四

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 0)
            resolve(2)
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) => {
    console.log(res);
})
console.log('end');
  1. 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3
  2. 遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end
  3. 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4
  4. 微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时 1不会输出。

最后的执行结果如下

  • 3
  • end
  • 2
  • 4

你可能感兴趣的:(JavaScript)