一道面試題 聊聊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í)行是宿主說了算的,每個瀏覽器對一個特性的執(zhí)行可能會不相同,瀏覽器環(huán)境和node環(huán)境表現(xiàn)可能也會不相同, 本文只討論瀏覽器環(huán)境。
2 關(guān)于異步任務(wù)執(zhí)行原理(event loop)網(wǎng)上的解釋和討論也是多種多樣的。本文從宏任務(wù)與微任務(wù)的角度進行說明。

1 宏任務(wù)與微任務(wù)

image.png

Js 中,有兩類任務(wù)隊列:宏任務(wù)隊列(macro tasks)和微任務(wù)隊列(micro tasks)。宏任務(wù)隊列可以有多個,微任務(wù)隊列只有一個(瀏覽器為了能夠使得JS內(nèi)部(macro)task與DOM任務(wù)能夠有序的執(zhí)行,會在一個(macro)task執(zhí)行結(jié)束后,在下一個(macro)task 執(zhí)行開始前,對頁面進行重新渲染)。

宏任務(wù):script(全局任務(wù)), setTimeout, setInterval, setImmediate, I/O, UI 事件.
(消息隊列,添加在執(zhí)行棧的尾部)
微任務(wù):process.nextTick, Promise, Object.observer, MutationObserver.
(作業(yè)隊列, 優(yōu)先級高于宏任務(wù))
Event Loop 會無限循環(huán)執(zhí)行上面3步,這就是Event Loop的主要控制邏輯。其中,第3步(更新UI渲染)會根據(jù)瀏覽器的邏輯,決定要不要馬上執(zhí)行更新。畢竟更新UI成本大,所以,一般都會比較長的時間間隔,執(zhí)行一次更新。


image.png

看一個例子

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的阻塞,是因為 setTimeout 的 delayTime 最少是 4ms. 為了避免認為 setTimeout 是因為4ms的延遲而后面才被執(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ù)
概念:生成器對象是由一個 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 }

看一個例子:
執(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ù)的改進,體現(xià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表示緊跟在后面的表達式需要等待結(jié)果。

(3)更廣的適用性。

co模塊約定,yield命令后面只能是 Thunk 函數(shù)或 Promise 對象,而async函數(shù)的await命令后面,可以是 Promise 對象和原始類型的值(數(shù)值、字符串和布爾值,但這時會自動轉(zhuǎn)成立即 resolved 的 Promise 對象)。

(4)返回值是 Promise。(本題重點)

async函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。你可以用then方法指定下一步的操作。

進一步說,async函數(shù)完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內(nèi)部then命令的語法糖。正常情況下,await命令后面是一個 Promise 對象,返回該對象的結(jié)果。如果不是 Promise 對象,就直接返回對應(yīng)的值。
到這里本題就全部解答完畢了:
同步任務(wù)>微任務(wù)>宏任務(wù)

3拓展: 驗證: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
配置比較復雜以后補充

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ù)之后立即使用這個方法,獲取更新后的 DOM。

以上為vue文檔對nextTick的介紹,主線程的執(zhí)行過程就是一個 tick,而所有的異步結(jié)果都是通過 “任務(wù)隊列” 來調(diào)度。
下面來分析一下,為什么nextTick(callback)中callback執(zhí)行的時候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的時候為true
      macroTimerFunc()
    } else {
      microTimerFunc()
     // 其他情況走的微任務(wù)
    // 它們都會在下一個 tick 執(zhí)行 flushCallbacks,flushCallbacks 的邏輯非常簡單,對 callbacks 遍歷,然后執(zhí)行相應(yīng)的回調(diào)函數(shù)。
// 執(zhí)行flushCallbacks的時候會執(zhí)行 pending = false, callbecks.length = 0
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

當我們在某個方法中調(diào)用vm.nextTick的時候,向callback中push了一個方法
以文檔的例子進行說明:

  • 首先我們改變了vm.msg的值,所以先觸發(fā)了派發(fā)更新,如下圖


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

    由圖可知會把渲染watcher的執(zhí)行邏輯先添加到callbacks里面

  • 然后再是push,框架使用者的callback,
    所以當執(zhí)行flushCallbacks的時候,因為都是微任務(wù)(或者不支持promise都為宏任務(wù)),先執(zhí)行渲染相關(guān)邏輯(value = this.getter.call(vm, vm) // 執(zhí)行updateComponent()),而且渲染為同步任務(wù),然后再是執(zhí)行用戶的邏輯。
    所以nextTick其實是主線任務(wù)(數(shù)據(jù)更新)-> 異步隊列更新(dom) -> 用戶定義異步任務(wù)隊列執(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容