回调地狱
function loadScript(src, cb) {
const script = document.createElement("script");
script.src = src;
// 这里约定第一个参数是处理error
script.onload = () => cb(null, script);
script.onerror = () => cb(new Error(`Script load error for ${src}`));
document.head.appendChild(script);
}
loadScript("./1.js", (err, script) => {
if (err) {
throw Error(err);
return;
}
console.log("文件加载完毕啦~", script);
loadScript("./2.js", (err, script) => {
if (err) {
throw Error(err);
return;
}
console.log("第二个文件加载完毕!", script);
});
});
- 回调函数默认第一个参数是error
-
Promise
基本使用
Promise为我们解决了什么问题? 在传统的异步编程中,如果异步之间存在依赖关系,就需要通过层层嵌套回调的方式满足这种依赖,如果嵌套层数过多,可读性和可以维护性都会变得很差,产生所谓的“回调地狱”,而 Promise 将嵌套调用改为链式调用,增加了可阅读性和可维护性。也就是说,Promise 解决的是异步编码风格的问题。 那 Promise 的业界实现都有哪些呢? 业界比较著名的实现 Promise 的类库有 bluebird、Q、ES6-Promise。
Promise 本质是一个状态机,每个 Promise 有三种状态:pending、fulfilled 以及rejected。状态转变只能是pending —> fulfilled 或者 pending —> rejected,fulfilled和rejected会成为settled。状态转变不可逆。
- then 方法可以被同一个 promise 调用多次。
- then 方法必须返回一个 promise。规范2.2.7中规定, then 必须返回一个新的 Promise
- 值穿透 => 链式调用
- finally中没有参数
- catch相当于.then(null,()=>{})
var p = new Promise(function(resolve, reject) {
if(/* good condition */) {
resolve('Success!');
}
else {
reject('Failure!');
}
});
p.then(function(data) {
/* do something with the result */
}).catch(function(error) {
/* error :( */
}).finally(()=>{
/* cleanup */
})
promise.then(f1).catch(f2);
promise.then(f1, f2);
二者相同吗?
不相同:如果 f1 中出现 error,那么在这儿它会被 .catch 处理,而f2不会。
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = src;
script.onload = () => resolve(script);
script.onerror = () =>
reject(new Error(`Script load error for ${src}`));
document.head.appendChild(script);
});
}
let promise = loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"
);
promise
.then((script) => console.log(`${script.src} is loaded!`))
.then((script) => loadScript("./1.js"))
.then((script) => loadScript("./2.js"))
.then((script) => console.log("全部加载完毕~!"))
.catch((err) => console.log(err));
//https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js is loaded!
// Another handler...
为了使用loadScript.then这种形式,loadScript需要返回Promise,并且在onload和onerror中分别resolve和reject
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
})
.then((result) => {
console.log(result); // 1
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
})
.then((result) => {
console.log(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
})
.then((result) => {
console.log(result); // 4
return result * 2;
})
.then((result) => console.log(result));
为了使链式可以延续,好的做法是每个then之后,都返回Promise
错误处理-catch
new Promise((resolve, reject) => {
// hahah();
resolve(22);
})
.then(() => {
throw new Error("Go Wrong");
})
.then(() => {
reject("reject");
})
.catch((err) => {
console.log(err);
throw new Error("Wrong Again");
})
.then(() => console.log("catch之后的then被执行"))
.catch((err) => {
console.log("再次捕获", err);
});
JS错误、reject、throw的错误都会被catch捕获
- promise链中catch捕获之后的then还会继续执行
- promise链中一旦某个then发生错误,紧跟着该处的then(catch之前)不会执行
- catch中再次抛出错误会被下一个catch捕获
- 如果没有catch错误,脚本会被杀死,不会继续往下执行。通过unhandledrejection可以捕获到该类错误
- Promise 的执行者(executor)和 promise 的处理程序(handler)周围有一个“隐式的 try..catch”
new Promise(function(resolve, reject) {
setTimeout(() => {
throw new Error("Whoops!");
}, 1000);
}).catch(alert);
函数代码周围有个“隐式的 try..catch”。所以,所有同步错误都会得到处理。
这里的错误并不是在 executor 运行时生成的,而是在稍后生成的。因此,promise 无法处理它。
//============正确姿势===============
new Promise(function (resolve, reject) {
setTimeout(() => {
try {
throw new Error("Whoops!");
} catch (err) {
console.log("try catch 捕获")
}
}, 1000);
}).catch((err) => console.log("promise catch 捕获"));
//"try catch 捕获"
new Promise((resolve, reject) => {
// hahah();
reject("Go Wrong");
})
.catch((err) => {
console.log(err);
})
.then(() => console.log("继续执行"));
console.log("last execute");
//======================================
// promise reject错误
window.addEventListener("unhandledrejection", (e) => {
console.log("unhandledrejection", e);
});
new Promise((resolve, reject) => {
// hahah();
reject("Go Wrong");
})
console.log("last execute");
静态方法
Promise 类有 5 种静态方法:
- Promise.all(promises) —— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。
- Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果:
- status: “fulfilled” 或 “rejected”
- value(如果 fulfilled)或 reason(如果 rejected)。
- Promise.race(promises) —— 等待第一个 settle 的 promise,并将其 result/error 作为结果。
- Promise.resolve(value) —— 使用给定 value 创建一个 resolved 的 promise。
- Promise.reject(error) —— 使用给定 error 创建一个 rejected 的 promise。
Promise.all([])
promise.race([])
promise.allSettled([])
Promise.resolve()
Promise.reject()
Promise.all([
"First Value",
new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
new Promise((resolve,reject) => setTimeout(() => reject(new Error('Whoops!')), 1000)), // 3
]).then((value) => console.log(value)).catch(err=> console.log(err)); //
Promise.allSettled([
"First Value",
new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
new Promise((resolve,reject) => setTimeout(() => reject(new Error('Whoops!')), 1000)), // 3
]).then((value) => console.log(value)).catch(err=> console.log(err)); //
常规函数只会返回一个单一值(或者不返回任何值)。
而 Generator 可以按需一个接一个地返回(“yield”)多个值
执行 Generator 函数会返回一个遍历器对象,(也就是说可以for of、扩展运算符……)。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
基本使用
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
console.log(generator);
let sequence = [0,...generateSequence()];
console.log(sequence); // [0,1,2] 注意没有3
for (let value of generator) {
console .log(value); // 1,然后是 2
}
console.log(generator.next()); // 1
console.log(generator.next()); // 2
console.log(generator.next()); // 3
异步generator
Generator可以通过yield和next进行暂停和执行,是异步编程的一种解决方案。
//模拟请求
const fetchData = (data) =>
setTimeout((val) => console.log(val), 1000, data + 1);
function* generatorFetch() {
yield fetchData(1000);
yield fetchData(2000);
yield fetchData(3000);
}
const gen = generatorFetch();
gen.next(); //1001
gen.next(); //2001
gen.next(); //3001
这段代码非常像同步操作,除了加上了yield命令
function* generatorFunction() {
let ask1 = yield '2 + 2 = ?'; // ask1: 4
let ask2 = yield '3 * 3 = ?'; // ask2: 9
}
const generator = generatorFunction();
const g1 = generator.next().value; // "2 + 2 = ?"
const g2 = generator.next(4).value; // "3 * 3 = ?"
const g3 = generator.next(9).done; // true
//错误捕获
try {
generator.throw(new Error('The answer is not found in my database')); // (2)
} catch (err) {
console.log(err);
}
手写 Generator 函数
// 简易版
// 定义生成器函数,入参是任意集合
function webCanteenGenerator(list) {
var index = 0;
var len = list.length;
return {
// 定义 next 方法
// 记录每次遍历位置,实现闭包,借助自由变量做迭代过程中的“游标”
next: function() {
var done = index >= len; // 如果索引还没有超出集合长度,done 为 false
var value = !done ? list[index++] : undefined; // 如果 done 为 false,则可以继续取值
// 返回遍历是否完毕的状态和当前值
return {
done: done,
value: value
}
}
}
}
var canteen = webCanteenGenerator(['道路千万条', '安全第一条', '行车不规范']);
canteen.next();
canteen.next();
canteen.next();
// {done: false, value: "道路千万条"}
// {done: false, value: "安全第一条"}
// {done: false, value: "行车不规范"}
// {done: true, value: undefined}
https://mp.weixin.qq.com/s/b-4wgsYmTYYF0NxrnA8pIg
Generator 缺陷:
- 1.函数外部无法捕获异常
- 2.多个 yield 会导致调试困难
async 函数对 Generator 函数改进如下:
- 1.内置执行器
- 2.更好的语义
- 3.更广的适用性
- 4.返回值是 Promise
Async/Await
基本使用
async/await 做的事情就是将 Generator 函数转换成 Promise,说白了,async 函数就是 Generator 函数的语法糖,await 命令就是内部 then 命令的语法糖
async
确保了函数返回一个 promise,也会将非 promise 的值包装成promise
关键字 await
让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待,直到 promise resolve (*)
alert(result); // "done!"
}
f();
f().then(val=>console.log(val))
相比于 promise.then
,await 只是获取 promise 的结果的一个更优雅的语法,同时也更易于读写。
强调一下:await
实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
//等价于;
async function async1() {
console.log("async1 start");
Promise.resolve(async2()).then(() => {
console.log("async1 end");
});
}
我们可以用async包裹匿名函数
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
错误处理:一般用try/catch来捕获
async function f() {
try {
await Promise.reject(new Error("Whoops!"));
} catch (err) {
console.log(err);
}
}
f();
手写
尝试通过 Babel[8] 官网转换一下上述代码,可以看到其核心就是 _asyncToGenerator
方法。
//模拟请求
const fetchData = (data) =>
new Promise((resolve) =>
setTimeout(
(val) => {
console.log(val);
resolve(val);
},
1000,
data + 1
)
);
async function fetchResult() {
const result1 = await fetchData(1);
const result2 = await fetchData(result1);
const result3 = await fetchData(result2);
}
fetchResult();
==================================================
// 1.传入参数是generator
// 2.返回promise
// 3.自动调用generator.next()
function asyncToGenerator(genFunction) {
return function (...args) {
const gen = genFunction.apply(this, args);
return new Promise((resolve, reject) => {
function step(key, arg) {
let genResult;
try {
genResult = gen[key](arg); //相当于执行generator.next(args);
} catch (err) {
return reject(err);
}
const { value, done } = genResult;
if (done) {
return resolve(value);
}
return Promise.resolve(value).then(
(val) => {
step('next', val);
},
(err) => {
step('throw', err);
}
);
}
step('next');
});
};
}
function* fetchResult() {
const data = yield fetchData('data');
const data2 = yield fetchData(data);
return 'success';
}
const gen = asyncToGenerator(fetchResult);
gen().then((res) => console.log(res));
练习
手写promise
Promise
一步一步按照思路来实现,简易版:
const PENDING = 'pending';
const FULLFILLED = 'fullfiled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = (val) => {
if (this.status === PENDING) {
this.status = FULLFILLED;
this.value = val;
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFullfilled, onRejected) {
if (this.status === FULLFILLED) {
onFullfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
}
}
new MyPromise((resolve, reject) => {
resolve('第一个处理的数值');
}).then(
(val) => {
console.log(val);
},
(reason) => {
console.log(reason);
}
);
ok,看起来的是work了,但是如果将第41行换成setTimeout的时候,发现不符合预期。
new MyPromise((resolve, reject) => {
// 传入一个异步操作
setTimeout(() => {
resolve('第一个处理的数值');
}, 100);
}).then(
(val) => {
console.log(val);
},
(reason) => {
console.log(reason);
}
);
//没有任何输出
原因是什么?因为setTimeout是宏任务,不会立刻执行,then的时候,还是PENDING状态
所以在then中需要判断下,当前状态是 pending,我们需要先将成功和失败的回调分别存放起来,在executor()的异步任务被执行时,触发 resolve 或 reject,依次调用成功或失败的回调。
const PENDING = 'pending';
const FULLFILLED = 'fullfiled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = []; //**********************
this.onRejectedCallbacks = []; //**********************
const resolve = (val) => {
if (this.status === PENDING) {
this.status = FULLFILLED;
this.value = val;
this.onResolvedCallbacks.forEach((fn) => fn());//**********************
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) => fn());//**********************
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFullfilled, onRejected) {
if (this.status === FULLFILLED) {
onFullfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
//**********************
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => onFullfilled(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.reason));
}
}
}
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('第一个处理的数值');
}, 100);
}).then(
(val) => {
console.log(val);
},
(reason) => {
console.log(reason);
}
);
已经解决了以上问题了,Promise最核心的是 值穿透
具体如何实现呢?思考一下,如果每次调用 then 的时候,我们都重新创建一个 promise 对象,并把上一个 then 的返回结果传给这个新的 promise 的 then 方法,不就可以一直 then 下去了么?
const PENDING = 'pending';
const FULLFILLED = 'fullfiled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (val) => {
if (this.status === PENDING) {
this.status = FULLFILLED;
this.value = val;
this.onResolvedCallbacks.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFullfilled, onRejected) {
return new Promise((resolve, reject) => { //*****************
//模拟事件循环中then的微任务
setTimeout(() => { //*****************
if (this.status === FULLFILLED) {
const result = onFullfilled(this.value);
resolve(result); //*****************
// resolvePromise(promise2, x, resolve, reject);
}
}, 0);
if (this.status === REJECTED) {
const reason = onRejected(this.reason);
reject(reason);
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
resolve(onFullfilled(this.value));
});
this.onRejectedCallbacks.push(() => {
const reason = onRejected(this.reason);
reject(reason);
});
}
});
}
}
new MyPromise((resolve, reject) => {
// resolve(2321);
setTimeout(() => {
resolve('第一个处理的数值');
}, 100);
})
.then(
(val) => {
console.log(val);
return '第二个被处理的值';
},
(reason) => {
console.log(reason);
}
)
.then((val) => console.log('值穿透', val));
值穿透已经初步支持。
关于then有很多规范,如下:
then 方法是 Promise 的核心,这里做一下详细介绍。
promise.then(onFulfilled, onRejected)
一个 Promise 的then接受两个参数: onFulfilled和onRejected(都是可选参数,并且为函数,若不是函数将被忽略)
- onFulfilled 特性:
- 当 Promise 执行结束后其必须被调用,其第一个参数为 promise 的终值,也就是 resolve 传过来的值
- 在 Promise 执行结束前不可被调用
- 其调用次数不可超过一次
- onRejected 特性
- 当 Promise 被拒绝执行后其必须被调用,第一个参数为 Promise 的拒绝原因,也就是reject传过来的值
- 在 Promise 执行结束前不可被调用
- 其调用次数不可超过一次
- 调用时机
onFulfilled
和onRejected
只有在执行环境堆栈仅包含平台代码时才可被调用(平台代码指引擎、环境以及 promise 的实施代码) - 调用要求
onFulfilled
和onRejected
必须被作为函数调用,如果不是函数,忽略。后面的then中可以获取之前的值。promise.then().then()
,那么其后面的 then 依旧可以得到之前 then 返回的值,这就是所谓的值的穿透 - 多次调用
then
方法可以被同一个promise
调用多次- 当
promise
成功执行时,所有onFulfilled
需按照其注册顺序依次回调 - 当
promise
被拒绝执行时,所有的onRejected
需按照其注册顺序依次回调
- 当
- 返回
then
方法会返回一个Promise
,关于这一点,Promise/A+标准并没有要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(详情)对象,所以在我们的实现中,也让then返回一个新的Promise对象。
promise2 = promise1.then(onFulfilled, onRejected);
- 如果
onFulfilled
或者onRejected
返回一个值x
,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)
- 如果
onFulfilled
或者onRejected
抛出一个异常e
,则promise2
必须拒绝执行,并返回拒因e
- 如果
onFulfilled
不是函数且promise1
成功执行,promise2
必须成功执行并返回相同的值 - 如果
onRejected
不是函数且promise1
拒绝执行,promise2
必须拒绝执行并返回相同的拒因- 不论 promise1 被 reject 还是被 resolve , promise2 都会被 resolve,只有出现异常时才会被 rejected。
每个Promise对象都可以在其上多次调用then方法,而每次调用then返回的Promise的状态取决于那一次调用then时传入参数的返回值,所以then不能返回this,因为then每次返回的Promise的结果都有可能不同。
- 不论 promise1 被 reject 还是被 resolve , promise2 都会被 resolve,只有出现异常时才会被 rejected。
const resolvePromise = (promise2, x, resolve, reject) => {
// 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// Promise/A+ 2.3.3.3.3 只能调用一次
let called;
// 后续的条件要严格判断 保证代码能和别的库一起使用
if ((typeof x === 'object' && x != null) || typeof x === 'function') {
try {
// 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候) Promise/A+ 2.3.3.1
let then = x.then;
if (typeof then === 'function') {
// 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3
then.call(x, y => { // 根据 promise 的状态决定是成功还是失败
if (called) return;
called = true;
// 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
resolvePromise(promise2, y, resolve, reject);
}, r => {
// 只要失败就失败 Promise/A+ 2.3.3.3.2
if (called) return;
called = true;
reject(r);
});
} else {
// 如果 x.then 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.3.4
resolve(x);
}
} catch (e) {
// Promise/A+ 2.3.3.2
if (called) return;
called = true;
reject(e)
}
} else {
// 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4
resolve(x)
}
}
finally/catch
Promise.prototype.catch = function(errCallback){
return this.then(null,errCallback)
}
Promise.prototype.finally = function(cb) {
return this.then((value)=>{
return Promise.resolve(cb()).then(()=>value)
},(reason)=>{
return Promise.resolve(cb()).then(()=>{throw reason})
})
}
Promise.resolve/reject
static resolve(val){
return new Promise((resolve,reject)=>{
resolve(val);
})
}
static reject(reason){
return new Promise((resolve,reject)=>{
reject(reason);
})
}
Promise.allSettled
if (!Promise.allMySettled) {
Promise.allMySettled = function (promises) {
const convertedPromises = promises.map((p) =>
Promise.resolve(p).then(
(value) => ({ status: "fulfilled", value }),
(reason) => ({ status: "rejected", reason })
)
);
return Promise.all(convertedPromises);
};
}
Promise.allMySettled([
"First Value",
new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
new Promise((resolve, reject) =>
setTimeout(() => reject(new Error("Whoops!")), 1000)
), // 3
])
.then((value) => console.log(value))
.catch((err) => console.log(err));
Promise.all
if (!Promise.myAll) {
Promise.myAll = function (promises) {
return new Promise((resolve, reject) => {
let value = [];
let count = 0;
for (let [i, p] of promises.entries()) {
// 传入的参数中如果不是promise
Promise.resolve(p).then(
(val) => {
value[i] = val;
count++;
// 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
if (count === promises.length) resolve(value);
},
(err) => reject(err)
);
}
});
};
}
Promise.myAll([
"11111",
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) =>
setTimeout(() => reject(new Error("Whoops!")), 2000)
),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then((val) => console.log(val))
.catch((err) => console.log(err));
Promise.race
if (!Promise.myRace) {
Promise.myRace = function (promises) {
return new Promise((resolve, reject) => {
for (let p of promises) {
Promise.resolve(p).then(
(val) => resolve(val),
(err) => reject(err)
);
}
});
};
}
Promise.myRace([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) =>
setTimeout(() => reject(new Error("Whoops!")), 1000)
),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then((val) => console.log(val))
.catch((err) => console.log(err));
Promisify
将回调函数转换成promisepromisify(f)
:它接受一个需要被 promisify 的函数 f
,并返回一个包装(wrapper)函数。
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
// 用法:
// loadScript('path/script.js', (err, script) => {...})
function promisify(f) {
return function (...args) { // 返回一个包装函数(wrapper-function) (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // 我们对 f 的自定义的回调 (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // 将我们的自定义的回调附加到 f 参数(arguments)的末尾
f.call(this, ...args); // 调用原始的函数
});
};
}
// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
但是如果原始的 f 期望一个带有更多参数的回调 callback(err, res1, res2, ...),该怎么办呢?
我们可以继续改进我们的辅助函数。让我们写一个更高阶版本的 promisify。
当它被以 promisify(f) 的形式调用时,它应该以与上面那个版本的实现的工作方式类似。
当它被以 promisify(f, true) 的形式调用时,它应该返回以回调函数数组为结果 resolve 的 promise。这就是具有很多个参数的回调的结果。
// promisify(f, true) 来获取结果数组
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // 我们自定义的 f 的回调
if (err) {
reject(err);
} else {
// 如果 manyArgs 被指定,则使用所有回调的结果 resolve
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
}
// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);
promise链式调用
红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promise 实现)
function red() {
console.log("red");
}
function green() {
console.log("green");
}
function yellow() {
console.log("yellow");
}
function light(ms, cb) {
return new Promise((resolve, reject) => {
setTimeout(() => {
cb();
resolve();
}, ms);
});
}
function step() {
Promise.resolve(() => {
return light(1000, red);
})
.then(() => {
return light(2000, green);
})
.then(() => {
return light(3000, yellow);
})
.then(() => {
step();
});
}
step();
class A {
constructor() {
this.list = Promise.resolve();
}
say(message) {
this.list = this.list.then(() => {
console.log(message);
});
return this;
}
delay(ms) {
this.list = this.list.then(()=>{
return new Promise(resolve=>{
setTimeout(()=>{
resolve()
},ms)
})
})
return this
}
}
const a = new A();
a.say("1")
.delay(1000)
.say("2")
.delay(4000)
.say("fsaffa");
实现 mergePromise 函数,把传进去的函数数组按顺序先后执行,并且把返回的数据先后放到数组 data 中。
思路:全局定义一个promise实例p
循环遍历函数数组,每次循环更新p,将要执行的函数item通过p的then方法进行串联,并且将执行结果推入data数组
最后将更新的data返回,这样保证后面p调用then方法,如何后面的函数需要使用data只需要将函数改为带参数的函数。
const timeout = ms =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
const ajax1 = () =>
timeout(2000).then(() => {
console.log("1");
return 1;
});
const ajax2 = () =>
timeout(1000).then(() => {
console.log("2");
return 2;
});
const ajax3 = () =>
timeout(2000).then(() => {
console.log("3");
return 3;
});
const mergePromise = ajaxArray => {
let data = [];
let p = Promise.resolve();
for (let ajax of ajaxArray) {
p = p.then(ajax).then(val => {
data.push(val);
return data;
});
}
return p;
};
mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log("done");
console.log(data); // data 为 [1, 2, 3]
});
并发
有 8 个图片资源的 url,已经存储在数组 urls 中(即urls = [‘http://example.com/1.jpg‘, …., ‘http://example.com/8.jpg']),而且已经有一个函数 function loadImg,输入一个 url 链接,返回一个 Promise,该 Promise 在图片下载完成的时候 resolve,下载失败则 reject。
但是我们要求,任意时刻,同时下载的链接数量不可以超过 3 个。
const urls = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function load(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url);
}, (Math.random() * 10000) >> 0);
});
}
function limitLoad(urls, handler, limit) {
const queue = urls.slice(0);
const promises = queue.splice(0, limit).map((url, idx) => {
return handler(url).then(() => idx);
});
let p = Promise.race(promises);
for (let i = 0; i < queue.length; i++) {
p = p.then((idx) => {
promises[idx] = handler(queue[i]).then(() => idx);
return Promise.race(promises);
});
}
}
limitLoad(urls, load, 3);
=============超时重试========
let urls = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
urls = urls.map((v) => ({
url: v,
retry: 0,
}));
function load(urlInfo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve(urlInfo);
} else {
reject(urlInfo);
}
}, (Math.random() * 3000) >> 0);
});
}
function limitLoad(urls, handler, limit, retryNum) {
const queue = urls.slice(0);
while (limit) {
exec(queue.shift());
limit--;
}
function exec(urlInfo) {
handler(urlInfo)
.then(() => {
const curUrlInfo = queue.shift();
if (curUrlInfo) {
exec(curUrlInfo);
}
})
.catch((res) => {
if (res.retry >= retryNum) {
count++;
const curUrlInfo = queue.shift();
exec(curUrlInfo);
} else {
exec({
...res,
retry: ++res.retry,
});
}
});
}
}
limitLoad(urls, load, 3, 3);
const urls = [
'https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg',
'https://www.kkkk1000.com/images/getImgData/gray.gif',
'https://www.kkkk1000.com/images/getImgData/Particle.gif',
'https://www.kkkk1000.com/images/getImgData/arithmetic.png',
'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif',
'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg',
'https://www.kkkk1000.com/images/getImgData/arithmetic.gif',
'https://www.kkkk1000.com/images/wxQrCode2.png'
];
function loadImg(url) {
return new Promise((resolve, reject) => {
const ImageName = url.split(/\//g)[5] || url.split(/\//g)[4];
const img = new Image();
img.onload = function () {
console.log('一张图片加载完成', ImageName);
resolve(ImageName);
};
img.onerror = reject;
img.src = url;
});
}
解法一
function fetchLimit(urls, limit, handler) {
while (limit--) {
loop();
}
function loop() {
if (urls.length > 0) {
handler(urls.shift()).then(loop);
}
}
}
fetchLimit(urls, 3, loadImg);
解法二
let mapLimit = (list, limit, asyncHandle) => {
//这5个异步请求中无论哪一个先执行完,都会继续执行下一个list项
let loop = (urls) => {
return asyncHandle(urls.shift())
.then(()=>{
// 数组还未迭代完,递归继续进行迭代
if (urls.length!==0) return loop(urls)
else return '这个坑位finish';
})
};
let asyncList = []; // 正在进行的所有并发异步操作
while(limit--) {
asyncList.push( loop([...list]) );
}
return Promise.all(asyncList); // 所有并发异步操作都完成后,本次并发控制迭代完成
}
mapLimit(urls,5,loadImg).then((res)=>console.log(res))
解法三
1.用 Promise.race来实现,先并发请求3个图片资源,这样可以得到 3 个 Promise实例,组成一个数组promises
2.然后不断的调用 Promise.race 来返回最快改变状态的 Promise,从数组(promises )中删掉这个 Promise 对象实例
3.再加入一个新的 Promise实例,直到全部的 url 被取完。
function limitLoad(urls, handler, limit) {
// 对数组做一个拷贝
const urlList = [...urls]
let promises = [];
//并发请求到最大数,得到3个promise实例 [p1,p2,p3]
promises = urlList.splice(0, limit).map((url, index) => {
// 这里返回的 index 是任务在 promises 的脚标,
//用于在 Promise.race 之后找到完成的任务脚标
return handler(url,index).then(() => {
return index
});
});
(async function loop() {
//Promise.race(promises).then(x=>{
// promises[x] = handler(urlList.shift()).then(()=>loop())
//});
let p = Promise.race(promises);
for (let i = 0; i < urlList.length; i++) {
p = p.then((res) => {
promises[res] = handler(urlList[i]).then(() => {
return res
});
return Promise.race(promises)
})
}
})()
}
limitLoad(urls, loadImg, 3)
let urls = ['http://dcdapp.com', …];
/*
*实现一个方法,比如每次并发的执行三个请求,如果超时(timeout)就输入null,直到全部请求完
*batchGet(urls, batchnum=3, timeout=3000);
*urls是一个请求的数组,每一项是一个url
*最后按照输入的顺序返回结果数组[]
*/
async function batchFetchData(urls, limit = 3, timeout = 3000) {
let ret = [];
while (urls.length > 0) {
var preList = urls.splice(0, limit);
let requestList = preList.map((url) => {
return request(url, timeout);
});
const result = await Promise.allsettled(requestList);
ret.concat(
result.map((item) => {
if (item.status === 'rejected') {
return null;
} else {
return item.value;
}
})
);
}
return ret;
}
function request(url, timeout) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, timeout);
// ajax发送请求
ajax({ url }, (data) => {
resolve(data);
});
});
}
// urls为一个不定长的数组
batchFetchData(['http1', 'http2', 'http3']);
/*
可以批量请求数据,所有的 URL 地址在 urls 参数中,
同时可以通过 max 参数控制请求的并发度,当所有请
求结束之后,需要执行 callback 回调函数。发请求的
函数可以直接使用 fetch 即可
*/
function fetchLimit(sourceUrls, limit, cb) {
const urls = [...sourceUrls];
while (limit--) {
exec();
}
const finished = 0;
function exec() {
const url = urls.splice(urls);
if (finished >= urls.length) {
return cb();
}
fetch(url)
.then((res) => {
finished++;
exec();
})
.catch((err) => {
exec();
});
}
}
=============================================
function fetchLimit(urls, limit, cb) {
let currentIdx = 0; //当前请求地址的数组索引
let finished = 0; //已经完成的请求数量
for (let i = 0; i < limit; i++) {
loop();
}
function loop() {
console.log("finished", finished, "currentIdx", currentIdx);
// 当所有请求完成之后执行cb
if (finished === urls.length) {
return cb();
}
//当超出urls
if (currentIdx > urls.length) {
limit = 0;
}
if (limit > 0) {
fetch(urls[currentIdx])
.then((res) => {
// do your logic
console.log(currentIdx, urls[currentIdx]);
finished++;
loop();
})
.catch((err) => {
// do your logic
loop();
});
}
currentIdx++;
}
}
const callback = () => {
console.log("全部请求完毕");
};
fetchLimit(urls, 3, callback);
/**
*
* @param { Array } urls 请求地址数组
* @param { Number } max 最大并发请求数
* @param { Function } callback 回调地址
*/
function parallelFetch(urls, max, callback) {
// 如果当前环境不支持 fetch , 则提示程序无法正常运行
if (!window.fetch || "function" !== typeof window.fetch) {
throw Error("当前环境不支持 fetch 请求,程序终止");
}
// 如果参数有误,则提示输入正确的参数
if (!urls || urls.length <= 0) {
throw Error("urls is empty: 请传入正确的请求地址");
}
const _urlsLength = urls.length; // 请求地址数组的长度
const _max = max || 1; // 保证最大并发值的有效性
let _currentIndex = 0; // 当前请求地址的索引
let _maxFetch = max <= _urlsLength ? max : _urlsLength; // 当前可以正常请求的数量,保证最大并发数的安全性
let _finishedFetch = 0; // 当前完成请求的数量,用于判断何时调用回调
console.log(`开始并发请求,接口总数为 ${_urlsLength} ,最大并发数为 ${_maxFetch}`);
// 根据最大并发数进行循环发送,之后通过状态做递归请求
for (let i = 0; i < _maxFetch; i++) {
fetchFunc();
}
// 请求方法
function fetchFunc() {
// 如果所有请求数都完成,则执行回调方法
if (_finishedFetch === _urlsLength) {
console.log(`当前一共 ${_urlsLength} 个请求,已完成 ${_finishedFetch} 个`)
if ("function" === typeof callback) return callback();
return false;
}
// 如果当前请求的索引大于等于请求地址数组的长度,则不继续请求
if (_currentIndex >= _urlsLength) {
_maxFetch = 0;
}
//如果可请求的数量大于0,表示可以继续发起请求
if (_maxFetch > 0) {
console.log( `当前正发起第 ${_currentIndex + 1 } 次请求,当前一共 ${_urlsLength} 个请求,已完成 ${_finishedFetch} 个,请求地址为:${urls[_currentIndex]}`);
// 发起 fetch 请求
fetch(urls[_currentIndex])
.then((res) => {
// TODO 业务逻辑,正常的逻辑,异常的逻辑
// 当前请求结束,正常请求的数量 +1
_maxFetch += 1;
_finishedFetch += 1;
fetchFunc();
})
.catch((err) => {
// TODO 异常处理,处理异常逻辑
// 当前请求结束,正常请求的数量 +1
_maxFetch += 1;
_finishedFetch += 1;
fetchFunc();
});
// 每次请求,当前请求地址的索引 +1
_currentIndex += 1;
// 每次请求,可以正常请求的数量 -1
_maxFetch -= 1;
}
}
}
let urls = [];
for (let i = 0; i < 100; i++) {
urls.push(`https://jsonplaceholder.typicode.com/todos/${i}`);
}
const max = 10;
const callback = () => {
console.log("我请求完了");
};
parallelFetch(urls, max, callback);
import asyncPool from 'tiny-async-pool';
const timeout = (i) =>
new Promise((resolve) => setTimeout(() => resolve(i), i));
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = []; // 存储所有的异步任务
const executing = []; // 存储正在执行的异步任务
for (const item of array) {
// 调用iteratorFn函数创建异步任务
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p); // 保存新的异步任务
// 当poolLimit值小于或等于总任务个数时,进行并发控制
if (poolLimit <= array.length) {
// 当任务完成后,从正在执行的任务数组中移除已完成的任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e); // 保存正在执行的异步任务
if (executing.length >= poolLimit) {
await Promise.race(executing); // 等待较快的任务执行完成
}
}
}
return Promise.all(ret);
}
(async () => {
const results = await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
console.log(2222, results);
})();
超时中断
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟的接口调用 ajax 超时设置
resolve("请求成功");
}, 3000);
});
function fetchWithTimeout(fetchPromise, timeout = 2000) {
const timePromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("请求超时timeout");
}, timeout);
});
return Promise.race([fetchPromise, timePromise]);
}
fetchWithTimeout(promise, 1000)
.then(val => console.log(val))
.catch(err => console.log(err));
Reference
https://mp.weixin.qq.com/s/b-4wgsYmTYYF0NxrnA8pIg
https://juejin.cn/post/6850037281206566919#heading-5
https://github.com/sisterAn/blog/issues/13
https://github.com/sisterAn/JavaScript-Algorithms/issues/85
https://segmentfault.com/a/1190000022705474