前言:
javascript是一种单线程编程语言, 一般来说它的执行顺序是按照从上到下执行,但是有些特殊情况则会改变这样的执行顺序,我们需要理解和掌握其中的原理,需要了解同步任务和异步任务以及宏任务和微任务,这样才有助于我们在今后开发中及时发现代码执行问题,提高开发效率。Javascript 是单线程语言,所以一切 Javascript “多线程” 都是单线程模拟出来的。
1.Javascript 中的同步任务和异步任务
同步任务:可以立即执行不需要等待,例如 var a = 123
异步任务:不能立即执行,需要等待一段时间后才能执行,例如 xhr网络请求 和 setTimeout定时器。
执行流程如下:
执行整个脚本,解析到同步和异步任务时分别放进不同的执行场所,同步任务放进主线程,异步任务放进事件表(Event Table)中并注册函数。
当指定的事情执行完成时,事件表(Event Table)会将这个函数移入事件队列(Event Queue)中等待执行。
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
不断重复整个过程,形成Event Loop(事件循环)。
举一个例子:
console.log('111') //同步任务 1
setTimeout(()=>{ //异步任务 1
console.log('222')
},3000)
setTimeout(() => { // 异步任务 2
console.log('333')
}, 1000)
console.log('444') //同步任务 2
答:111 444 333 222
对这个过程进行解析:
将同步任务1和2 放入主线程中,将异步任务1和2放入Event Table,并注册setTimeout的回调函数。
执行 主线程中的任务 按照从上到下的先后顺序 先输出 111 后 输出 444
当setTimeout定时结束后返回回调函数进入 Event Queue等待执行。
主线程执行完毕后从Event Queue读取setTimeout的回调函数放入主线程中并执行,因为两个异步任务执setTimeout执行时间不同,所以返回回调函数先后顺序也有所不同。
2.为什么异步任务中要设置队列(事件监听器)?
对于http、xhr等网络请求,执行的时间是不确定的,针对于这种情况设置了消息队列(事件监听器)。
事件监听器可以监听异步任务的状态,如果可以执行回调就会将相应的任务放入事件队列中。
举个例子:
假设两个网络请求,监听器会先监听到第二个请求得到响应,那么会先执行第二个的回调,所以下面这段代码的输出是1 2 3 4
console.log('111') // 同步任务1
ajax().then(() => { // 异步任务1
console.log('222')
})
ajax().then(() => { // 异步任务2
console.log('333')
})
console.log('444') // 同步任务2
发起请求的两个异步任务,通过事件监听器来监听,如果异步任务2先返回回调函数,异步任务1后返回回调函数,那么就先执行异步任务1后执行异步任务2,答案则是:111 444 333 222。
3.什么是宏任务和微任务?
如果javascript 按照真正的单线程执行的话,如果在遇到任务繁多的情况下,那么执行效率会降低,执行的时间也会更长,或者在解析到中间代码过程中遇到了错误的代码,那么就会停止在这一步影响后面代码的执行,这是非常不好的用户体验。为了解决这种传统意义上的单线程执行问题,在需要执行很多任务的这种情况下,就需要利用微任务和宏任务模拟产生“多线程”才能确保代码更高的执行效率。
宏任务和微任务是异步任务中的两个分类。
在ES6规范中 宏任务是由宿主发起的,微任务是由Javascript自身发起的。
4.宏任务和微任务区别是什么?
宏任务:由宿主(Node、浏览器等)发起,在微任务后运行,并会触发新一轮的 netxtTick()。
具体事件:
script (可以理解为外层同步代码)
setTimeout(定时器) / setInterval(计数器)
UI rendering / UI事件
postMessage,MessageChannel5. setImmediate,I/O(Node.js)
微任务:由JS引擎发起,在宏任务前运行,不会触发新一轮的 netxtTick()。
具体事件:
Promise
MutaionObserver
Object.observe(已废弃;Proxy 对象替代)
process.nextTick(Node.js)
5.宏任务和微任务执行顺序
执行顺序:
执行脚本,先执行同步代码,遇到异步宏任务则放入到宏任务队列中,遇到异步微任务则放入到微任务队列中,当所有同步代码执行完毕后,再将异步任务从队列中调入主线程执行,先将微任务执行完毕后再将宏任务从队列中调入主线程执行。
异步任务有宏任务和微任务两种,先将宏任务添加到宏任务队列中,将宏任务里面的微任务添加到微任务队列中,所有同步任务执行完后执行异步任务,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将宏任务从队列中调入主线程执行。这个过程一直循环执行(事件循环 Event loop)
6.案例题
例题一:
const promise1=new Promise((resolve, reject) => {
console.log('promise1')
})
console.log('1', promise1);
// 'promise1' '1' Promise{<pending>}
解析:
从上到下,先执行到 new Promise函数, 实例化过程中执行的代码是同步任务 输出:'promise1'。
人后执行到同步代码 console.log('1', promise1); 这个时候的promise1 没有被 resolve 或者。reject,因此状态函数pending,输出:'1' Promise{<pending>}。
例题二:
const promise =new Promise((resolve, reject) => {
console.log(1);
resolve('success')
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
解析:
从上至下,先遇到new Promise,执行其中的同步代码1。
再遇到resolve('success'), 将promise的状态改为了resolved并且将值保存下来。
继续执行同步代码2。
执行到promise.then这个微任务,将其加入微任务队列。
执行同步代码4。
本轮宏任务全部执行完毕,检查微任务队列,发现promise.then这个微任务且状态为resolved,执行它。
例题三:
setTimeout(function(){
console.log('1');
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3');
}).then(function(){
console.log('4')
});
console.log('5');
// 2 5 3 4 1
解析:
从上到下先执行到setTimeout,它是异步宏任务,放入到宏任务队列中。
执行到 new Promise 实例化过程中执行的代码是同步任务,放入主线程中执行,直接输出2。
promise.then中的回调函数是异步微任务,放入到微任务队列。
执行到 同步任务 console.log('5'); 放入主线程中执行,直接输出5,至此同步任务中的同步任务执行完成。
从微任务队列中取出微任务到主线程中,输出3、 4,微任务队列为空,然后检查宏任务队列。
从宏任务队列中取出宏任务到主线程中,输出1,宏任务队列为空。
例题四:
setTimeout(()=>{
new Promise(resolve=>{
resolve();
}).then(()=>{
console.log('test');
});
console.log(4);
});
new Promise(resolve=> {
resolve();
console.log(1)
}).then( () => {
console.log(3);
Promise.resolve().then(() => {
console.log('哈哈');
}).then(() => {
Promise.resolve().then(() => {
console.log('呵呵')
})
})
})
console.log(2);
// 1 2 3 哈哈 呵呵 4 test
解析:
从上到下执行到setTimeout,是异步宏任务,将setTimeout的回调函数放入宏任务队列。
遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1。
而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中。
遇到同步任务console.log(2),输出2;主线程中同步任务执行完。
从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中。
从微任务队列中取出任务a到主线程中,输出 哈哈。
从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中。
从微任务队列中取出任务c到主线程中,输出 呵呵;微任务队列为空。
从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空。
从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空。