2. Promise手写&&结合Event Loop输出顺序训练题&&如何中断promise&& promise相关题目

318 阅读11分钟

PromiseA+规范

术语

  1. promise 是一个有then方法的对象或者是函数,行为遵循本规范
  2. thenable 是一个有then方法的对象或者是函数
  3. value 是promise状态成功时的值,也就是resolve的参数, 包括各种数据类型, 也包括undefined/thenable或者是 promise
  4. reason 是promise状态失败时的值, 也就是reject的参数, 表示拒绝的原因
  5. exception 是一个使用throw抛出的异常值

规范

Promise States

promise应该有三种状态. 要注意他们之间的流转关系.

  1. pending

    1.1 初始的状态, 可改变.
    1.2 一个promise在resolve或者reject前都处于这个状态。
    1.3 可以通过 resolve -> fulfilled 状态;
    1.4 可以通过 reject -> rejected 状态;

  2. fulfilled

    2.1 最终态, 不可变.
    2.2 一个promise被resolve后会变成这个状态.
    2.3 必须拥有一个value值

  3. rejected

    3.1 最终态, 不可变.
    3.2 一个promise被reject后会变成这个状态
    3.3 必须拥有一个reason

Tips: 总结一下, 就是promise的状态流转是这样的

pending -> resolve(value) -> fulfilled
pending -> reject(reason) -> rejected

then

promise应该提供一个then方法, 用来访问最终的结果, 无论是value还是reason.

promise.then(onFulfilled, onRejected)
  1. 参数要求

    1.1 onFulfilled 必须是函数类型, 如果不是函数, 应该被忽略. 1.2 onRejected 必须是函数类型, 如果不是函数, 应该被忽略.

  2. onFulfilled 特性

    2.1 在promise变成 fulfilled 时,应该调用 onFulfilled, 参数是value 2.2 在promise变成 fulfilled 之前, 不应该被调用. 2.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)

  3. onRejected 特性

    3.1 在promise变成 rejected 时,应该调用 onRejected, 参数是reason 3.2 在promise变成 rejected 之前, 不应该被调用. 3.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)

  4. onFulfilled 和 onRejected 应该是微任务

    这里用queueMicrotask来实现微任务的调用.

  5. then方法可以被调用多次

    5.1 promise状态变成 fulfilled 后,所有的 onFulfilled 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onFulfilled的回调) 5.2 promise状态变成 rejected 后,所有的 onRejected 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onRejected的回调)

  6. 返回值

    then 应该返回一个promise

    promise2 = promise1.then(onFulfilled, onRejected);
    

    6.1 onFulfilled 或 onRejected 执行的结果为x, 调用 resolvePromise( 这里大家可能难以理解, 可以先保留疑问, 下面详细讲一下resolvePromise是什么东西 ) 6.2 如果 onFulfilled 或者 onRejected 执行时抛出异 常e, promise2需要被reject 6.3 如果 onFulfilled 不是一个函数, promise2 以promise1的value 触发fulfilled 6.4 如果 onRejected 不是一个函数, promise2 以promise1的reason 触发rejected

  7. resolvePromise

    resolvePromise(promise2, x, resolve, reject)
    

    7.1 如果 promise2 和 x 相等,那么 reject TypeError

    7.2 如果 x 是一个 promsie

     如果x是pending态,那么promise必须要在pending,直到 x 变成 fulfilled or rejected.
     如果 x 被 fulfilled, fulfill promise with the same value.
     如果 x 被 rejected, reject promise with the same reason.
    

    7.3 如果 x 是一个 object 或者 是一个 function

     let then = x.then.
     如果 x.then 这步出错,那么 reject promise with e as the reason.
     如果 then 是一个函数,then.call(x, resolvePromiseFn, rejectPromise)
         resolvePromiseFn 的 入参是 y, 执行 resolvePromise(promise2, y, resolve, reject);
         rejectPromise 的 入参是 r, reject promise with r.
     如果 resolvePromise 和 rejectPromise 都调用了,那么第一个调用优先,后面的调用忽略。
     如果调用then抛出异常e 
     如果 resolvePromise 或 rejectPromise 已经被调用,那么忽略
     则,reject promise with e as the reason
     如果 then 不是一个function. fulfill promise with x.
    

手写一个Promise

const PENDING = 'pending';
const FULFULLED = 'fulfilled';
const REJECTED = 'rejected';
class MPromise {
    constructor(fn) {
        this.status = PENDING;
        this.value = '';
        this.reason = '';
        this.resolveMicroQueueTaskList = [];
        this.rejectMicroQueueTaskList = [];
        fn(this.resolve.bind(this), this.reject.bind(this));
    }
    resolve(value) {
        if (this.status === PENDING) {
            this.value = value;
            this.status = FULFULLED;
        }
    }
    reject(reason) {
        if (this.status === PENDING) {
            this.reason = reason;
            this.status = REJECTED;
        }
    }
    get status() {
        return this._status;
    }

    set status(newStatus) {
        this._status = newStatus;
        if (newStatus === FULFULLED) {
            this.resolveMicroQueueTaskList.forEach(cb => {
                cb()
            });
        } else if (newStatus === REJECTED) {
            this.rejectMicroQueueTaskList.forEach(cb => {
                cb()
            });
        }

    }

    then(resolve, reject) {
        const resolveFunction = resolve ? resolve : (value) =>  value;
        const rejectFunction = reject ? reject : (reason) =>  reason;
        const nextPromse = new MPromise((resolve, reject) => {
            const resolveMicroQueueTask = () => {
                queueMicrotask(() => {
                    const x = resolveFunction(this.value);
                    this.resolveNextPromise(x, resolve);
                })
            }
            const rejectMicroQueueTask = () => {
                queueMicrotask(() => {
                    const y = rejectFunction(this.reason)
                    this.resolveNextPromise(y, resolve);
                })
            }
            switch (this.status) {
                case PENDING: {
                    this.resolveMicroQueueTaskList.push(resolveMicroQueueTask);
                    this.rejectMicroQueueTaskList.push(rejectMicroQueueTask);
                    break;
                }
                    
                case FULFULLED: {
                    resolveMicroQueueTask();
                    break;
                }
                    
                case REJECTED: {
                    rejectMicroQueueTask();
                }
            }
        })
        return nextPromse;
    }

    catch(reject) {
        this.then(null, reject);
    }

    resolveNextPromise(x, resolve) {
        resolve(x);
    }

    static resolve(value) {
        if(value instanceof MPromise) {
           return value; 
        } 
        return new MPromise((resolve, reject) => {
            resolve(value);
        })
        
    }
    static reject(value) {
        if(value instanceof MPromise) {
            return value;
        } else {
            return new MPromise((resolve, reject) => {
                reject(value);
            })
        }
    }
    static race (promiseList) {
        let promiseListLen = promiseList.length;
        
        return new MPromise((resolve, reject) => { 
            if(promiseListLen === 0) {
                resolve()
            }
            for(var i = 0; i< promiseList.length; i++){
                MPromise.resolve(promiseList[i]).then(res=> {
                    resolve(res)
                }).catch(err => {
                    reject(err)
                })
            }
        })
    }

    static all (promiseList) {
        let promiseListLen = promiseList.length;
        let j = 0;
        let promiseValList = [];
        return new MPromise((resolve, reject) => { 
            if(promiseListLen === 0) {
                resolve()
            }
            for(var i = 0; i< promiseList.length; i++){
                MPromise.resolve(promiseList[i]).then(res=> {
                    j++
                    promiseValList.push(res);
                    if(promiseListLen === j) {
                        resolve(promiseValList)
                    }
                }).catch(err => {
                    reject(err)
                })
            }
        })
    }
}

调用方式

链式调用

const promiseA = new MPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('reject promiseA')
    }, 1000);
})

promiseA.then(res => {
    console.log('then1 res', res);
}).then(res=> {
    console.log('then2 res', res);
}).catch(err => {
    console.log('catch err', err)
})

static resolve | reject方法

MPromise.resolve(promiseA).then(res=> {
    console.log('res', res)
}).catch(err => {
    console.log('err', err)
})
MPromise.then('123').then((res)=> {
    console.log('res', res)
})
MPromise.reject('123').catch((res)=> {
    console.log('err', res)
})

race | all

const promiseA = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        resolve(4000)
    }, 4000)
})
const promiseB = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        resolve(3000)
    }, 3000)
})
const promiseC = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        resolve(2000)
    }, 2000)
})
// const promiseD = new MPromise ((resolve, reject) => {
//     setTimeout(function() {
//         resolve(100)
//     }, 100)
// })
const promiseD = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        reject(100)
    }, 100)
})
const promiseE = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        resolve(10)
    }, 10)
})

const promiseAsyncList = [promiseA, promiseB, promiseC, promiseD, promiseE]

#### race

MPromise.race(promiseAsyncList).then(res=> {
    console.log('res result is', res);
}).catch(err => {
    console.log('err result is', err);
})

#### all
MPromise.all(promiseAsyncList).then(res=> {
    console.log('res result is', res);
}).catch(err => {
    console.log('err result is', err);
})

Promise.all 和 Promise.allSettled 有什么区别?

最大不同:Promise.allSettled永远不会被reject

Promise.all的痛点

const promises = [
  delay(100).then(() => 1),
  delay(200).then(() => 2),
  Promise.reject(3)
  ]

Promise.all(promises).then(values=>console.log(values))
// 最终输出: Uncaught (in promise) 3

Promise.all(promises)
.then(values=>console.log(values))
.catch(err=>console.log(err))
// 加入catch语句后,最终输出:3

当需要处理多个Promise并行时,一旦有一个promise出现了异常,被reject了,情况就会变的麻烦。
尽管能用catch捕获其中的异常,但你会发现其他执行成功的Promise的消息都丢失了。所以要么全部成功,要么全部重来

Promise.allSettled

const promises = [
  delay(100).then(() => 1),
  delay(200).then(() => 2),
  Promise.reject(3)
  ]

Promise.allSettled(promises).then(values=>console.log(values))
// 最终输出: 
//    [
//      {status: "fulfilled", value: 1},
//      {status: "fulfilled", value: 2},
//      {status: "rejected", value: 3},
//    ]

Promise.allSettled情况下,当前promise的状态,没有任何一个promise的信息被丢失。

因此,当用Promise.allSettled时,我们只需专注在then语句里,当有promise被异常打断时,我们依然能妥善处理那些已经成功了的promise,不必全部重来。

面试练习题

## 1.
const test = new MPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(111);
    }, 1000);
}).then((res) => {
    console.log(res);
    return res; // -->如果没有return res,后面的test.value的值就是undefined
});

setTimeout(() => {
    console.log(test.value);
}, 3000)

## 2.
 const test = new MPromise((resolve, reject) => {
    setTimeout(() => {
        reject(111);
    }, 1000);
}).catch((reason) => {
    console.log('报错' + reason); // 报错111
    console.log('catch', test) // catch Promise {<pending>}
});

setTimeout(() => {
    console.log('timeout', test);// timeout Promise {<fulfilled>: undefined}
}, 3000)

40道promise && 事件循环机制 && await && async 输出顺序训练题链接

fe.ecool.fun/topic-list?…

如何中断Promise?

Promise有个缺点,那就是一旦创建就无法取消,所以本质上promise是无法终止的。但是开发过程中会碰到2个需求:

  1. 中断调用链
  2. 中断Promise

1. 中断调用链

就是在某个 then/catch 执行之后,不想让后续的链式调用继续执行了。
Promise的then方法接收两个参数:Promise.prototype.then(onFulfilled, onRejected)

若onFulfilled或onRejected是一个函数,当函数返回一个新Promise对象时,原Promise对象的状态将跟新对象保持一致,详见Promises/A+标准。

因此,当新对象保持“pending”状态时,原Promise链将会中止执行。

Promise.resolve().then(() => {
    console.log('then 1')
    return new Promise(() => {}) // 返回一个一直pending的promise,原promise对象会与此对象保持一致,就会中断不会向下执行
}).then(() => {
    console.log('then 2')
}).then(() => {
    console.log('then 3')
}).catch((err) => {
    console.log(err)
})

2. 中断Promise

注意这里是中断而不是终止,因为 Promise 无法终止,这个中断的意思是:在合适的时候,把 pending 状态的 promise 给 reject 掉。例如一个常见的应用场景就是希望给网络请求设置超时时间,一旦超时就就中断,我们这里用定时器模拟一个网络请求,随机 3 秒之内返回。

function timeoutWrapper(p, timeout = 2000) {
    const wait = new Promise((resolve, reject) => {
        setTimeout(() => {
        reject('请求超时')
        }, timeout)
    })
    return Promise.race([p, wait])
}

Promise中,resolve后面的语句是否还会执行?

会被执行。如果不需要执行,需要在 resolve 语句前加上 return。

image.png

image.png

Promise中的值穿透是什么?

.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透 当then中传入的不是函数,则这个then传入的值无效,返回的promise的data将会保存上一个的promise.data。这就是发生值穿透的原因。而且每一个无效的then所返回的promise的状态都为resolved。

Promise.resolve(1) 
    .then(2) // 注意这里 
    .then(Promise.resolve(3)) 
    .then(console.log)

上面代码的输出是
1
Promise {<fulfilled>: undefined}

使用Promise实现每隔1秒输出1,2,3

const arr = [1, 2, 3]
arr.reduce((p, x) => {
  return p.then(() => {
    return new Promise(r => {
      setTimeout(() => r(console.log(x)), 1000)
    })
  })
}, Promise.resolve())

实现mergePromise函数

实现mergePromise函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。

const time = (timer) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve()
    }, timer)
  })
}
const ajax1 = () => time(2000).then(() => {
  console.log(1);
  return 1
})
const ajax2 = () => time(1000).then(() => {
  console.log(2);
  return 2
})
const ajax3 = () => time(1000).then(() => {
  console.log(3);
  return 3
})

function mergePromise () {
  // 在这里写代码
}

mergePromise([ajax1, ajax2, ajax3]).then(data => {
  console.log("done");
  console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

这道题有点类似于Promise.all(),不过.all()不需要管执行顺序,只需要并发执行就行了。但是这里需要等上一个执行完毕之后才能执行下一个。 解题思路:

  • 定义一个数组data用于保存所有异步操作的结果
  • 初始化一个const promise = Promise.resolve(),然后循环遍历数组,在promise后面添加执行ajax任务,同时要将添加的结果重新赋值到promise上。
function mergePromise (ajaxArray) {
  // 存放每个ajax的结果
  const data = [];
  let promise = Promise.resolve();
  ajaxArray.forEach(ajax => {
  	// 第一次的then为了用来调用ajax
  	// 第二次的then是为了获取ajax的结果
    promise = promise.then(ajax).then(res => {
      data.push(res);
      return data; // 把每次的结果返回
    })
  })
  // 最后得到的promise它的值就是data
  return promise;
}

使用Promise封装一个异步加载图片的方法

这个比较简单,只需要在图片的onload函数中,使用resolve返回一下就可以了。

function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function() {
      resolve(img);
    };
    img.onerror = function() {
    	reject(new Error('Could not load image at' + url));
    };
    img.src = url;
  });
}

使用Promise实现:限制异步操作的并发个数,并尽可能快的完成全部

有8个图片资源的url,已经存储在数组urls中。 urls类似于['https://image1.png', 'https://image2.png', ....] 而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。

但有一个要求,任何时刻同时下载的链接数量不可以超过3个。

请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。

var urls = [
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
];
function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function() {
      console.log("一张图片加载完成");
      resolve(img);
    };
    img.onerror = function() {
    	reject(new Error('Could not load image at' + url));
    };
    img.src = url;
  });
}



// 答案
function limitLoad(urls, handler, limit) {
  let sequence = [].concat(urls); // 复制urls
  // 这一步是为了初始化 promises 这个"容器"
  let promises = sequence.splice(0, limit).map((url, index) => {
    return handler(url).then(() => {
      // 返回下标是为了知道数组中是哪一项最先完成
      return index;
    });
  });
  // 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
  return sequence
    .reduce((pCollect, url) => {
      return pCollect
        .then(() => {
          return Promise.race(promises); // 返回已经完成的下标
        })
        .then(fastestIndex => { // 获取到已经完成的下标
        	// 将"容器"内已经完成的那一项替换
          promises[fastestIndex] = handler(url).then(
            () => {
              return fastestIndex; // 要继续将这个下标返回,以便下一次变量
            }
          );
        })
        .catch(err => {
          console.error(err);
        });
    }, Promise.resolve()) // 初始化传入
    .then(() => { // 最后三个用.all来调用
      return Promise.all(promises);
    });
}
limitLoad(urls, loadImg, 3)
  .then(res => {
    console.log("图片全部加载完毕");
    console.log(res);
  })
  .catch(err => {
    console.error(err);
  });

promise.catch后面的.then还会执行吗?

会继续执行.then.catch.finally都可以链式调用,其本质上是因为返回了一个新的Promise实例。

.catch只会处理rejected的情况,并且也会返回一个新的Promise实例。

.catch(onRejected)then(undefined, onRejected)在表现上是一致的。

事实上,catch(onRejected)从内部调用了then(undefined, onRejected)。

  • 如果.catch(onRejected)onRejected回调中返回了一个状态为rejectedPromise实例,那么.catch返回的Promise实例的状态也将变成rejected
  • 如果.catch(onRejected)onRejected回调中抛出了异常,那么.catch返回的Promise实例的状态也将变成rejected
  • 其他情况下,.catch返回的Promise实例的状态将是fulfilled

image.png

image.png

image.png