一、为什么JS要采用单线程
单线程就是同一时间只能干一件事。JS作为一门浏览器脚本语言,它主要的作用就是和用户交互,操作DOM。浏览器一次只分配给JS一个主线程,执行一个任务。如果同时有两个线程,一个线程在增添,另一个在删除,会造成混乱,所以JS只能是单线程的。
为了利用CPU的多核,HTML5提出Web Worker标准,它允许JS脚本同时开启多个线程,但只能有一个主线程,其他分线程听从主线程并且不能操作DOM。
二、同步与异步任务
2.1 什么是同步异步任务
同步任务指没有被JS引擎挂起,在主线程上执行的任务,只有前一个任务执行完,后一个任务才会执行。
异步任务是指被JS引擎挂起,没有进入主线程,进入任务队列的任务,只有JS引擎认为该任务可以执行了(比如Ajax请求从服务器返回了结果),才会进入主线程执行(采用回调函数形式),在异步任务之后的同步任务不用等待异步任务执行完再执行,因此异步任务不具有阻塞效应。
异步任务必须指定回调函数,在主线程上执行的就是对应的回调函数
2.2 哪些是异步任务
主要有:Ajax网络请求,DOM事件监听,setTimeOut(),setInterval()
注意:alert()是同步任务,如果不点击alert弹出框的确定按钮,其后的代码就不会执行
2.3 如何实现异步任务
任务队列:在JS引擎代码初始化阶段,会把包含回调函数的代码交给事件管理模块,当回调代码被触发执行(定时器时间到达指定时间,DOM事件被触发),放到任务队列里,等初始化代码(同步任务)执行完以后,就可以执行异步任务。
事件循环(Event Loop):JS引擎在执行完同步任务后,每隔一段时间就会检查这些在任务队列里的代码能否执行,这种循环检查的机制叫做事件循环。
2.4 node.js的事件循环
node.js的运行机制:
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
node.js也贡献了两个与任务队列相关的函数:process.nextTick(),setImmediate()
process.nextTick(): 在当前执行栈的尾部,下一次事件循环之前执行回调函数,且内部包含的 代码一次执行完,它会在所有异步任务执行之前执行
setImmediate(): 在当前任务队列的尾部添加一个回调函数,也就是加入的代码会在下一次事件循环时执行
小栗子1:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() { //异步代码
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
process.nextTick()里的回调代码会在异步代码执行之前,同步代码执行完毕后调用,且内部的回调函数一次性执行完毕,所以2在TIMEOUT FIRED之前输出。
小栗子2:
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
setTimeOut(fn,0)和setImmediate都是在下一次事件循环执行,但是谁先谁后呢?答案是不一定。
这时输出结果可能为1,TIMEOUT FIRED,2或者TIMEOUT FIRED,1,2
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2
若将setTimeOut和setImmediate同时放在setImmediate函数内,这时setImmediate代码一定在setTimeOut(fn,0)之前执行。则执行结果一定是1,TIMEOUT FIRED,2,因为setImmediate是在下一轮事件循环中执行,1和TIMEOUT FIRED是一轮,2在它们之后的一轮。
三、宏任务与微任务
3.1 什么是宏任务、微任务
宏任务和微任务是JS中处理等待任务队列中异步任务的机制
JS里有等待任务队列、同步任务队列,同步任务队列处理同步任务
3.2 哪些算宏任务、微任务
宏任务: 整体代码script,setTimeOut(),setInterval(),setImmediate()
微任务:promise.then(),process.nextTick(),async定义的函数里await下面的代码, Object.observe,Mutation.observer
3.4 执行顺序
都遵循微任务优于宏任务
首先JS会执行整体script代码,先执行同步代码,把宏任务放到宏任务队列中,微任务放到微任务队列中,执行完同步代码后,再从微任务队列依次取出微任务执行,执行完再依次从宏任务队列中取出宏任务执行,之后再执行微任务,宏任务,一直到执行完毕
注意:如果遇到微任务中包含宏任务,会先执行完微任务,再来执行包含着的宏任务。
小栗子1:
setTimeout(() => {
console.log('1');
new Promise(function (resolve, reject) {
console.log('2');
setTimeout(() => {
console.log('3');
}, 0);
resolve();
}).then(function () {
console.log('4') //执行微任务,执行完执行任务队列里的宏任务,输出6
})
}, 0);
console.log('5'); //5 7 10 8 1 2 4 6 3
setTimeout(() => { //宏任务队列中该定时器优于输出3的定时器执行
console.log('6');
}, 0);
new Promise(function (resolve, reject) {
console.log('7');
// reject();
resolve();
}).then(function () {
console.log('8')
}).catch(function () {
console.log('9')
})
console.log('10');
小栗子2:
async定义的函数,会默认返回一个promise实例,如果不是,会将它包装成一个promise对象
let p;
async function f3() {
p = await 18;
console.log(p);
}
f3();
console.log(1);
console.log(p);
// 结果是1,undefined,18
函数体内console.log(p)在await后面,变成微任务,先执行函数体外console.log(1), console.log(p), p未定义,输出undefined.
百度机考题:
以下代码的运行结果是?
const promiseA = Promise.resolve('a')
promiseA.then((res) => {
console.log(res)
}).then((res) => {
console.log(res)
})
const promiseB = Promise.resolve('b')
promiseB.then((res) => {
console.log(res)
})
promiseB.then((res) => {
console.log(res)
})
//输出:a,b,b,undefined