JavaScript的异步编程
在javascript中异步编程是一项必备的技能,很多公司在面试的时候都会涉及到这部分的内容。其考察形式包括但不限于理论题、输出结果题以及编程题,甚至有的Node岗位还会问到其底层实现(libuv)。本文对javascript的异步编程所涉及到的一部分面试题结合其原理进行整理,务求覆盖到异步编程的每一个方面,方便以后查阅。
理论题
介绍一下事件循环
JavaScript是单线程的,当前任务执行完才能执行下一任务,如果当前任务执行时间过长会造成阻塞,为了解决该问题,js的运行时采用事件循环机制来解决这一问题。在
js中,把任务分为同步任务和异步任务。可以理解为,在执行同步任务的过程中,会产生一部分事件(如定时器创建,I/O等),这部分事件如果继续往下同步执行,就会阻塞后面代码的执行,为了解决这个问题,引入异步任务的概念,当前代码块执行完实际上是不包括代码本身产生的事件的,这些事件放后面在进行处理(具体处理流程见下面)。异步任务包括宏任务和微任务:
常见微任务:
Promise.then,Object.observe,MutationObserver,process.nextTick(Node 环境)。常见宏任务:
setTimeout,ajax,dom事件,setImmediate(Node 环境),requestAnimationFrame。
对异步任务的处理流程如下:
同步任务执行完毕后会开始从调用栈中去执行异步任务
优先执行微任务队列,当微任务队列清空后才会去执行宏任务
每次单个宏任务执行完毕后会去检查微任务队列是否为空,如果不为空会按照先入先出的原则执行微任务(微任务中也可以产生异步任务),待微任务队列清空后再执行下一个宏任务,如此循环往复。
JavaScript 中的异步编程解决方案
常用的方案有三种:回调函数、Promise、async await。
传统的JavaScript使用回调进行异步编程,即:在完成某个操作后,就会执行回调函数内地内容。不过,如果回调的层数一多,就会很麻烦,比如:
fs.readFile("a.txt", () => {
fs.writeFile("a.txt", "Hello World", () => {
console.log("Write file success!");
});
});这还是比较少的情况,嵌套一多直接头皮发麻,即所谓回调地狱。
Promise的出现解决了回调地狱的问题,链式调用增强了代码可读性,上面的代码可变为:
fs.readFileSync("a.txt").then(() => {
fs.writeFileSync("a.txt", "Hello World").then(() => {
console.log("Write file success!");
});
});不过代码变得冗杂了,语义化并不强。
async await是生成器函数的语法糖,使用这种方案可以将异步代码使用同步的写法。从结果上看,编码更加清晰明了。
输出结果题
题目 1
setTimeout(() => console.log(0));
new Promise((resolve) => {
console.log(1);
resolve(2);
console.log(3);
}).then((o) => console.log(o));
new Promise((resolve) => {
console.log(4);
resolve(5);
})
.then((o) => console.log(o))
.then(() => console.log(6));分析
创建计时器,此时计时器内传入函数放到宏任务队列中等待执行
new Promise(fn)中的fn是立即执行的,所以先依次输出1 3 4,then中的任务推入微任务队列中等待执行当前同步执行完成,会去检测微任务队列中是否有任务并按序执行,所以输出
2 5,此时第二个promise then产生了新的promise then任务,也是推入微任务队列中等待执行,之后会被执行,输出6执行宏任务队列中的任务,输出
0
题目 2
setTimeout(() => {
console.log("A");
Promise.resolve().then(() => {
console.log("B");
});
}, 1000);
Promise.resolve().then(() => {
console.log("C");
});
new Promise((resolve) => {
console.log("D");
resolve("");
}).then(() => {
console.log("E");
});
async function sum(a, b) {
console.log("F");
}
async function asyncSum(a, b) {
await Promise.resolve();
console.log("G");
return Promise.resolve(a + b);
}
sum(3, 4);
asyncSum(3, 4);
console.log("H");分析
首先分析当前代码块的同步代码:
setTimeout、Promise.resolve、new Promise、sum(3,4)、asyncSum(a + b)、console.log("H")。其中setTimeout会创建一个定时器,其回调会放到下一个宏任务执行;Promise.resolve、new Promise中的参数如果是函数,会被立即调用;sum(3,4)内没有异步语句,是直接执行的;asyncSum(a + b)执行await Promise.resolve()实际上等同如下代码:function asyncSum(a, b) { Promise.resolve().then(() => { console.log("G"); }); return Promise.resolve(a + b); }会创建微任务,后面语句等待改该微任务被调用后才会执行。所以第一趟输出应该是
D F H之后按序执行第一次产生的微任务队列中的逻辑(此处只展示输出字符部分):
console.log("C")、console.log("E")、console.log("G")。所以之后的输出应该是C E G最后在
3000ms后执行刚刚创建的定时器的回调,输出A。产生的微任务在定时器回调执行完后会被立即执行,输出B。最终结果:
D F H C E G A B
题目 3
Promise.resolve(console.log(0))
.then(() => {
console.log(1);
Promise.resolve(console.log(5))
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(6));
})
.then(() => console.log(2))
.then(() => console.log(7));分析
console.log(0)立即执行,输出0,将then中的函数推入微任务队列等待执行。按序执行微任务队列的事件:
console.log(1)、Promise.resolve(console.log(5)),输出1 5。将() => console.log(3)、() => console.log(2)推入微任务队列等待执行。按序执行微任务队列的事件:
() => console.log(3)、() => console.log(2),输出3 2,将() => console.log(4)、() => console.log(7)推入微任务队列等待执行。按序执行微任务队列的事件:
() => console.log(4)、() => console.log(7),输出4 7,将() => console.log(6)推入微任务队列等待执行。按序执行微任务队列的事件:
() => console.log(6)、输出6。结果:
0 1 5 3 2 4 7 6。
编程题
promise.map
实现一个 promise.map,进行并发数控制,有以下测试用例:
pMap([1, 2, 3, 4, 5], (x) => Promise.resolve(x + 1));
pMap([Promise.resolve(1), Promise.resolve(2)], (x) => x + 1);
// 注意输出时间控制
pMap([1, 1, 1, 1, 1, 1, 1, 1], (x) => sleep(1000), { concurrency: 2 });分析
首先分析用例的参数,第一个参数是一个数组;第二个参数是调用的方法;最后一个参数是配置对象,根据用例可知第三个参数要有一个属性concurrency来控制并发。如果只看前两个参数的话,其效果有点像Array.prototype.map。
实现
function pMap(argsArr, fn, config) {
let queue = [];
const next = () => {
if (queue.length > 0) {
queue.shift().then((_task) => _task());
}
};
const run = async (fn, resolve, args) => {
const result = await (async () => fn(...args))();
resolve(result);
try {
result;
} catch (e) {}
next();
};
return new Promise((resolve, reject) => {
const concurrency = (config || {}).concurrency;
if (!concurrency) {
Promise.all(
argsArr.map((item) =>
item instanceof Promise ? item.then(fn) : fn(item)
)
)
.then(resolve)
.catch(reject);
} else {
queue = argsArr.map(async (item) =>
run.bind(null, fn, resolve, [
item instanceof Promise ? await item : item,
])
);
for (let i = 0; i < concurrency; i++) {
queue[i].then((_task) => _task());
}
}
});
}上面的代码是我看别人公众号分享的,我自己也写了一版控制并发的pMap,不借助Promise.all实现,功能大致是相同的。下面是完整代码:
推荐开源项目
关于异步操作并发,在生产中可以使用下面的库:
https://github.com/sindresorhus/p-limit.githttps://github.com/sindresorhus/p-limit.git
其源码也是比较简单,40 多行,也是值得一看的。
Promise.all
分析
鉴于Promise.all是原生的功能,我们先看看这个函数做了什么,然后照猫画虎进行实现即可。下面是MDN上的描述:
Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set 都属于 ES6 的 iterable 类型)的输入,并且只返回一个
Promise实例,那个输入的所有 promise 的 resolve 回调的结果是一个数组。这个Promise的 resolve 回调执行是在所有输入的 promise 的 resolve 回调都结束,或者输入的 iterable 里没有 promise 了的时候。它的 reject 回调执行是,只要任何一个输入的 promise 的 reject 回调执行或者输入不合法的 promise 就会立即抛出错误,并且 reject 的是第一个抛出的错误信息。
对上面的文字的要点进行提取:
接收一个
promise的iterable类型只返回一个
Promise实例resolve的回调结果是一个数组,执行时机:所有输入的
promise的resolve回调都结束输入的
iterable里没有promise了的时候
reject回调执行时机:- 只要任何一个输入的
promise的reject回调执行或者输入不合法的promise就会立即抛出错误,并且reject的是第一个抛出的错误信息
- 只要任何一个输入的
实现
const PromiseAll = (taskArr) => {
return new Promise((resolve, reject) => {
const res = new Array(taskArr.length).fill(0);
let finishNum = 0;
taskArr.forEach((item, index) => {
if (item instanceof Promise) {
item
.then((_res) => {
res[index] = _res;
finishNum++;
if (finishNum === taskArr.length) resolve(res);
})
.catch(reject);
} else {
res[index] = item;
finishNum++;
if (finishNum === taskArr.length) resolve(res);
}
});
});
};Promise.race
分析
先看看MDN上关于Promise.race的描述:
Promise.race(iterable)方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
即该方法以iterable中最先响应的结果作为返回结果,无论成功和失败都是遵循这个规则。
实现
const PromiseRace = (taskArr) => {
return new Promise((resolve, reject) => {
const length = taskArr.length;
for (let i = 0; i < length; i++) {
if (taskArr[i] instanceof Promise) {
taskArr[i].then(resolve).catch(reject);
} else {
resolve(taskArr[i]);
}
}
});
};
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "one");
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "two");
});
PromiseRace([promise1, promise2]).then((value) => {
console.log(value);
// Both resolve, but promise2 is faster
});promisify
实现一个通用的函数,将回调风格的函数调用改成Promise风格的处理方式。
分析
回调风格的函数形如fn(...args,callback),promisify的返回的新函数的返回值应该是一个Promise对象,且这个对象的then传入的方法会当作原函数的回调被调用。
另外,传入的函数还要遵循nodeCallback的规范:
1、回调函数在主函数参数的位置是最后一个
2、回调函数的第一个参数是error
实现
const promisify = (fn) => {
return (...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, data) => (err ? reject(err) : resolve(data)));
});
};手撕 Promise
关于Promise的一切,可以在这里看到:Promise - JavaScript | MDN,看完后就可以开始着手如何攻克这个BOSS了。
简单版本 Promise 实现
最简版Promise基于setTimeout,实现了最基本的Promise对象创建以及then和catch的链式调用。下面按照顺序来对代码进行设计。
Promise 类设计
首先,Promise有三种状态:pending、fulfilled和reject。且状态的改变仅允许两种情形:pending => fulfilled、pending => reject。
我们一般会像下面代码一样去使用Promise:
new Promise((resolve, reject) => {
/**...**/
});可以看到传入构造函数的参数是一个函数,且该函数有两个参数resolve和reject。调用这两个函数会改变Promise对象的状态。我们在使用中还会往这两个函数里传参,作为成功执行的结果或者失败的原因。
根据以上信息,我们可以得出类的基本形状如下:
type PromiseStatus = "pending" | "fulfilled" | "rejected";
class MyPromise {
status: PromiseStatus = "pending";
value: any = null;
reason: any = null;
constructor(
fn: (resolve: (val?: any) => void, reject: (e?: any) => void) => any
) {
const resolve = (val?: any) => {};
const reject = (e: any) => {};
fn(resolve, reject);
}
}一个Promise对象还需要有then和catch方法:
type PromiseStatus = "pending" | "fulfilled" | "rejected";
class MyPromise {
status: PromiseStatus = "pending";
value: any = null;
reason: any = null;
constructor(
fn: (resolve: (val?: any) => void, reject: (e?: any) => void) => any
) {
const resolve = (val?: any) => {};
const reject = (e: any) => {};
fn(resolve, reject);
}
then = () => {};
catch = () => {};
}这样,一个基本的Promise类的结构就定义出来了,下面对内部的逻辑进行实现。
then、catch 方法实现
then()方法返回一个Promise(en-US)。它最多需要有两个参数:Promise 的成功和失败情况的回调函数如果忽略针对某个状态的回调函数参数,或者提供非函数 (nonfunction) 参数,那么
then方法将会丢失关于该状态的回调函数信息,但是并不会产生错误。如果调用then的Promise的状态(fulfillment 或 rejection)发生改变,但是then中并没有关于这种状态的回调函数,那么then将创建一个没有经过回调函数处理的新Promise对象,这个新Promise只是简单地接受调用这个then的原Promise的终态作为它的终态。
由此可以定义then方法结构:
then = (onFulfilled?: (res: any) => any, onRejected?: (e: any) => any) => {
return new MyPromise((resolve, reject) => {});
};因为then的两个参数都是可选参数,所以我们需要对传入参数进行处理:
then = (onFulfilled?: (res: any) => any, onRejected?: (e: any) => any) => {
const _onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value: any) => value;
const _onRejected =
typeof onRejected === "function"
? onRejected
: (reason: any) => {
throw reason;
};
return new MyPromise((resolve, reject) => {});
};传入then的两个函数仅在status变为非pending状态的时候,才有机会被调用。执行then时,当前状态若为fulfilled时调用onFulfilled;当前状态若为rejected时调用onRejected;当前状态若为pending时,我们需要在对象中维护一个变量用于暂存这两个回调,直到状态为pending时才进行调用。
实现到这一步,then方法就基本完成了。关于catch,实际上就是对then的调用,只不过第一个参数传undefined而已。
这时候的代码是这个样子的:
type PromiseStatus = "pending" | "fulfilled" | "rejected";
type CallbackItem = {
fn: any;
resolve: (val?: any) => void;
reject: (e?: any) => void;
};
class MyPromise {
status: PromiseStatus = "pending";
value: any = null;
reason: any = null;
callback: Record<"fulfilled" | "rejected", CallbackItem[]> = {
fulfilled: [],
rejected: [],
};
constructor(
fn: (resolve: (val?: any) => void, reject: (e?: any) => void) => any
) {
const resolve = (val?: any) => {};
const reject = (e: any) => {};
fn(resolve, reject);
}
then = (onFulfilled?: (res: any) => any, onRejected?: (e: any) => any) => {
const _onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value: any) => value;
const _onRejected =
typeof onRejected === "function"
? onRejected
: (reason: any) => {
throw reason;
};
return new MyPromise((resolve, reject) => {
switch (this.status) {
case "pending":
this.callback.fulfilled.push({
fn: _onFulfilled,
resolve,
reject,
});
this.callback.rejected.push({
fn: _onRejected,
resolve,
reject,
});
break;
case "fulfilled":
resolve(_onFulfilled(this.value));
break;
case "rejected":
reject(_onRejected(this.reason));
break;
}
});
};
catch = (onRejected?: (e: any) => any) => this.then(undefined, onRejected);
}完善 constructor
前面说到:
Promise有三种状态:pending、fulfilled和reject。且状态的改变仅允许两种情形:pending => fulfilled、pending => reject。
我们在constructor中的resolve和reject函数中实现status的变化以及一些额外的操作,包括以下几个要点:
当前状态不为
pending时,resolve和reject函数直接返回,不执行其他逻辑。如:new MyPromise((resolve, reject) => { resolve(10); resolve(20); reject(30); });Promise的状态改变以第一次resolve为准,其他无视掉。在
then方法执行时,如果状态为pending,会将onFulfilled和onRejected存入对应的队列中。我们需要在constructor中的resolve和reject逻辑中,在改变状态为非pending后,执行对应队列的任务(使用setTimeout模拟微任务的创建)。
下面是对上述要点的代码实现:
type PromiseStatus = "pending" | "fulfilled" | "rejected";
type CallbackItem = {
fn: any;
resolve: (val?: any) => void;
reject: (e?: any) => void;
};
class MyPromise {
status: PromiseStatus = "pending";
value: any = null;
reason: any = null;
callback: Record<"fulfilled" | "rejected", CallbackItem[]> = {
fulfilled: [],
rejected: [],
};
constructor(
fn: (resolve: (val?: any) => void, reject: (e?: any) => void) => any
) {
const resolve = (val?: any) => {
if (this.status !== "pending") return;
setTimeout(() => {
this.status = "fulfilled";
this.value = val;
this.callback.fulfilled.forEach(({ fn, resolve: _resolve }) =>
_resolve(fn(val))
);
});
};
const reject = (e: any) => {
if (this.status !== "pending") return;
setTimeout(() => {
this.status = "rejected";
this.reason = e;
this.callback.rejected.forEach(({ fn, reject: _reject }) =>
_reject(fn(e))
);
});
};
fn(resolve, reject);
}
}自此,一个粗糙版的、支持链式调用的Promise类就完成了。
完整代码
type PromiseStatus = "pending" | "fulfilled" | "rejected";
type CallbackItem = {
fn: any;
resolve: (val?: any) => void;
reject: (e?: any) => void;
};
class MyPromise {
status: PromiseStatus = "pending";
value: any = null;
reason: any = null;
callback: Record<"fulfilled" | "rejected", CallbackItem[]> = {
fulfilled: [],
rejected: [],
};
constructor(
fn: (resolve: (val?: any) => void, reject: (e?: any) => void) => any
) {
const resolve = (val?: any) => {
if (this.status !== "pending") return;
setTimeout(() => {
this.status = "fulfilled";
this.value = val;
this.callback.fulfilled.forEach(({ fn, resolve: _resolve }) =>
_resolve(fn(val))
);
});
};
const reject = (e: any) => {
if (this.status !== "pending") return;
setTimeout(() => {
this.status = "rejected";
this.reason = e;
this.callback.rejected.forEach(({ fn, reject: _reject }) =>
_reject(fn(e))
);
});
};
fn(resolve, reject);
}
then = (onFulfilled?: (res: any) => any, onRejected?: (e: any) => any) => {
const _onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value: any) => value;
const _onRejected =
typeof onRejected === "function"
? onRejected
: (reason: any) => {
throw reason;
};
return new MyPromise((resolve, reject) => {
switch (this.status) {
case "pending":
this.callback.fulfilled.push({
fn: _onFulfilled,
resolve,
reject,
});
this.callback.rejected.push({
fn: _onRejected,
resolve,
reject,
});
break;
case "fulfilled":
resolve(_onFulfilled(this.value));
break;
case "rejected":
reject(_onRejected(this.reason));
break;
}
});
};
catch = (onRejected?: (e: any) => any) => this.then(undefined, onRejected);
}
new MyPromise((resolve, reject) => {
resolve(10);
})
.then((o) => o * 10)
.then((o) => o + 10)
.then((o) => {
console.log(o);
});
new MyPromise((resolve, reject) => reject("Error")).catch((e) => {
console.log("Error", e);
});提示
20230312: 复习一下该部分的实现,重新写了一遍:
