js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。
举一个例子,如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
var i, t = Date.now()
for (i = 0; i < 100000000; i++) {}
console.log(Date.now() - t) // 238
像上面这样,如果排队是因为计算量大,CPU忙不过来,但是,如果是网络请求就不合适。因为一个网络请求的资源什么时候返回是不可预知的,这种情况再排队等待就不明智了。
所以出现了同步与异步。
同步
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
// 同步代码
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
fun1();
fun2();
输出会依次输入1,2,因为代码是从上到下依次执行,执行完fun1(),才继续执行fun2()。
异步
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
function fun3() {
console.log(3);
}
fun1();
setTimeout(function(){
fun2();
},0);
fun3();
依次输出1,3,2,因为我们会优先执行同步函数,然后在执行异步函数。
正是由于JavaScript是单线程的,而异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。
JavaScript的执行顺序:(重点)
1.先同步后异步。
2.异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列。
3.调用Promise 中的resolve,reject属于微任务队列,setTimeout属于宏任务队列。
那什么是微任务和宏任务?
异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。
宏任务
macrotask,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个macrotask执行结束后,在下一个macrotask 执行开始前,对页面进行重新渲染,流程如下:
macrotask->渲染->macrotask->...
宏任务包含:
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
微任务
microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
微任务包含:
Promise.then
Object.observe
MutaionObserver
process.nextTick(Node.js 环境)
Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
1.执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行。
2.检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列。
3.更新render(每一次事件循环,浏览器都可能会去更新渲染)。
4.重复以上步骤。
宏任务 > 所有微任务 > 宏任务,如下图所示:
从代码执行顺序的角度来看,程序最开始是按代码顺序执行代码的,遇到同步任务,立刻执行;遇到异步任务,则只是调用异步函数发起异步请求。此时,异步任务开始执行异步操作,执行完成后到消息队列中排队。程序按照代码顺序执行完毕后,查询消息队列中是否有等待的消息。如果有,则按照次序从消息队列中把消息放到执行栈中执行。执行完毕后,再从消息队列中获取消息,再执行,不断重复。
由于主线程不断的重复获得消息、执行消息、再取消息、再执行。所以,这种机制被称为事件循环。
用代码表示:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
如果当前没有任何消息queue.waitForMessage 会等待同步消息到达。
下面以一个实例来解释事件循环机制:
console.log(1)
div.onclick = () => {console.log('click')}
console.log(2)
setTimeout(() => {console.log('timeout')},1000)
1、执行第一行代码,第一行是一个同步任务,控制台显示1
2、执行第二行代码,第二行是一个异步任务,发起异步请求,可以在任意时刻执行鼠标点击的异步操作
3、执行第三行代码,第三行是一个同步任务,控制台显示2
4、执行第四行代码,第四行是一个异步任务,发起异步请求,1s后执行定时器任务
5、假设从执行第四行代码的1s内,执行了鼠标点击,则鼠标任务在消息队列中排到首位
6、从执行第四行代码1s后,定时器任务到消息队列中排到第二位
7、现在同步任务已经执行完毕,则从消息队列中按照次序把异步任务放到执行栈中执行
8、则控制台依次显示’click‘、‘timeout’
9、过了一段时间后,又执行了一次鼠标点击,由于消息队列中已经空了,则鼠标任务在消息队列中排到首位
10、同步任务执行完毕后,再从消息队列中按照次序把异步任务放到执行栈中执行
11、 则控制台显示’click’
异步过程
下面以一个实例来解释一次完整的异步过程:
div.onclick = function fn(){console.log('click')}
1、主线程通过调用异步函数div.onclick发起异步请求
2、在某一时刻,执行异步操作,即鼠标点击
3、接着,回调函数fn到消息队列中排队
4、主线程从消息队列中读取fn到执行栈中
5、然后在执行栈中执行fn里面的代码console.log(‘click’)
6、于是,控制台显示’click’
function fun1() {
console.log("1")
}
fun1()
setTimeout(function () {
console.log('2')
});
function fun3() {
console.log("3")
}
fun3()
var l4 = new Promise(function (resolve) {
for (var i = 0; i < 10000; i++) {
i == 99 && resolve();
}
}).then(function () {
console.log('4')
});
console.log('5');