一道面试题 聊聊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属于宿主语言,怎么执行是宿主说了算的,每个浏览器对一个特性的执行可能会不相同,浏览器环境和node环境表现可能也会不相同, 本文只讨论浏览器环境。
2 关于异步任务执行原理(event loop)网上的解释和讨论也是多种多样的。本文从宏任务与微任务的角度进行说明。
1 宏任务与微任务
image.pngJs 中,有两类任务队列:宏任务队列(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] )
-
参数:
{Function} [callback]
{Object} [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了一个方法
以文档的例子进行说明:
-
首先我们改变了vm.msg的值,所以先触发了派发更新,如下图
响应式梳理.png
由图可知会把渲染watcher的执行逻辑先添加到callbacks里面
- 然后再是push,框架使用者的callback,
所以当执行flushCallbacks的时候,因为都是微任务(或者不支持promise都为宏任务),先执行渲染相关逻辑(value = this.getter.call(vm, vm) // 执行updateComponent()
),而且渲染为同步任务,然后再是执行用户的逻辑。
所以nextTick其实是主线任务(数据更新)-> 异步队列更新(dom) -> 用户定义异步任务队列执行
最后:参考
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