看一道筆試題(頭條)
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');
});
console.log('script end');
答案:(瀏覽器端:chrome 75.0.3770.142)
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
解釋:
提前說明:
1 js屬于宿主語言夏伊,怎么執(zhí)行是宿主說了算的,每個(gè)瀏覽器對一個(gè)特性的執(zhí)行可能會不相同础淤,瀏覽器環(huán)境和node環(huán)境表現(xiàn)可能也會不相同, 本文只討論瀏覽器環(huán)境。
2 關(guān)于異步任務(wù)執(zhí)行原理(event loop)網(wǎng)上的解釋和討論也是多種多樣的熄守。本文從宏任務(wù)與微任務(wù)的角度進(jìn)行說明励幼。
1 宏任務(wù)與微任務(wù)
Js 中,有兩類任務(wù)隊(duì)列:宏任務(wù)隊(duì)列(macro tasks)和微任務(wù)隊(duì)列(micro tasks)雕什。宏任務(wù)隊(duì)列可以有多個(gè)缠俺,微任務(wù)隊(duì)列只有一個(gè)(瀏覽器為了能夠使得JS內(nèi)部(macro)task與DOM任務(wù)能夠有序的執(zhí)行,會在一個(gè)(macro)task執(zhí)行結(jié)束后贷岸,在下一個(gè)(macro)task 執(zhí)行開始前壹士,對頁面進(jìn)行重新渲染)。
宏任務(wù):script(全局任務(wù)), setTimeout, setInterval, setImmediate, I/O, UI 事件.
(消息隊(duì)列偿警,添加在執(zhí)行棧的尾部)
微任務(wù):process.nextTick, Promise, Object.observer, MutationObserver.
(作業(yè)隊(duì)列躏救, 優(yōu)先級高于宏任務(wù))
Event Loop 會無限循環(huán)執(zhí)行上面3步,這就是Event Loop的主要控制邏輯螟蒸。其中盒使,第3步(更新UI渲染)會根據(jù)瀏覽器的邏輯,決定要不要馬上執(zhí)行更新七嫌。畢竟更新UI成本大少办,所以,一般都會比較長的時(shí)間間隔抄瑟,執(zhí)行一次更新凡泣。
看一個(gè)例子
console.log('script start');
// 微任務(wù)
Promise.resolve().then(() => {
console.log('p 1');
});
// 宏任務(wù)
setTimeout(() => {
console.log('setTimeout');
}, 0);
var s = new Date();
while(new Date() - s < 50); // 阻塞50ms
/*
上面之所以加50ms的阻塞,是因?yàn)?setTimeout 的 delayTime 最少是 4ms. 為了避免認(rèn)為 setTimeout 是因?yàn)?ms的延遲而后面才被執(zhí)行的皮假,我們加了50ms阻塞鞋拟。
*/
// 微任務(wù)
Promise.resolve().then(() => {
console.log('p 2');
});
console.log('script ent');
/*** output ***/
// one macro task
script start
script ent
// all micro tasks
p 1
p 2
// one macro task again
setTimeout
2 async/await
概念: 一句話,async 函數(shù)就是 Generator 函數(shù)的語法糖惹资。(async 函數(shù)是非常新的語法功能贺纲,新到都不屬于 ES6,而是屬于 ES7褪测。目前猴誊,它仍處于提案階段潦刃,但是轉(zhuǎn)碼器 Babel 和 regenerator 都已經(jīng)支持,轉(zhuǎn)碼后就能使用懈叹。)
2.1 Generator 函數(shù)
概念:生成器對象是由一個(gè) generator function 返回的,并且它符合可迭代協(xié)議和迭代器協(xié)議乖杠。
列子:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
看一個(gè)例子:
執(zhí)行三次next返回什么?
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
// 執(zhí)行這三次返回什么?
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
2.2 async函數(shù)對 Generator 函數(shù)的改進(jìn)澄成,體現(xiàn)在以下四點(diǎn)胧洒。
(1)內(nèi)置執(zhí)行器。
Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器墨状,所以才有了co模塊卫漫,而async函數(shù)自帶執(zhí)行器。也就是說肾砂,async函數(shù)的執(zhí)行列赎,與普通函數(shù)一模一樣,只要一行镐确。
asyncReadFile();
上面的代碼調(diào)用了asyncReadFile函數(shù)包吝,然后它就會自動執(zhí)行,輸出最后結(jié)果辫塌。這完全不像 Generator 函數(shù)漏策,需要調(diào)用next方法,或者用co模塊臼氨,才能真正執(zhí)行掺喻,得到最后結(jié)果。
(2)更好的語義储矩。
async和await感耙,比起星號和yield,語義更清楚了持隧。async表示函數(shù)里有異步操作即硼,await表示緊跟在后面的表達(dá)式需要等待結(jié)果。
(3)更廣的適用性屡拨。
co模塊約定只酥,yield命令后面只能是 Thunk 函數(shù)或 Promise 對象,而async函數(shù)的await命令后面呀狼,可以是 Promise 對象和原始類型的值(數(shù)值裂允、字符串和布爾值,但這時(shí)會自動轉(zhuǎn)成立即 resolved 的 Promise 對象)哥艇。
(4)返回值是 Promise绝编。(本題重點(diǎn))
async函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。你可以用then方法指定下一步的操作十饥。
進(jìn)一步說窟勃,async函數(shù)完全可以看作多個(gè)異步操作,包裝成的一個(gè) Promise 對象逗堵,而await命令就是內(nèi)部then命令的語法糖秉氧。正常情況下,await命令后面是一個(gè) Promise 對象砸捏,返回該對象的結(jié)果谬运。如果不是 Promise 對象,就直接返回對應(yīng)的值垦藏。
到這里本題就全部解答完畢了:
同步任務(wù)>微任務(wù)>宏任務(wù)
3拓展: 驗(yàn)證:async是否真的是語法糖
源碼:
// 1.js
async function f() {
console.log(1)
let a = await 1
console.log(a)
let b = await 2
console.log(b)
}
方法一: typescript編譯
1 npm install -g typescript
2 tsc ./1.ts --target es6
輸出的結(jié)果:
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
function f() {
return __awaiter(this, void 0, void 0, function* () {
console.log(1);
let a = yield 1;
console.log(a);
let b = yield 2;
console.log(b);
});
}
結(jié)果和上面說的一樣。(ps: ts大法好!)
方法二: babel
配置比較復(fù)雜以后補(bǔ)充
4 變式:
變式1
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
上面代碼輸出什么?
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
5 拓展vue(vm.nextick)
Vue.nextTick( [callback, context] )
-
參數(shù):
{Function} [callback]
{Object} [context]
用法:
vm.msg = 'Hello'
// DOM 還沒有更新
Vue.nextTick(function () {
// DOM 更新了
})
在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)伞访。在修改數(shù)據(jù)之后立即使用這個(gè)方法掂骏,獲取更新后的 DOM。
以上為vue文檔對nextTick的介紹厚掷,主線程的執(zhí)行過程就是一個(gè) tick弟灼,而所有的異步結(jié)果都是通過 “任務(wù)隊(duì)列” 來調(diào)度。
下面來分析一下冒黑,為什么nextTick(callback)中callback執(zhí)行的時(shí)候dom一定更新好了?
看一下nextTick源碼:
// 省略部分
let useMacroTask = false
let pending = false
let callbacks = []
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending)
pending = true
if (useMacroTask) {
// updateListener的時(shí)候?yàn)閠rue
macroTimerFunc()
} else {
microTimerFunc()
// 其他情況走的微任務(wù)
// 它們都會在下一個(gè) tick 執(zhí)行 flushCallbacks田绑,flushCallbacks 的邏輯非常簡單,對 callbacks 遍歷抡爹,然后執(zhí)行相應(yīng)的回調(diào)函數(shù)掩驱。
// 執(zhí)行flushCallbacks的時(shí)候會執(zhí)行 pending = false, callbecks.length = 0
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
當(dāng)我們在某個(gè)方法中調(diào)用vm.nextTick的時(shí)候,向callback中push了一個(gè)方法
以文檔的例子進(jìn)行說明:
-
首先我們改變了vm.msg的值冬竟,所以先觸發(fā)了派發(fā)更新欧穴,如下圖
由圖可知會把渲染watcher的執(zhí)行邏輯先添加到callbacks里面
- 然后再是push,框架使用者的callback,
所以當(dāng)執(zhí)行flushCallbacks的時(shí)候泵殴,因?yàn)槎际俏⑷蝿?wù)(或者不支持promise都為宏任務(wù))涮帘,先執(zhí)行渲染相關(guān)邏輯(value = this.getter.call(vm, vm) // 執(zhí)行updateComponent()
),而且渲染為同步任務(wù),然后再是執(zhí)行用戶的邏輯。
所以nextTick其實(shí)是主線任務(wù)(數(shù)據(jù)更新)-> 異步隊(duì)列更新(dom) -> 用戶定義異步任務(wù)隊(duì)列執(zhí)行
最后:參考
http://www.ruanyifeng.com/blog/2015/05/async.html
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7
http://es6.ruanyifeng.com/#docs/async
https://ustbhuangyi.github.io/vue-analysis/reactive/next-tick.html#vue-%E7%9A%84%E5%AE%9E%E7%8E%B0