一道面試題 聊聊js異步

看一道筆試題(頭條)

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ù)

image.png

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í)行一次更新凡泣。


image.png

看一個(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ā)更新欧穴,如下圖


    響應(yīng)式梳理.png

    由圖可知會把渲染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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笑诅,一起剝皮案震驚了整個(gè)濱河市调缨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吆你,老刑警劉巖弦叶,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異早处,居然都是意外死亡湾蔓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門砌梆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來默责,“玉大人贬循,你說我怎么就攤上這事√倚颍” “怎么了杖虾?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長媒熊。 經(jīng)常有香客問我奇适,道長,這世上最難降的妖魔是什么芦鳍? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任嚷往,我火速辦了婚禮,結(jié)果婚禮上柠衅,老公的妹妹穿的比我還像新娘皮仁。我一直安慰自己,他們只是感情好菲宴,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布贷祈。 她就那樣靜靜地躺著,像睡著了一般喝峦。 火紅的嫁衣襯著肌膚如雪势誊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天谣蠢,我揣著相機(jī)與錄音粟耻,去河邊找鬼。 笑死漩怎,一個(gè)胖子當(dāng)著我的面吹牛勋颖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播勋锤,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼饭玲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叁执?” 一聲冷哼從身側(cè)響起茄厘,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谈宛,沒想到半個(gè)月后次哈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吆录,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年窑滞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,814評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哀卫,死狀恐怖巨坊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情此改,我是刑警寧澤趾撵,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站共啃,受9級特大地震影響占调,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜移剪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一究珊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挂滓,春花似錦苦银、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纺念。三九已至贝椿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陷谱,已是汗流浹背烙博。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烟逊,地道東北人渣窜。 一個(gè)月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像宪躯,于是被迫代替她去往敵國和親乔宿。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評論 2 351