JavaScript深入之异步
我们先来看一段代码
Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("3");
setTimeout(function () {
console.log("setTimeout");
}, 0);
console.log("5");
结果是
这是为什么呢?接下来我们往下面去看。
1.单线程与多线程
单线程语言:JavaScript 的设计就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等),决定了它是一门单线程语言。因为如果有多个线程它们一起操作DOM那网页将会非常混乱。
console.log("1");
console.log("2");
console.log("3");
那如果一个任务的处理耗时(或者是等待)很久的话,如:网络请求、定时器、等待鼠标点击等,后面的任务也就会被阻塞,也就是说会阻塞所有的用户交互(按钮、滚动条等),会带来极不友好的体验。
console.log("1");
console.log("2");
setTimeout(()=>{
console.log("hunian")
},1000)
console.log("3");
console.log("4");
“hunian”在4后打印的
其实,JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的。
JS引擎线程
事件触发线程
定时触发器线程
异步http请求线程
GUI渲染线程
当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞。所以,所以一切javascript版的"多线程"都是用单线程模拟出来的。
我们总结一下这张导图
1.同步和异步任务分别进入不同的执行场所,同步的进入主线程,异步的进入Event Table并注册函数。
2.当指定的事情完成时,Event Table会将这个函数移入Event Queue。
3.主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
4.上述过程会不断重复,也就是常说的Event Loop(事件循环)。
2.同步与异步
上面说到了异步,JavaScript 中有同步代码与异步代码。
下面是同步例子
console.log("1");
console.log("2");
console.log("3");
console.log("4");
//1
//2
//3
//4
setTimeout(()=>{
console.log("0");
},1000)
console.log("1");
//1
//0
上面的 setTimeout 函数便不会立刻返回结果,而是发起了一个异步,setTimeout 便是异步的发起函数或者是注册函数,() => {…} 便是异步的回调函数。这里,JS引擎线程只会关心异步的发起函数是谁、回调函数是什么?并将异步交给 webapi 去处理,然后继续执行其他任务。
异步函数一般是网络请求,定时器,DOM时间监听。
我们再通过一个例子来看看这个执行过程
console.log("1");
setTimeout(() => {
console.log("2");
}, 1000);
console.log("3");
console.log("4");
//1
//3
//4
//2
1.主代码块(script)依次加入执行栈,依次执行,主代码块为:
console.log(‘1’)
setTimeout()
console.log(‘3’)
console.log(‘4’)
2.console.log() 为同步代码,JS引擎线程处理,打印 “1”,出栈;
3.遇到异步函数 setTimeout,交给定时器触发线程(异步触发函数为:setTimeout,回调函数为:() => { … }),JS引擎线程继续,出栈;
4.console.log() 为同步代码,JS引擎线程处理,打印 “3”,出栈;
5.console.log() 为同步代码,JS引擎线程处理,打印 “4”,出栈;
6.如果执行栈为空,也就是JS引擎线程空闲,这时从消息队列中取出(如果有的话)一条任务(callback)加入执行栈,并执行;这里就是输出“2”
7.重复第6步。
可以看出,setTimeout异步函数对应的回调函数( () => {} )会在执行栈为空,主代码块执行完了后才会执行。
3.宏任务与微任务
以上机制在ES5的情况下够用了,但是ES6会有一些问题。
Promise同样是用来处理异步的:
我们再来看最初的代码
Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("3");
setTimeout(function () {
console.log("setTimeout");
}, 0);
console.log("5");
//3
//5
//promise1
//promise2
//setTimeout
这里有一个新概念:宏任务 和 微任务。
宏任务:setTimeout、setInterva,Ajax和DOM等(可以看到,事件队列中的每一个事件都是一个 宏任务,现在称之为宏任务队列)
微任务:Promise、async和await等
同步任务毫无疑问先执行所以先打印出“3”,“5”,按照代码段顺序会将异步任务放到任务队列中,并且秉持先微再宏的原则,入主线程执行。则微任务promise1和promise2打印出来。然后再打印宏任务setTimeout。
手画下执行过程: