JS的同步和异步、微任务和宏任务

1,702 阅读6分钟

1.为什么JS是单线程的

js是运行于浏览器的脚本语言,因其经常涉及操作dom,如果是多线程的,如果一个线程修改dom,另一个线程删除dom,那么浏览器就不知道该先执行哪个操作,所以js执行的时候会按照一个任务一个任务来执行,那么任务是怎么排列的呢

2.为什么JS的任务要分类为同步任务和异步任务

试想一下,如果js的任务都是同步的,那么遇到定时器、网络请求等这类型需要延时执行回调函数的任务会发生什么?页面会像瘫痪一下暂停下来等待这些需要需要时间的代码执行完毕,基于此,又引入了异步任务,每个异步任务都必须引入

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

示例一我们可以看到既有同步任务也有异步任务,那么在主线程执行的过程中就会把两个定时器中的回调函数注册到异步事件队列里面,最后输出1 2 3 4,但是为什么是第二个setTimeout先输出,很明显从代码中可以看到是因为等待的时间不同,但是如果是网络请求呢,那时间不像是定时器一样提前规定好的,为了解决这个问题设置了消息队列,也可以理解为事件监听器

```
console.log(1) // 同步任务
setTimeout(() => { // 异步任务  
    console.log(4)
},  2000)
setTimeout(() => { // 异步任务  
    console.log(3)
},  1000)
console.log(2) // 同步任务
```

3.为什么异步任务中要设置消息队列(事件监听)

事件监听器可以监听异步任务的状态,如果已经可以执行回调就会将对应的任务放到事件队列中

  • 例子①

示例一假设了两个网络请求,监听器会先监听到第二个请求得到响应,那么会先执行第二个的回调,所以下面这段代码的输出是1 2 3 4

```
console.log(1) // 同步任务
ajax().then(() => { // 异步任务且假设3s后得到响应  
    console.log(4)
})
ajax().then(() => { // 异步任务且假设1s后得到响应  
    console.log(3)
})
console.log(2) // 同步任务

```

4.为什么异步任务的事件中要区分宏任务和微任务

假设如果异步任务的回调是在操作dom,但是页面渲染也是一个任务,如果把dom渲染的任务排到异步任务的队尾,那么页面同样会出现瘫痪,所以js就规定一些可以插队完成的任务,这类型的任务称为微任务,比如说dom渲染,后面又引入promise.then,

每个宏任务后面都会存在微任务,如下图所示,遇到一段代码,首先执行所有同步代码,然后再读取宏任务队列,拿上面的例子举例,console.log(1)和console.log(2)作为同步任务最先执行,然后执行宏任务1,第一个ajax,事件监听器会判断出哪一个事件先返回结果最终将回调函数放到主线程执行栈

  • 宏任务:HTML解析、鼠标事件、键盘事件、网络请求、执行主线程js代码和定时器(new Promise(xxx)中xxx是同步代码)
  • 微任务:promise.then,dom渲染,async,process.nextTick

5.下面通过几个例子理解宏任务和微任务

例子①:下面的执行结果是1 2 3,原因是1 2是同步任务,3是异步任务

```
console.log(1)
setTimeout(() => {  
    console.log(3)
})
console.log(2)

```

例子②:下面的执行结果是1 2 3 4,原因是1 2是同步任务,3 4是异步任务,且第二个定时器先执行回调

```
console.log(1)
setTimeout(() => {  
    console.log(4)
}, 2000)
setTimeout(() => {  
    console.log(3)
}, 1000)
console.log(2)

```

例子③:下面的执行结果是1 2 3 4 5,原因是1 2 3是同步任务,4 5是异步任务,且4是微任务,console.log(2)执行结束后宏任务队列中的第一个任务的微任务就是console.log(4),第二个宏任务是定时器,所以4先一步注入到执行栈

```
console.log(1)
new Promise((resolve, reject) => {  
    console.log(2)  
    resolve()
}).then(() => {  
    console.log(4)
})
setTimeout(() => {  
    console.log(5)
})
console.log(3)

```

例子④:先看例子思考哪些是同步任务 哪些是异步任务 哪些是宏任务和微任务

```
console.log(1)
new Promise((resolve, reject) => {  
    console.log("请求即将进行")  
    setTimeout(() => {    
        console.log("已请求到数据")    
        resolve()  
    },  1000)
}).then(() => {  
    console.log(3)
})
setTimeout(() => {  
    console.log(4)
},  2000)
console.log(2) // 执行结果1请求即将进行2已请求到数据34

```
  • 下图大概描述代码执行过程

例子⑤:例子4和例子5结合来看,我们知道了事件监听器的重要性,也可以把那俩个定时器看作是两个ajax请求,那么我们就能想到浏览器是怎么处理网络请求和同步代码之间的关系的

```
console.log(1)
new Promise((resolve, reject) => {  
    console.log("请求即将进行")  
    setTimeout(() => {    
        console.log("已请求到数据")    
        resolve()  
    },  2000)
}).then(() => {  
    console.log(3)
})
setTimeout(() => {  
    console.log(4)
},  1000)
console.log(2) // 与上一个例子的唯一区别是定时器设置的时间不同,
根据上图可知,上述代码的执行结果为1请求即将进行24已请求到数据3 

```

例子⑥:这个例子需要了解ES6的知识,async和promise是一样的道理

```
console.log('script start')
async function async1() { // 第一个promise  
    await async2()  
    console.log('async1 end')
}
function async2() {  
    console.log('async2 end')
}
setTimeout(function() {  // 第一个定时器  
    console.log('setTimeout')
})
async1()
new Promise(resolve => { // 第二个promise  
    console.log('promise1')  
    resolve()
}).then(function() {  
    console.log('promise2')
}).then(function() {  
    console.log('promise3')
})
new Promise(resolve => { // 第三个promise  
    console.log('promise4')  
    setTimeout(()=>{  // 第二个定时器    
        resolve()  
    })
}).then(function() {  
    console.log('promise5')
}).then(function() {  
    console.log('promise6')
})
console.log('script end')
// 执行结果
// script start async2 end promise1 promise4 script end  async1 end promise2  promise3  setTimeout promise5 promise6
```

图解,将执行整个js代码视为宏任务1,这样就可以理解第一个微任务队列是如何产生的

例子⑦:当前这个例子结合promise的特性和this指向问题

```
{
    var obj = {  
        A: function A() {    
              console.log(this.A)  
            },  
        B: () => {    
              console.log(this.B)  
            },  
        C:()=> {    
              console.log(this.C)  
            },  
        D: () => {    
              console.log(this.D)  
            },  
        E: 'E', 
        F: 'F'
    }
}
var A = 'A', B = 'B';
const C = 'C', D = 'D';
function test() {  
    obj.A(); 
    // 必然访问当前实例的A,即function A  
    setTimeout(obj.B) 
    // 放入异步 最后执行打印B 
    Promise.resolve().then(() => {      
        obj.C.bind(obj)() // C是箭头函数会bind会报undefined,因此打印undefined后会执行catch中的代码      
        return Promise.reject()    
    })
    .then(obj.D)    
    .catch(() => {      
        console.log(obj.E) // 打印E    
    })
    .then(() => {      
        console.log(obj.F) // 打印F   
     })
}
test(); // function A undefined E F B

```

6.总结JS运行机制

同步代码都在主线程上排队进行执行,异步任务的队列是受监听器调节的,每个异步任务都有回调且有一个微任务队列,没有对应的微任务即为空即可,js忍着秘籍是这样解释js的运行机制的,我觉得比较贴合自己的理解,下图展示一下

以上如果有理解错误,希望指出并互相学习