js中的同步和异步
同步
js是单线程的,浏览器只会分配一个js引擎线程,用来执行js代码,当其执行代码时,js一次只能执行一次事件,这就是js中的同步-
异步
异步是由浏览器任务队列的机制决定的:
我们说js单线程指的是浏览器分配给js执行时的js引擎线程是单线程的,一般也称为主线程,但浏览器本身是多进程的,其主要的进程是渲染进程,一个tab就是一个渲染进程,所以一个tab崩溃才不会影响别的tab。一个渲染进程会包括定时器进程、事件处理线程、js引擎线程等。
而js引擎线程遇到要异步执行的任务(比如定时器、事件绑定、Ajax、Promise、async await等),浏览器渲染进程会开启对应的线程去处理异步任务,当任务执行完成后会返回一个回调任务,这个回调任务就会存放到对应的任务队列event queue中,等待被js主线程同步调用;当浏览器同步任务都执行完后,浏览器渲染线程闲下来了,就会去任务队列按照指定的顺序领一个任务执行,当执行完后,会按照顺序领下一个任务,直到任务队列清空为止,这个过程,就是我们常说的事件循环
event loop任务队列
event queue中存放有两种:- 宏任务:定时器(即使设为
0,也是4ms后执行代码),简单的可以记为除以下微任务的都是宏任务 - 微任务:promise、 async await、promise.nextTice
微任务的优先级高于宏任务
- 宏任务:定时器(即使设为
常见的异步任务
Promise
其实Promise本身并不是异步执行的,当new Promise((resolve, reject) => {}),这个里面的函数(resolve, reject) => {}是立即执行的,它的异步体现在resolve()或reject()回调,不是立即通知then中的方法执行,而是等其处理完事情后,再把promise的状态改变,并通知then中的方法执行
new Promise((resolve, reject) => {
// 这里立即执行
// ...
resolve()
}).then(resolve => {
}, reason => {
})
generator
generator可以通过yield将函数的执行权交出去,然后通过调用next()方法执行一次回调
function* gen() {
let a = yield 111;
console.log(a)
let b = yield 222;
console.log(b)
let c = yield 333;
console.log(c)
}
let t = gen()
t.next(1) // 第一次执行,传递的参数无效,故无打印结果
t.next(2) // a输出2
t.next(3) // b输出3
t.next(4) // c输出4
async await
async await是generator(本质上是Promise)的语法糖,async对应的是generator中的*号;await对应的是yield,可以“暂停”异步方法的执行,直到拿到异步执行结果后,再以同步方式执行后面的代码。
async await也是异步编辑的终极方案,以同步的方式写异步。
下面例子中,async函数,也不是func函数本身是异步的,它会立即执行await对应的表达式,即函数func1(),然后看它的结果,await必须保证返回的是成功态,才会把下面代码执行,所以它的异步体现在:await下面的代码先不执行,等func1()返回成功才执行
async function func() {
// func1立即执行,但console.log(1111)要等func1的结果才能执行
await func1();
console.log(1111)
}
// 相当于
function func() {
// func1立即执行,但console.log(1111)要等func1的结果才能执行
new Promise(resovle => {
func1()
resolve()
}).then(res => {
console.log(1111)
})
}
关于异常
一般我们执行promise的话,使用try-catch是无法获取到异步代码抛出的异常的,一般需要在promise的catch中获取
但是使用await,就可以使用同步方式try-catch来获取错误了
(async function () {
try {
await interview(1)
await interview(2)
await interview(3)
console.log('smile');
}catch (e) {
return console.log(`cry at ${e}`)
}
})()
function interview(round) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(Math.random() > 0.5) {
resolve(round)
console.log('成功了');
}else {
reject(round)
}
}, 300)
})
}
上题目
说那么多,不如做题来理解,这道题整合了async/await、Promise、setTimeout、script几种类型
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');
}).then(function() {
console.log('promise3');
})
console.log('script end')
// chrome 89.0.4389.90(正式版本)输出结果如下,按微任务放置顺序执行:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// promise3
// setTimeout
// nodejs v10.15.3输出结果如下,会优先执行promise,再执行await后语句:
// script start
// async1 start
// async2
// promise1
// script end
// promise2
// promise3
// async1 end
// setTimeout
分析:
- async1,async2方法定义,先略过不管,执行
console.log('script start')输出script start - setTimeout设置间隔为
0,但是依然需要至少等4ms后才执行,然后放到宏任务队列,等待被执行 -
async1()方法执行,输出async1 start -
await async2()这句立即执行async2(),输出async2 -
console.log('async1 end')因为await async2,相当于Promise.resolve().then(res => console.log('async1 end')),所以被放到微任务队列 -
new Promise立即执行console.log('promise1'),输出promise1,resolve()后,then中的方法放入微任务队列 - 同步代码执行
console.log('script end'),输出script end - 这一轮后的结果:
宏任务:setTimeout
微任务:1.await async2后面的代码,2.promise2 - 这时候浏览器渲染线程空闲了,去任务队列中找任务,这时候就涉及到任务队列的优先级了,微任务先于宏任务这个顺序是必须的,但微任务队列的优先级却是不一定的:
一般来说,正常微任务执行顺序,是按谁先放置的谁就先执行,但是不同的v8版本或引擎版本对于它的处理会有所偏差
可以看到在chrome 89的版本中,是按照微任务放置顺序来执行的;但在nodejs 10版本中,却是promise的优先级较高,在nodejs 11之后的版本就基本趋于一致了 - 这里我们且先按顺序的来说明,先输出
async1 end - 这时,再取下一次微任务
promise2,输出promise2,这时产生一个promise3的微任务:
宏任务:setTimeout
微任务:1.promise3 - 微任务还是优先于宏任务,输出
promise3 - 最后输出宏任务
setTimeout
考查事件触发+异步结合
<body>
<button id="btn">按钮</button>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('1'))
console.log('2');
})
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('3'))
console.log('4');
})
</script>
</body>
当手动点击按钮时,打印顺序是什么?----2143
当使用btn.click()时,打印顺序是一样的吗?----2413
两者为什么不一样?

当使用手动点击时,其实相当于每个监听事件,都是一个独立的函数作用域,相当于:
function A() {
Promise.resolve().then(() => console.log('1'))
console.log('2');
}
function B() {
Promise.resolve().then(() => console.log('3'))
console.log('4');
}
btn.addEventListener('click', A)
btn.addEventListener('click', B)
// 手动点击后,相当于组合调用
A()
B()
当执行A(),生成一个独立的执行上下文EC(A),整个A()中的执行过程,可以看成是一个事件循环tick,在这个事件循环中,先执行同步代码2,再执行微任务1;在下一个事件循环tick,B()同理,所以结果是2143
当使用btn.click()时,相当于调用一个函数,这个函数会将所有事件监听的回调整合在同一个函数作用域内,也就是相当于:
function total() {
// A
Promise.resolve().then(() => console.log('1'))
console.log('2');
// B
Promise.resolve().then(() => console.log('3'))
console.log('4');
}
在这个函数作用域中,也是按顺序先执行同步代码2 => 4,然后再按顺序执行微任务1 => 3,所以结果为2413