yanyue404 / blog

Just blog and not just blog.
https://yanyue404.github.io/blog/
Other
87 stars 13 forks source link

前端常见需求的解决方案 —— 玩转异步 #248

Open yanyue404 opened 1 year ago

yanyue404 commented 1 year ago

1. 玩转异步

1.1 异步重试

异步任务多次重试尝试获取正确信息,可以分为按时间和按次数两个方向。

按时间重试

 const p = () =>
    new Promise((resolve) =>
      setTimeout(() => {
        let a = Math.random();
        let flag = a > 0.1 ? 0 : 1; // 90% 的可能性返回 0,10% 可能性返回 1
        console.log("本次尝试得到:", flag);
        resolve(flag);
      }, 1000)
    );
/**
 * 按时间异步重试
 *
 * @param {number} [all=3000] 总尝试时间
 * @param {number} [delay=500] 间隔重试时间
 * @param {function} [asyncFn=undefined]  异步方法
 * @param {function} [expectFn=function () {}] 期待的正例
 * @return {promise} 尝试结束回调
 */
function promiseReTry(
  all = 3000,
  delay = 500,
  asyncFn = function () {},
  expectFn = function () {}
) {
  let cur_retry_num = 0; // 重试次数
  return new Promise((resolve) => {
    (async function main() {
      // 异步反应
      const task = asyncFn();
      const res = await task;
      if (expectFn(res) || ++cur_retry_num >= all / delay) {
        resolve();
      } else {
        setTimeout(main, delay);
      }
    })();
  });
}
  // 3000 ms 时间内,每 500ms 重试一次
  promiseReTry(3000, 500, p, (res) => res === 1).then(() => {
    console.log("结束~");
  });
  // 正例:
  // 本次尝试得到: 0 【5 次打印】
  // 本次尝试得到: 1
  // 结束~

  //  反例:
  // 本次尝试得到: 0 【6 次打印】
  // 结束~

按次数重试

实现 Promie.retry ,成功后 resolve 结果,失败后重试,尝试超过一定次数才真正 reject

const p = () =>
  new Promise((resolve, reject) =>
    setTimeout(() => {
      let a = Math.random();
      let flag = a > 0.1 ? 0 : 1;
      console.log(flag);
      flag === 1 ? resolve(flag) : reject(flag);
    }, 1000)
  );

// 大同小异的实现方式

/*   Promise.retry = function (promiseFn, times = 3) {
        return new Promise(async (resolve, reject) => {
          while (times--) {
            try {
              let ret = await promiseFn();
              resolve(ret);
              break;
            } catch (error) {
              if (!times) reject(error);
            }
          }
        });
      }; */

/*  Promise.retry = function (promiseFn, times = 3) {
        return new Promise((resolve, reject) => {
          let count = 0;
          let action = function () {
            promiseFn()
              .then(resolve)
              .catch((err) => {
                count++;
                if (count >= times) {
                  reject(err);
                } else {
                  action();
                }
              });
          };
          action();
        });
      }; */

Promise.retry = function (asyncFn, times = 3) {
  let count = 0;
  function executeFn() {
    return new Promise((resolve, reject) => {
      resolve(asyncFn());
    })
      .then((res) => {
        return Promise.resolve(res);
      })
      .catch((err) => {
        count++;
        if (count >= times) {
          return Promise.reject(err);
        } else {
          return executeFn();
        }
      });
  }
  return executeFn();
};

Promise.retry(p, 3).then(() => {
  console.log("okkk");
});

1.2 任务中断

批量执行异步任务,有任务返回 false 就中断执行并返回结果。

let asyncFn = (val) => {
  return new Promise((resolve) => {
    setInterval(() => {
      resolve(val);
    }, 1000);
  });
};

let tasks = [true, false, false].map((v) => () => asyncFn(v));
// 基础 for 循环
/*       async function run() {
           for (let i = 0; i < tasks.length; i++) {
             const task = tasks[i];
             let res = await task();
             if (!res) {
               return false;
             }
           }
           return true;
         } */
// for of
async function run() {
  for (const task of tasks) {
    let res = await task();
    if (!res) {
      return false;
    }
  }
  return true;
}

async function main() {
  let result = await run();
  console.log("result", result);
}
main(); // result false

1.3 异步竞态

参考 如何解决前端常见的竞态问题 https://juejin.cn/post/7127953386514677790

认识竞态问题

竞态问题的出现的原因是无法保证异步操作的完成会按照他们开始时的顺序进行返回。下面的代码例子:

  const fetch = (duration, data) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(data);
      }, duration);
    });
  };

  setTimeout(() => {
    fetch(1000, "结果是=>第一个请求").then((res) => console.log(res));
  }, 5);
  setTimeout(() => {
    fetch(5000, "结果是=>第二个请求").then((res) => console.log(res));
  }, 10);
  setTimeout(() => {
    fetch(2000, "结果是=>第三个请求").then((res) => console.log(res));
  }, 20);
  // 结果是=>第一个请求
  // 结果是=>第三个请求
  // 结果是=>第二个请求

这就是一个常见的异步竞态的例子,在我们的投保流程中,测算在极端的情况下会碰到这种情况。

使用唯一 id 标识每次请求

  const fetch = (duration, data) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(data);
      }, duration);
    });
  };

  function onlyResolvesLast(fn) {
    // 利用闭包保存最新的请求 id
    let id = 0;
    const wrappedFn = (...args) => {
      // 发起请求前,生成新的 id 并保存
      id++;
      const fetchId = id;
      // 执行请求
      const result = fn.apply(this, args);
      return new Promise((resolve, reject) => {
        // result 可能不是 promise,需要包装成 promise
        Promise.resolve(result).then(
          (value) => {
            // 只处理最新一次请求
            if (fetchId === id) {
              resolve(value);
            }
          },
          (error) => {
            // 只处理最新一次请求
            if (fetchId === id) {
              reject(error);
            }
          }
        );
      });
    };
    return wrappedFn;
  }

  const wrappedFn = onlyResolvesLast(fetch);
  //   注意:请求顺序按先后顺序进入,响应顺序不一致
  setTimeout(() => {
    wrappedFn(1000, "结果是=>第一个请求").then((res) => console.log(res));
  }, 5);
  setTimeout(() => {
    wrappedFn(5000, "结果是=>第二个请求").then((res) => console.log(res));
  }, 10);
  setTimeout(() => {
    wrappedFn(3000, "结果是=>第三个请求").then((res) => console.log(res));
  }, 20);

  // 结果是=>第三个请求

封装指令式 promise

  const fetch = (duration, data) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(data);
      }, duration);
    });
  };
  // 封装指令式 promise https://github.com/slorber/awesome-imperative-promise/blob/master/src/index.ts
  const createImperativePromise = (promiseArg) => {
    let resolve, reject;

    const wrappedPromise = new Promise((_resolve, _reject) => {
      resolve = _resolve;
      reject = _reject;
    });
    // 绑定真实的 promise 回调
    promiseArg &&
      promiseArg.then(
        (val) => {
          resolve && resolve(val);
        },
        (error) => {
          reject && reject(error);
        }
      );

    return {
      promise: wrappedPromise,
      resolve: (value) => {
        resolve && resolve(value);
      },
      reject: (reason) => {
        reject && reject(reason);
      },
      //  将 resolve,reject 设为 null,让 promise 永远不会 resolve/reject。
      cancel: () => {
        resolve = null;
        reject = null;
      },
    };
  };

  const wrappedPromise = new Promise((resolve, reject) => {});

  // Wrap an existing promise and expose some additional imperative methods
  // The existingPromise paramter is optional and the returned promise with resolve/reject when the existing promise do
  const { promise, resolve, reject, cancel } = createImperativePromise(
    wrappedPromise
  );

  cancel(); // 阻止 promise resolved,关闭后下面的 res 就会有结果了

  resolve("some value");

  promise.then((res) => {
    console.log(res);
  });
 // 包装一个异步函数,并确保只有最后一次调用才会解析/拒绝 https://github.com/slorber/awesome-only-resolves-last-promise/blob/master/src/index.ts
  function onlyResolvesLast(fn) {
    // 保存上一个请求的 cancel 方法
    let cancelPrevious = null;

    const wrappedFn = (...args) => {
      // 当前请求执行前,先 cancel 上一个请求
      cancelPrevious && cancelPrevious();
      // 执行当前请求
      const result = fn.apply(this, args);

      // 创建指令式的 promise,暴露 cancel 方法并保存
      const { promise, cancel } = createImperativePromise(result);
      cancelPrevious = cancel;

      return promise;
    };

    return wrappedFn;
  }

  const wrappedFn = onlyResolvesLast(fetch);
  //   注意:请求顺序按先后顺序进入,响应顺序不一致
  setTimeout(() => {
    wrappedFn(1000, "结果是=>第一个请求").then((res) => console.log(res));
  }, 5);
  setTimeout(() => {
    wrappedFn(5000, "结果是=>第二个请求").then((res) => console.log(res));
  }, 10);
  setTimeout(() => {
    wrappedFn(3000, "结果是=>第三个请求").then((res) => console.log(res));
  }, 20);

  // 结果是=>第三个请求