event loop 事件循环/事件轮询
首先,js是单线程运行的,异步要基于回调来实现。event loop
就是异步回调的实现原理。
js是如何执行的?
①从前到后,一行一行执行;
②如果某一行执行报错,则停止后面代码的执行;
③先把同步代码执行完,再执行异步;
看一个:
console.log('Hi');
setTimeout(function() {
console.log('夏洛');
}, 5000);
console.log('Bye');
event loop
总结:
①同步代码,一行一行放在Call Stack执行
②遇到异步,会先“记录”下,等待时机(定时、网络请求等)
③时机到了,就移动到Callback Queue
④如果Call Stack为空,即同步代码都执行完了,Event Loop开始工作
⑤轮询查找Callback Queue,如有则移动到Call Stack执行
⑥继续轮询查找(永动机一样)
(有时间上图)
ps: 异步(setTimeout
、ajax
等)使用回调,基于event loop,DOM事件也使用回调,也基于event loop
,但是DOM事件不是异步。
Promise有三种状态的变化
- 三种状态
pending
、resolved
、rejected
pending
->resolved
或pending
->rejected
这几个状态,一旦改变,就不会再变化,是不可逆的。 - 状态的表现和变化
pending
状态,不会触发then
和catch
。
resolved
状态,会触发后续的then
回调函数。
rejected
状态,会触发后续的catch
回调函数。 -
then
和catch
对状态的影响
两句hin牛逼的真言:
then
正常返回resolved
,里面如果有报错则返回rejected
。
catch
正常返回resolved
,里面如果有报错则返回rejected
。
是不是有点懵,好吧我懵了。但是看了下面结合这两句话我又好了:
const p1 = Promise.resolve().then(() => {
return 100;
});
console.log('p1', p1); // resolved 触发后续 then回调
p1.then(() => {
console.log('123'); // 会执行
})
// ------------------------------------------
const p2 = Promise.resolve().then(() => {
return new Error('then error');
});
console.log('p2', p2); // rejected 触发后续 catch回调
p2.then(() => {
console.log('456'); // 不会执行
}).catch((err) => {
console.log('err100', err); // 会执行
})
// ------------------------------------------
const p3 = Promise.reject('my error').catch(err => {
console.error(err);
});
console.log('p3', p3); // resolved 注意!触发 then 回调
p3.then(() => {
console.log(100); // 可以打印
})
// ------------------------------------------
const p4 = Promise.reject('my error').catch(err => {
throw new Error('catch error');
});
console.log('p4', p4); // rejected 触发 catch 回调
p4.then(() => {
console.log(200); // 不会打印出来
}).catch(() => {
console.log('some err'); // 可以打印
}) // resolved 因为catch没有报错,返回的是resloved,不是rejected
是不是有点明白了?记住then
和catch
都会返回reslove
和rejected
就好了。
下面来几道题巩固一下:
// 第一题
Promise.resolve().then(() => {
console.log(1); // 1
}).catch(() => {
console.log(2);
}).then(() => {
console.log(3); //2
});
// 1
// 3
// 最终的结果是一个resolved的promise
// 第二题
Promise.resolve().then(() => { // rejected
console.log(1); // 1
throw new Error('error1');
}).catch(() => { // resolved
console.log(2); // 2
}).then(() => {
console.log(3); // 3
}); // resolved
// 1
// 2
// 3
// 最终的结果是一个resolved的promise
// 第三题
Promise.resolve().then(() => { // rejected
console.log(1); // 1
throw new Error('error1');
}).catch(() => { // resolved
console.log(2); // 2
}).catch(() => {
console.log(3); // 不执行,因为上一个catch中没有报错,上一步返回的是resloved,所以这部的catch不执行
})
// 1
// 2
// 最终的结果还是一个resolved的promise
async/await
我们都知道,异步回调存在callback hell,promise
的then
和catch
的链式调用解决了callback hell,但是也是基于回调函数。而async
、await
是用同步的语法,写异步的代码啊,来彻底消灭回调函数。
async/await
和promise
的关系:
①执行async
函数,返回的是promise
对象,不管它后面的是一个值,还是一个promise
对象;
②await
相当于promise
的then
,但是它处理不了catch
;
③try...catch
可捕获异常,代替了promise
的catch
。
看一个async/await
的一个比较基础的例子,加深理解~
请写出console
的打印顺序:
async function async1() {
console.log('async1 start'); // 2
await async2();
console.log('async1 end'); // 5
await async3();
console.log('async2 end 2'); // 7
};
async function async2() {
console.log('async2'); // 3
};
async function async3() {
console.log('async3'); // 6
};
console.log('script start'); // 1
async1();
console.log('script end'); // 4
分析一波:
①函数从上之下依次执行,显示几个函数定义,不用管他,下面先打印“script start”;
②调用函数async1
,执行async1
中的代码,打印 “async1 start”;
③接着遇到await
函数,先执行await
后面的函数async2
,函数async2
中打印 “async2”;
④await
后面的所有代码,都可以看做是callback里的内容,即异步,(可以理解为我们把后面的所有的代码放进一个队列等待),所以我们这时还是执行同步代码,即执行最后一个console
,打印 “script end”;
⑤这时,所有的同步的代码都执行完啦,我们就要去刚才的队列里执行那些代码,把他们放入callback queue中,event loop去循环执行。所以这时候应该打印async1 end
;
⑥这时又来一个await
,我们还是先将其后面的函数async3
执行,这时打印 “async3”;
⑦await
后面的代码又异步了,我们又把它们放入队列里等待执行,然后我们执行同步代码。然鹅?已经木有同步代码啦,所以我们event loop一下,执行console,打印 “async2 end 2”。
for...of
for...in
以及forEach
、for
是同步遍历,这都是常规操作,国际惯例。
for...of
常用于异步的遍历
function muti(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num * num);
}, 1000)
})
}
const nums = [1, 2, 3];
// 同步循环
nums.forEach(async (i) => {
const res = await muti(i);
console.log(res); // 1秒后输出所有值
});
// 异步循环
(async function() {
for(let i of nums) {
const res = await muti(i);
console.log(res); // 先输出1 再输出4 再输出9
}
})();
宏任务和微任务
宏任务:setTimeout
、setInterval
、Ajax
、DOM
事件
微任务:Promise
、async/await
ps:微任务执行时机比宏任务要早
为什么呢?
先了解一下event loop
和DOM
渲染的关系:
其实在每次call stack清空(同步任务执行完),event loop将要启动时,都是DOM
重新渲染的机会,DOM
结构如有改变则重新渲染。然后再去触发下一次event loop。
宏任务在DOM
渲染后触发,如:setTimeout
微任务在DOM
渲染前触发,如:promise
从event loop
原理解释一下,为何微任务执行更早:
宏任务放入任务队列时,是放入callback queue中,微任务放入任务队列时,是放入一个叫micro task queue的队列中,二者不属于同一队列。同时,宏任务是由浏览器规定的,而微任务是由ES6语法规定的,二者不一样,存放的对方自然就不一样了。
所以,在event loop
机制中,当call stack清空时,要先执行当前的微任务,然后再尝试DOM
渲染,接着执行宏任务,最后触发event loop。
最后来一道面试题:
还是看console的输出顺序
async function async1() {
console.log('async1 start'); // 2
await async2();
console.log('async1 end'); // 微任务 6
}
async function async2() {
console.log('async2'); // 3
}
console.log('script start'); // 1
setTimeout(function() { // 宏任务
console.log('setTimeout'); // 8
}, 0)
async1();
// 初始化promise时,传入的函数会立刻被执行
new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() { // 微任务
console.log('promise2'); // 7
});
console.log('script end'); // 5