Javascript语言是的一大特点是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会执行下一个任务.
为什么JavaScript是单线程的?根据阮一峰大神介绍,作为浏览器的脚本语言,JavaScript的主要用途是与用户互动,以及进行DOM操作.这决定了它只能是单线程.否则会带来很多复杂的同步问题.比如,JavaScript同时有2个线程,一个线程在DOM节点上添加内容,另一个线程删除了这个DOM节点,那么请问,这个时候浏览器应该以哪个线程为准呢?
为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制.并且还不能操作DOM,所以,这个新标准,并没有改变JavaScript是单线程的本质.
单线程就意味着,所有的任务都需要排队,前面一个任务结束,才会执行后一个任务.如果前一个任务耗时很长,后一个任务就不得不一直等着
任务分为2种:
同步任务是指:在主线程上排队执行的任务,只有当前一个任务执行完毕,才能执行后一个任务
var num = 0;
console.log('任务一');
for (let index = 0; index < 100000000; index++) {
num += index;
}
console.log(num);
console.log('任务二');
以上代码是一个同步任务.当任务一执行之后,进入for循环去计算,但是这个for循环计算需要很长的时间,所以不得不等着,只有当for循环计算完成之后,才可以去执行任务二,这种形式的任务,就是同步任务.
异步任务是指:不进入主线程,而是进入
任务队列
,只有任务队列
通知主线程,某个异步任务可以执行了,那么该任务才会进入主线程执行.
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这个异步任务不管写在哪里,都会先执行同步任务,再执行异步任务
同步任务执行也可以这么认为,因为它可以被视为没有异步任务的异步执行
setTimeout(function() { console.log(‘b’); }, 10)
console.log("1")
dom.onclick = function () { alert(123) }
console.log("2")
Javascript中的事件基本上都是异步的,上面代码,会先执行同步代码,打印结果1,2之后,等待触发点击事件后,才会执行,所以也是个异步任务
ajax请求,不用说了,都知道,它是异步请求数据
var promise = new Promise(function(resolve, reject) {//这里是同步任务
console.log(3);
resolve();
})
promise.then(function() {//这里是异步任务
console.log(4);
})
调用栈是一种后进先出的数据结构,当一个脚本执行的时候,js引擎会解析这段代码,并将其中同步代码按照执行顺序加入调用栈中,然后从头开始执行.
在谷歌浏览器中,我们F12调试的时候,可以看到右边的 Call Stack,也就是调用栈,所有的代码都会进出于这个调用栈.
后进先出的意思是:就像子弹壳装弹,一粒一粒的进去,但是打出来的时候,是从上面打出来的,最先压进去的最后弹出来,也就是说进去的顺序的123,打出来的顺序是321,这就是后进先出.
js引擎遇到一个异步任务之后,并不会一直等待其返回结果,而是会将这个任务交给浏览器的其他模块进行处理(以谷歌浏览器的webkit为例,是webcore模块) 继续执行调用栈中的其他任务.当一个异步任务返回结果后,js引擎会将这个任务加入与当前调用栈不同的另一个队列,我们称之为事件队列
也有叫"任务队列".
我们来解读下这个图:
call stack :当一个脚本执行的时候,js引擎会解析这段代码,并且将其中的同步代码按照执行顺序加入调用栈中,然后从头开始执行.
webcore module: js引擎遇到一个异步事件后并不会一直等待其返回结果,而是将这个事件挂起(其他模块进行处理),继续执行调用栈中的其他任务.一个异步事件返回结果后,js会将这个事件加入到事件队列.
task queue:被放入事件队列不会立刻执行其回调.而是等待当前执行栈中的所有任务都执行完毕,主线程处于闲置状态时,然后主线程会去查找事件队列中是否有任务,如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码,如此反复,这样就形成了一个无线的循环,这个过程被称为**事件循环(Event Loop)**
先执行微任务在执行宏任务
上述过程会不断重复,这就是Event Loop事件循环
上面说的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别.
不同的异步任务被分为两类:
微任务(micro task):
promise.then、promise.nextTick(node),MutationObserver(html5 新特性)
宏任务(macro task)
整体代码script、setTimeout、setInterval......
需要注意的是:new Promise是会进入到主线程中立刻执行,而promise.then则属于微任务
先执行整体的宏任务,再执行异步任务中的微任务,然后执行宏任务
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
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');
先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务
script start
async1 start
, 然后执行 async2(), 输出 async2
,把 async2() 后面的代码 console.log('async1 end')
放到微任务队列中promise1
,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数script end
。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码async1 end
、 promise2
, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout
最后的执行结果如下
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)
})
这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务
start
children4
, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的children2
,此时,会把 Promise.resolve().then
放到微任务队列中。children3
;然后开始执行宏任务②,即第二个 setTimeout,输出 children5
,此时将.then放到微任务队列中。children7
,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6
;最后的执行结果如下
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');
p1.then
会先放到微任务队列中,接着往下执行,输出 3
p().then
会先放到微任务队列中,接着往下执行,输出 end
p1.then
,输出 2
, 接着执行p().then
, 输出 4
resolve(1)
,但是此时 p1.then
已经执行完成,此时 1
不会输出。最后的执行结果如下