前端面试-Promise

2021/5/10 InterviewJavaScriptPromise

# 请求封装成Promise

链接:前端数据请求(axios,jQuery,原生)

# then可以接受失败回调吗

可以,它接收成功和失败的回调。

Promise.resolve().then(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => PromiseLike<never>)
1

# 异步加载图片

/**
 * 图片异步加载
 * @param { string } url
 * @returns {Promise<HTMLImageElement>}
 */
function loadImageAsync(url) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.onload = function () {
      resolve(image);
    };
    image.onerror = function () {
      reject(new Error(`Could not load image at: ${url}`));
    };
    image.src = url;
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

实现一个有并发限制的请求,以最快速度请求所有资源。需要先并发请求3张图片,当一张图片加载完成后,又会继续发起一张图片的请求,让并发数保持在3个,直到需要加载的图片都全部发起请求。

/**
 * 请求控制
 * @param {string[]} urls 请求地址
 * @param {(url: string) => Promise<string>} handler 请求执行函数
 * @param {number} limit 最大请求数量
 * @returns {Promise<number[]>}
 */
function limitLoad(urls, handler, limit = 3) {
  // 对数组做一个拷贝
  const sequence = [...urls];

  // 并发请求到最大数
  // 这里返回的 index 是任务在 promises 的脚标,用于在 Promise.race 之后找到完成的任务脚标
  const promises = sequence.splice(0, limit).map((url, index) => handler(url).then(() => index));

  // 利用数组的 reduce 方法来以队列的形式执行
  // 返回最快改变状态的 Promise 每一个promise的then都是在上一个race之后调用
  return sequence.reduce((prev, cur, currentIndex) => prev.then(() => Promise.race(promises)).catch((err) => {
    // 这里的 catch 不仅用来捕获前面 then 方法抛出的错误 更重要的是防止中断整个链式调用
    console.error(err);
  }).then((res) => {
    // 用新的 Promise 替换掉最快改变状态的 Promise
    promises[res] = handler(sequence[currentIndex]).then(() => res);
    // 初始值Promise.resolve()开始链式 最后的.then(() => Promise.all(promises)) 是为了全部执行完成后的链式调用
  }), Promise.resolve()).then(() => Promise.all(promises));
}

// 因为limitLoad函数也返回一个Promise,所以当所有图片加载完成后,可以继续链式调用
const images = [
  'https://picsum.photos/200/300?1',
  'https://picsum.photos/200/300?2',
  'https://picsum.photos/200/300?3',
  // 'https://picsum.photos_error/200/300?4', // error src
  'https://picsum.photos/200/300?5',
  'https://picsum.photos/200/300?6',
  'https://picsum.photos/200/300?7',
]
limitLoad(images, loadImageAsync, 3).then(() => {
  console.log('所有图片加载完成');
}).catch((err) => {
  console.error('图片未能全部加载', err);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

另一种实现方式:

function limitLoad(urls, handler, limit = 3) {
  return new Promise((resolve, reject) => {
    const result = [];
    let count = 0;
    let index = 0;

    async function run() {
      const curIndex = index;
      const url = urls[index];
      index++;

      try {
        const res = await handler(url);
        result[curIndex] = res;
      } catch (e) {
        result[curIndex] = null;
      }

      count++;
      if (count === urls.length) {
        resolve(result);
      }
      if (index < urls.length) {
        run();
      }
    }

    for (let i = 0; i < Math.min(limit, urls.length); i++) {
      run();
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 消除异步传染性

参考文章 (opens new window)

let load = (num) => new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(num);
  }, 2000);
});
const f0 = () => {
  console.log('f0');
  const res1 = load(111);
  const res2 = load(222);
  console.log('res1', res1);
  console.log('res2', res2);
  return res2;
};
const f1 = () => {
  console.log('f1');
  return f0();
};
const f2 = () => {
  console.log('f2');
  return f1();
};
const main = () => {
  console.log('main');
  const res = f2();
  console.log(res);
};
function run(fn) {
  const originLoad = load;
  const cache = [];
  let i = 0;
  load = (num) => {
    if (cache[i]) {
      const cacheData = cache[i];
      i++;
      if (cacheData.status === 'fulfilled') {
        return cacheData.data;
      }
      if (cacheData.status === 'rejected') {
        throw cacheData.err;
      }
    } else {
      const result = {
        status: 'pending',
        data: null,
        err: null,
      };
      cache[i] = result;
      throw originLoad(num).then((res) => {
        result.status = 'fulfilled';
        result.data = res;
      }).catch((err) => {
        result.status = 'rejected';
        result.err = err;
      });
    }
  };
  const execute = () => {
    try {
      i = 0;
      fn();
    } catch (error) {
      if (error instanceof Promise) {
        error.then(execute, execute);
      } else {
        throw error;
      }
    }
  };
  execute();
}
run(main);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

代码整体思路

  1. 函数中有异步操作,使用throw中断函数后续的执行。

  2. 异步操作执行完毕时,缓存异步结果(一个异步操作对应一个缓存)。

  3. 重新执行函数,异步操作直接返回缓存内容。

    • 怎么重新执行函数?(使用try...catch捕获错误,就可以在catch重新执行函数)。
    • 什么时机重新执行函数?(当异步有结果后,即Promise改变状态后,即可重新执行)。

优缺点

  • 优点:编写代码时直接编写同步代码即可,不需要使用asyncawait等。
  • 缺点:函数需要多次重复执行,asyncawait只需要执行一次。假如函数有其他大量计算,将影响性能。
  • 共同点:仍然是异步有结果后,才能真正进行下一步操作。

# 实现请求队列

页面上有三个按钮,分别为A、B、C,点击各个按钮都会发送异步请求且互不影响,每次请求回来的数据都为按钮的名字。请实现当用户依次点击A、B、C、A、C、B的时候,最终获取的数据为ABCACB。请求不能阻塞,但是输出可以阻塞。比如说B请求需要耗时3秒,其他请求耗时1秒,那么当用户点击BAC时,三个请求都应该发起,但是因为B请求回来的慢,所以得等着输出结果。

class Queue {
  promise = Promise.resolve();

  execute(promise) {
    this.promise = this.promise.then(() => promise);
    return this.promise;
  }
}

const queue = new Queue();

const delay = (params) => {
  const time = Math.floor(Math.random() * 5);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(params);
    }, time * 500);
  });
};

const handleClick = async (name) => {
  const res = await queue.execute(delay(name));
  console.log(res);
};

handleClick('A');
handleClick('B');
handleClick('C');
handleClick('A');
handleClick('C');
handleClick('B');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
最近更新: 2024年11月12日 18:54:35