阅读 2306

promise经典面试题

上期讲了promise基本概念和用法,今天结合上期的内容,讲解几道经典的相关面试题。

promise基本规则:

1. 首先Promise构造函数会立即执行,而Promise.then()内部的代码在当次事件循环的结尾立即执行(微任务)。

2. promise的状态一旦由等待pending变为成功fulfilled或者失败rejected。那么当前promise被标记为完成,后面则不会再次改变该状态。

3. resolve函数和reject函数都将当前Promise状态改为完成,并将异步结果,或者错误结果当做参数返回。

4. Promise.resolve(value)

返回一个状态由给定 value 决定的 Promise 对象。如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行决定;否则的话(该 value 为空,基本类型或者不带 then 方法的对象),返回的 Promise 对象状态为 fulfilled,并且将该 value 传递给对应的 then 方法。通常而言,如果你不知道一个值是否是 Promise 对象,使用 Promise.resolve(value) 来返回一个 Promise 对象,这样就能将该 value 以 Promise 对象形式使用。

5. Promise.all(iterable)/Promise.race(iterable)

简单理解,这2个函数,是将接收到的promise列表的结果返回,区别是,all是等待所有的promise都触发成功了,才会返回,而race有一个成功了就会返回结果。其中任何一个promise执行失败了,都会直接返回失败的结果。

6. promise对象的构造函数只会调用一次,then方法和catch方法都能多次调用,但一旦有了确定的结果,再次调用就会直接返回结果。

开始答题

题目一

const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
    reject('error');
})
promise.then(() => {
    console.log(3);
}).catch(e => console.log(e))
console.log(4);
复制代码

可以看:规则一,promise构造函数的代码会立即执行,then或者reject里面的代码会放入异步微任务队列,在宏任务结束后会立即执行。规则二:promise的状态一旦变更为成功或者失败,则不会再次改变,所以执行结果为:1,2,4,3。而catch里面的函数不会再执行。

题目二

const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
             console.log('once')
             resolve('success')
        }, 1000)
 })
promise.then((res) => {
       console.log(res)
     })
promise.then((res) => {
     console.log(res)
 })
复制代码

根据规则6,promise的构造函数只会执行一次,而then方法可以多次调用,但是第二次是直接返回结果,不会有异步等待的时间,所以执行结果是: 过一秒打印:once,success,success

题目三

在浏览器上,下面的程序会一次输出哪些内容?

const p1 = () => (new Promise((resolve, reject) => {
 console.log(1);
 let p2 = new Promise((resolve, reject) => {
  console.log(2);
  const timeOut1 = setTimeout(() => {
   console.log(3);
   resolve(4);
  }, 0)
  resolve(5);
 });
 resolve(6);
 p2.then((arg) => {
  console.log(arg);
 });

}));
const timeOut2 = setTimeout(() => {
 console.log(8);
 const p3 = new Promise(reject => {
  reject(9);
 }).then(res => {
  console.log(res)
 })
}, 0)


p1().then((arg) => {
 console.log(arg);
});
console.log(10);
复制代码

事件循环:javascript的执行规则里面有个事件循环Event Loot的规则,在事件循环中,异步事件会放到异步队列里面,但是异步队列里面又分为宏任务和微任务,浏览器端的宏任务一般有:script标签,setTimeout,setInterval,setImmediate,requestAnimationFrame。微任务有:MutationObserver,Promise.then catch finally。宏任务会阻塞浏览器的渲染进程,微任务会在宏任务结束后立即执行,在渲染之前。

回到题目,结果为:'1,2,10,5,6,8,9,3'。你答对了吗?如果对了,那你基本理解了事件队列,微任务,宏任务了。

第一步:执行宏任务,结合规则一,输出:1,2,10。这时候事件循环里面有异步任务timeOut1,timeOut2,p2.then,p1.then

第二步:宏任务执行完后Event Loop会去任务队列取异步任务,微任务会优先执行,这时候会先后执行p2.then,p1.then,打印5,6。

第三步:微任务执行完了,开始宏任务,由于2个settimeout等待时间一样,所以会执行先进入异步队列的timeOut2,先后打印:8。执行宏任务的过程中,p3.then微任务进入了队列,宏任务执行完毕会执行微任务,输出:9。之后执行timeOut1,输出:3。

第四步:结合规则6,由于p2这个Promise对象的执行结果已经确定,所以4不会被打印。

注:在node.js上输出结果并不是这样的,因为node.js的事件循环跟浏览器端的有区别。

题目四

在不使用async/await的情况下,顺序执行一组异步代码函数,并输出最后的结果。

在上篇文章中,已经讲到过,利用promise.resolve结合reduce能顺序执行一组异步函数。

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...dd) => x => dd.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(funca, funcb, funcc, funcd);
transformData(1).then(result => console.log(result,'last result')).catch(e => console.log(e));
复制代码

以上代码可以封装成工具来使用,利用的是规则4,promise.resolve函数的特点,其中dd可以是一组同步函数,也可以是异步函数。最后的结果在result里面,异常信息能在最后捕获。想看更具体的可以查看这篇文章: promise讲解

题目五

顺序加载10张图片,图片地址已知,但是同时最多加载3张图片,要求用promise实现。

const baseUrl = 'http://img.aizhifou.cn/';
const urls = ['1.png', '2.png', '3.png', '4.png', '5.png','6.png', '7.png', '8.png', '9.png', '10.png'];
const loadImg = function (url, i) {
 return new Promise((resolve, reject) => {
  try {
   // 加载一张图片
   let image = new Image();
   image.onload = function () {
    resolve(i)
   }
   image.onerror = function () {
    reject(i)
   };
   image.src = baseUrl + url;
  } catch (e) {
   reject(i)
  }
 })
}
function startLoadImage(urls, limits, endHandle) {
 // 当前存在的promise队列
 let promiseMap = {};
 // 当前索引对应的加载状态,无论成功,失败都会标记为true,格式: {0: true, 1: true, 2: true...}
 let loadIndexMap = {};
 // 当前以及加载到的索引,方便找到下一个未加载的索引,为了节省性能,其实可以不要
 let loadIndex = 0;
 const loadAImage = function () {
  // 所有的资源都进入了异步队列
  if (Object.keys(loadIndexMap).length === urls.length) {
   // 所有的资源都加载完毕,或者进入加载状态,递归结束
   const promiseList = Object.keys(promiseMap).reduce((arr, item) => {arr.push(promiseMap[item]); return arr}, [])
   Promise.all(promiseList).then(res => {
    // 这里如果没有加载失败,就会在所有加载完毕后执行,如果其中某个错误了,这里的结果就不准确,不过这个不是题目要求的。
    console.log('all');
    endHandle && endHandle()
   }).catch((e) => {
    console.log('end:' + e);
   })
  } else {
   // 遍历,知道里面有3个promise
   while (Object.keys(promiseMap).length < limits) {
    for (let i = loadIndex; i < urls.length; i++) {
     if (loadIndexMap[i] === undefined) {
      loadIndexMap[i] = false;
      promiseMap[i] = loadImg(urls[i], i);
      loadIndex = i;
      break;
     }
    }
   }
   // 获取当前正在进行的promise列表,利用reduce从promiseMap里面获取
   const promiseList = Object.keys(promiseMap).reduce((arr, item) => {arr.push(promiseMap[item]); return arr}, [])
   Promise.race(promiseList).then((index) => {
    // 其中一张加载成功,删除当前promise,让PromiseList小于limit,开始递归,加载下一张
    console.log('end:' + index);
    loadIndexMap[index] = true;
    delete promiseMap[index];
    loadAImage();
   }).catch(e => {
    // 加载失败也继续
    console.log('end:' + e);
    loadIndexMap[e] = true;
    delete promiseMap[e];
    loadAImage();
   })
  }
 }
 loadAImage()
}

startLoadImage(urls, 3)
复制代码

将代码复制到chrome浏览器可以看到下面的运行结果: 1.png 可以看到,所有图片加载完成,在没有失败的情况下,打印出来all

解析:根据规则5,Promise.race方法接受的参数中有一个promise对象返回结果了就会立即触发成功或者失败的函数。这里利用这个特性,先将promise队列循环加入,直到达到限制,等待racerace后又加入一个promise,利用递归一直循环这个过程,到最后用promise.all捕获剩下的图片加载。

题目六

写出下面函数的执行结果:

Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
复制代码

根据规则4,Promise.resolve(1)会返回一个promise对象并且会将1当做then的参数。而.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。所以最后会输出:1。

题目六

如何取消一个promise? 刚开始拿到这个题会觉得比较蒙,实际上,我们可以用Promise,race的特点,多个Promise有个状态变为完成,就会立马返回。

function wrap(p) {
        let obj = {};
        let p1 = new Promise((resolve, reject) => {
          obj.resolve = resolve;
          obj.reject = reject;
        });
        obj.promise = Promise.race([p1, p]);
        return obj;
      }

      let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(123);
        }, 1000);
      });
      let obj = wrap(promise);
      obj.promise.then(res => {
        console.log(res);
      });
      // obj.resolve("请求被拦截了");
复制代码

一旦开发者在1秒内主动调用obj.resolve,那么obj.promise方法就会被替换成我们自己的方法,而不会执行let promisethen方法,实现上比较巧妙。

总结

promise对象在JavaScript中的使用相对复杂,因为写法多变,而且灵活,提供的方法又比较复杂难懂,在ES6普及的今天,使用范围也广,所以会高频的出现在面试过程中。

相关阅读:

Promise讲解

前端异步是什么?哪些情况下会发生异步?

知道html5 Web Worker标准吗?能实现JavaScript的多线程?

学习如逆水行舟,不进则退,前端技术飞速发展,如果每天不坚持学习,就会跟不上,我会陪着大家,每天坚持推送博文,跟大家一同进步,希望大家能关注我,第一时间收到最新文章。

个人公众号:长按保存关注