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
考查事件触发+异步结合
当手动点击按钮
时,打印顺序是什么?----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