Promise应用

Promise应用

Promise在实践中的应用场景其实非常的多,比如最常见的使用场景,就是利用Promise对一些http请求库进行一些简单的封装。除此之外,Promise还可以用来实现异步任务同步执行的任务队列,这个方法在很多场景下也有很大的用处。同时还有我非常喜欢的睡眠函数。

睡眠函数

先来说下睡眠函数吧,虽然在应用场景中使用的比较少。但当我们想在某个固定时间后,去执行某段逻辑,如果我们用比较传统的方式来实现的话,会是下面这样:

const log1 = () => {
  console.log('log1');
};

const runCallbackAfterTimeout = (timeout, callback) => {
  setTimeout(() => {
    callback();
  }, timeout);
};

runCallbackAfterTimeout(1000, log1); // 1s 后打印 1
复制代码

但是如果我们用睡眠函数的话,就可以规避掉回调的形式,同时结合 asyncawait , 可以实现同步书写的方式来书写异步任务:

const sleep = (timeout) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
};

const log1 = () => {
  console.log('log1');
};

const main = async () => {
  await sleep(1000);
  log1();
}

main(); // 1s 后打印 1
复制代码

同步任务队列

在实现异步任务队列之前,我们先来实现一个同步任务队列。

首先我们先来说下什么是同步任务队列,当我们需要同步的依次执行一系列的同步任务时(并且前一个任务的结果值,可以作为参数,传递给下一个任务),我们会将这一系列的任务放到的一个数组里面,并依次执行,我们将其称之为同步任务队列。值得注意的是,同步任务队列的第一个函数,是可以接受多个参数的,而之后的执行的函数,最多只能接受一个参数。

我们首先写一个不需要进行参数传递的版本:

const pipe = (...handlers) => {
  return () => {
    for (let i = 0; i < handlers.length; i++) {
      handlers[i]();
    }
  };
};

const log1 = () => {
  console.log('log1');
};

const log2 = () => {
  console.log('log2');
};

const logs = pipe(log1, log2);

logs(); // log1 log2
复制代码

当然,我们也可以利用 JavaScript 原生的数组方法:

const log1 = () => {
  console.log('log1');
};

const log2 = () => {
  console.log('log2');
};

const pipe = (...handlers) => {
  return () => {
    handlers.forEach((handler) => {
      handler();
    });
  };
};

const logs = pipe(log1, log2);

logs(); // log1 log2
复制代码

接着,我们来试试传递参数,其实传递参数也很简单,我们只需要用一个变量来保存每次每一步函数运行的结果值,而在最开始的函数入参时,我们只需要将初始参数赋值给这个变量即可:

const add = (m, n) => {
  return m + n;
};

const square = (n) => {
  return n * n;
};

const pipe = (...handlers) => {
  return (...initArgs) => {
    let args = initArgs; // 因为刚开始我们传入的参数不定,所以用剩余参数的语法将它存储为数组
    for (let i = 0; i < handlers.length; i++) {
      args = [handlers[i](...args)]; // 为了保持循环的统一性,我们将结果值也存放在数组中,但是其实后续的过程中数组中只有1个值,即上一步的返回值并且是下一步的入参值
    }
    return args[0];
  };
};

const addAndSquare = pipe(add, square);

addAndSquare(1, 2); // 9
复制代码

同样的,我们也可以利用 JavaScript 原生的数组方法对其进行改写。因为我们一直用到了一个变量来缓存中间状态值,所以利用 reduce 方法来对其进行改写:

const add = (m, n) => {
  return m + n;
};

const square = (n) => {
  return n * n;
};

const pipe = (...handlers) => {
  return (...initArgs) => {
    return handlers.reduce((args, handler) => {
      return [handler(...args)];
    }, initArgs)[0];
  };
};

const addAndSquare = pipe(add, square);

addAndSquare(1, 2); // 9
复制代码

异步任务队列

异步任务队列就是说我们在队列里的任务是异步的了。在经历过同步任务队列代码的洗礼过后,我相信我们就不用在写不接受参数的版本了,我们可以直接来写正常传递参数的版本。

首先,我们还是来写一个通过 for 循环实现的版本:

const sleep = (timeout) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
};

const addAsync = async (m, n) => {
  await sleep(2000);
  return m + n;
};

const squareAsync = async (n) => {
  await sleep(2000);
  return n * n;
};

const asyncPipe = (...handlers) => {
  return async (...initArgs) => {
    let args = initArgs;
    for (let i = 0; i < handlers.length; i++) {
      args = await handlers[i](...args);
      args = [args];
    }
    return args[0];
  };
};

const addAndSquareAsync = asyncPipe(addAsync, squareAsync);

const main = async () => {
  const res = await addAndSquareAsync(1, 2);
  console.log(res);
};

main(); // 4s 后打印 9
复制代码

这个版本的优势是写起来容易,读起来简单,但是在某些 eslint 配置中会报错。

同样的,我们可以来通过 reduce 来对其进行改写。不过这个版本会稍微有些复杂,需要对Promise的基本功掌握好点。基础差的同学可以结合我前几篇文章再复习下。

那么开始:

// 省略其余代码
const asyncPipe = (...handlers) => {
  return (...initArgs) => {
    const handlersList = handlers.slice(1);
    return handlersList.reduce((p, handler) => {
      return p.then((nextArgs) => {
        return handler(nextArgs);
      });
    }, handlers[0](...initArgs));
  };
};
复制代码

这里和同步任务队列不一样的对方在于我们需要先单独处理下进入reduce函数的初始值,使其为第一个方法的结果值。因为我们在后续迭代过程中用到的变量p其实是一个Promise对象。

并且,因为我们初始入参有可能是多个参数,所以利用Promise.resolve(initArgs)来当作reduce的初始值也是不合理的,因为初始传入的 initArgs 是数组,而这必须保证后续的 nextArgs依然是数组,但是handler(nextArgs)的结果是一个Promise对象。

其实这种写法就是类似于将一个长长的Promise链的构造代码压缩在一个循环中了,严格意义上和 for 循环版本的思路并不一样。

网络请求

Promise还可以对网络请求进行一些简单的封装,比如当下流行的axios库,当我们在使用的时候,往往会在上层进行一层简单的封装,把 code 是否等于0的逻辑抽离出来,然后暴露给业务层成功的数据或者失败的信息。

这部分的代码今天就先不写了,我个人能想到的就有三种思路,可以提供给大家,然后大家顺着思考下:

  1. 利用 new Promise 构造函数;
  2. 利用 Promise.prototype.then 方法;
  3. 利用 asyncawait 处理。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享