回调地狱
function loadScript(src, cb) {const script = document.createElement("script");script.src = src;// 这里约定第一个参数是处理errorscript.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); // 1return new Promise((resolve, reject) => {setTimeout(() => resolve(result * 2), 1000);});}).then((result) => {console.log(result); // 2return new Promise((resolve, reject) => {setTimeout(() => resolve(result * 2), 1000);});}).then((result) => {console.log(result); // 4return 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)), // 1new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2new 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)), // 1new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2new 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] 注意没有3for (let value of generator) {console .log(value); // 1,然后是 2}console.log(generator.next()); // 1console.log(generator.next()); // 2console.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(); //1001gen.next(); //2001gen.next(); //3001
这段代码非常像同步操作,除了加上了yield命令
function* generatorFunction() {let ask1 = yield '2 + 2 = ?'; // ask1: 4let 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 为 falsevar 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.1if (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.1let then = x.then;if (typeof then === 'function') {// 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3then.call(x, y => { // 根据 promise 的状态决定是成功还是失败if (called) return;called = true;// 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1resolvePromise(promise2, y, resolve, reject);}, r => {// 只要失败就失败 Promise/A+ 2.3.3.3.2if (called) return;called = true;reject(r);});} else {// 如果 x.then 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.3.4resolve(x);}} catch (e) {// Promise/A+ 2.3.3.2if (called) return;called = true;reject(e)}} else {// 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4resolve(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)), // 1new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2new 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()) {// 传入的参数中如果不是promisePromise.resolve(p).then((val) => {value[i] = val;count++;// 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilledif (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 被指定,则使用所有回调的结果 resolveresolve(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);// 当所有请求完成之后执行cbif (finished === urls.length) {return cb();}//当超出urlsif (currentIdx > urls.length) {limit = 0;}if (limit > 0) {fetch(urls[currentIdx]).then((res) => {// do your logicconsole.log(currentIdx, urls[currentIdx]);finished++;loop();}).catch((err) => {// do your logicloop();});}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

