Promise应用
Promise
在实践中的应用场景其实非常的多,比如最常见的使用场景,就是利用Promise
对一些http
请求库进行一些简单的封装。除此之外,Promise
还可以用来实现异步任务同步执行的任务队列,这个方法在很多场景下也有很大的用处。同时还有我非常喜欢的睡眠函数。
睡眠函数
先来说下睡眠函数吧,虽然在应用场景中使用的比较少。但当我们想在某个固定时间后,去执行某段逻辑,如果我们用比较传统的方式来实现的话,会是下面这样:
const log1 = () => {
console.log('log1');
};
const runCallbackAfterTimeout = (timeout, callback) => {
setTimeout(() => {
callback();
}, timeout);
};
runCallbackAfterTimeout(1000, log1); // 1s 后打印 1
复制代码
但是如果我们用睡眠函数的话,就可以规避掉回调的形式,同时结合 async
与 await
, 可以实现同步书写的方式来书写异步任务:
const sleep = (timeout) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timeout);
});
};
const log1 = () => {
console.log('log1');
};
const main = async () => {
await sleep(1000);
log1();
}
main(); // 1s 后打印 1
复制代码
同步任务队列
在实现异步任务队列之前,我们先来实现一个同步任务队列。
首先我们先来说下什么是同步任务队列,当我们需要同步的依次执行一系列的同步任务时(并且前一个任务的结果值,可以作为参数,传递给下一个任务),我们会将这一系列的任务放到的一个数组里面,并依次执行,我们将其称之为同步任务队列。值得注意的是,同步任务队列的第一个函数,是可以接受多个参数的,而之后的执行的函数,最多只能接受一个参数。
我们首先写一个不需要进行参数传递的版本:
const pipe = (...handlers) => {
return () => {
for (let i = 0; i < handlers.length; i++) {
handlers[i]();
}
};
};
const log1 = () => {
console.log('log1');
};
const log2 = () => {
console.log('log2');
};
const logs = pipe(log1, log2);
logs(); // log1 log2
复制代码
当然,我们也可以利用 JavaScript 原生的数组方法:
const log1 = () => {
console.log('log1');
};
const log2 = () => {
console.log('log2');
};
const pipe = (...handlers) => {
return () => {
handlers.forEach((handler) => {
handler();
});
};
};
const logs = pipe(log1, log2);
logs(); // log1 log2
复制代码
接着,我们来试试传递参数,其实传递参数也很简单,我们只需要用一个变量来保存每次每一步函数运行的结果值,而在最开始的函数入参时,我们只需要将初始参数赋值给这个变量即可:
const add = (m, n) => {
return m + n;
};
const square = (n) => {
return n * n;
};
const pipe = (...handlers) => {
return (...initArgs) => {
let args = initArgs; // 因为刚开始我们传入的参数不定,所以用剩余参数的语法将它存储为数组
for (let i = 0; i < handlers.length; i++) {
args = [handlers[i](...args)]; // 为了保持循环的统一性,我们将结果值也存放在数组中,但是其实后续的过程中数组中只有1个值,即上一步的返回值并且是下一步的入参值
}
return args[0];
};
};
const addAndSquare = pipe(add, square);
addAndSquare(1, 2); // 9
复制代码
同样的,我们也可以利用 JavaScript 原生的数组方法对其进行改写。因为我们一直用到了一个变量来缓存中间状态值,所以利用 reduce
方法来对其进行改写:
const add = (m, n) => {
return m + n;
};
const square = (n) => {
return n * n;
};
const pipe = (...handlers) => {
return (...initArgs) => {
return handlers.reduce((args, handler) => {
return [handler(...args)];
}, initArgs)[0];
};
};
const addAndSquare = pipe(add, square);
addAndSquare(1, 2); // 9
复制代码
异步任务队列
异步任务队列就是说我们在队列里的任务是异步的了。在经历过同步任务队列代码的洗礼过后,我相信我们就不用在写不接受参数的版本了,我们可以直接来写正常传递参数的版本。
首先,我们还是来写一个通过 for 循环实现的版本:
const sleep = (timeout) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timeout);
});
};
const addAsync = async (m, n) => {
await sleep(2000);
return m + n;
};
const squareAsync = async (n) => {
await sleep(2000);
return n * n;
};
const asyncPipe = (...handlers) => {
return async (...initArgs) => {
let args = initArgs;
for (let i = 0; i < handlers.length; i++) {
args = await handlers[i](...args);
args = [args];
}
return args[0];
};
};
const addAndSquareAsync = asyncPipe(addAsync, squareAsync);
const main = async () => {
const res = await addAndSquareAsync(1, 2);
console.log(res);
};
main(); // 4s 后打印 9
复制代码
这个版本的优势是写起来容易,读起来简单,但是在某些 eslint 配置中会报错。
同样的,我们可以来通过 reduce
来对其进行改写。不过这个版本会稍微有些复杂,需要对Promise
的基本功掌握好点。基础差的同学可以结合我前几篇文章再复习下。
那么开始:
// 省略其余代码
const asyncPipe = (...handlers) => {
return (...initArgs) => {
const handlersList = handlers.slice(1);
return handlersList.reduce((p, handler) => {
return p.then((nextArgs) => {
return handler(nextArgs);
});
}, handlers[0](...initArgs));
};
};
复制代码
这里和同步任务队列不一样的对方在于我们需要先单独处理下进入reduce
函数的初始值,使其为第一个方法的结果值。因为我们在后续迭代过程中用到的变量p
其实是一个Promise
对象。
并且,因为我们初始入参有可能是多个参数,所以利用Promise.resolve(initArgs)
来当作reduce
的初始值也是不合理的,因为初始传入的 initArgs
是数组,而这必须保证后续的 nextArgs
依然是数组,但是handler(nextArgs)
的结果是一个Promise
对象。
其实这种写法就是类似于将一个长长的Promise
链的构造代码压缩在一个循环中了,严格意义上和 for 循环版本的思路并不一样。
网络请求
Promise
还可以对网络请求进行一些简单的封装,比如当下流行的axios
库,当我们在使用的时候,往往会在上层进行一层简单的封装,把 code
是否等于0的逻辑抽离出来,然后暴露给业务层成功的数据或者失败的信息。
这部分的代码今天就先不写了,我个人能想到的就有三种思路,可以提供给大家,然后大家顺着思考下:
- 利用
new Promise
构造函数; - 利用
Promise.prototype
.then 方法; - 利用
async
与await
处理。