一道面试题 聊聊js异步

2019-08-02  本文已影响0人  安石0

看一道笔试题(头条)

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属于宿主语言,怎么执行是宿主说了算的,每个浏览器对一个特性的执行可能会不相同,浏览器环境和node环境表现可能也会不相同, 本文只讨论浏览器环境。
2 关于异步任务执行原理(event loop)网上的解释和讨论也是多种多样的。本文从宏任务与微任务的角度进行说明。

1 宏任务与微任务

image.png

Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个(浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染)。

宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI 事件.
(消息队列,添加在执行栈的尾部)
微任务:process.nextTick, Promise, Object.observer, MutationObserver.
(作业队列, 优先级高于宏任务)
Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。


image.png

看一个例子

console.log('script start');

// 微任务
Promise.resolve().then(() => {
    console.log('p 1');
});

// 宏任务
setTimeout(() => {
    console.log('setTimeout');
}, 0);

var s = new Date();
while(new Date() - s < 50); // 阻塞50ms
/*
上面之所以加50ms的阻塞,是因为 setTimeout 的 delayTime 最少是 4ms. 为了避免认为 setTimeout 是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。
*/
// 微任务
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 函数就是 Generator 函数的语法糖。(async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。)
2.1 Generator 函数
概念:生成器对象是由一个 generator function 返回的,并且它符合可迭代协议迭代器协议
列子:

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 }

看一个例子:
执行三次next返回什么?

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
// 执行这三次返回什么?
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

2.2 async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

asyncReadFile();
上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义。

async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

(4)返回值是 Promise。(本题重点)

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
到这里本题就全部解答完毕了:
同步任务>微任务>宏任务

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
输出的结果:

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);
    });
}

结果和上面说的一样。(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] )

vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

以上为vue文档对nextTick的介绍,主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。
下面来分析一下,为什么nextTick(callback)中callback执行的时候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()
     // 其他情况走的微任务
    // 它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
// 执行flushCallbacks的时候会执行 pending = false, callbecks.length = 0
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

当我们在某个方法中调用vm.nextTick的时候,向callback中push了一个方法
以文档的例子进行说明:

最后:参考

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

上一篇下一篇

猜你喜欢

热点阅读