Promise 向左,Async/Await 向右?

326次阅读  |  发布于2年以前

1 . 前言

从事前端开发至今,异步问题经历了 Callback Hell 的绝望,Promise/Deffered 的规范混战,到 Generator 的所向披靡,到如今 Async/Await 为大众所接受,这其中 Promise 和 Async/Await 依然活跃代码中,对他们的认识和评价也经历多次反转,也有各自的拥趸,形成了一直延续至今的爱恨情仇,其背后的思考和启发,依旧值得我们深思。

预先声明:

本文的目标并不是引发大家的论战,也不想去推崇其中任何一种方式来作为前端异步的唯一最佳实践,想在介绍下 Promise 和 Async/Await 知识和背后的趣事的基础上,探究下这些争议下隐藏的共识。

2 . Promise

Promise 是异步编程的一种解决方案,相对于传统的回调地狱更加合理和强大。

所谓 Promise,简单来说就是一个容器,里面存储个未来才会结束的时间的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。其内部状态如下:

状态之间的流转是不可逆的,代码书写如下:

function httpPromise(): Promise<{ success: boolean; data: any }> {
  return new Promise((resolve, reject) => {
    try {
      setTimeout(() => {
        resolve({ success: true, data: {} });
      }, 1000);
    } catch (error) {
      reject(error);
    }
  });
}
httpPromise().then((res) => {}).catch((error) => {}).finally(() => {});

从语法角度上看,更加容易理解,但是美中不足的就是不够简洁,无法断点,冗余的匿名函数。

2.1. Promise 是如何实现的?

在刚入行的时候也去研究过《如何实现一个 Promise》这个课题,尝试写了下如下的代码。

class promise {
    constructor(handler) {
        this.resolveHandler = null;
        this.rejectedHandler = null;
        setTimeout(() => {
            handler(this.resolveHandler, this.rejectedHandler);
        }, 0);
    }

    then(resolve, reject) {
        this.resolveHandler = resolve;
        this.rejectedHandler = reject;
        returnthis;
    }
}
function getPromise() {
    return new promise((resolve, reject) => {
        setTimeout(() => {
            resolve(20);
        }, 1000);
    });
}
getPromise().then((res) => {
    console.log(res);
}, (error) => {
    console.log(error);
});

虽然羞耻,但是不得不说当时还是挺满足的,后面发现无法解决异步注册的问题。

const promise1 = getPromise();
setTimeout(() => {
    promise1.then((data) => {
        console.log(data);
    }).catch((error) => {
        console.error(error);
    });
}, 0);

function getFPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(20), 1000);
    });
}
// 执行情况 报错
// Uncaught TypeError: promise1.then(...).catch is not a function
// Uncaught TypeError: resolve is not a function

// vs 官方Promise实现
const promise2 = getFPromise();
setTimeout(() => {
    promise2.then((data) => {
        console.log(data);
    }).catch((error) => {
        console.error(error);
    });
}, 0);
// 执行情况,符合预期
// 20

对于这一部分有兴趣的同学可以自行查找标准版的实现方案,不过在这个探索过程中确实勾起对基础知识的兴趣,这也是本文去挖掘这部分知识的初衷。

接下来看看 Async/Await 吧。

3 . Async/Await

Async/Await 并不是什么新鲜的概念,事实的确如此。

早在 2012 年微软的 C# 语言发布 5.0 版本时,就正式推出了 Async/Await 的概念,随后在 Python 和 Scala 中也相继出现了 Async/Await 的身影。再之后,才是我们今天讨论的主角,ES 2016 中正式提出了 Async/Await 规范。

以下是一个在 C# 中使用 Async/Await 的示例代码:

public async Task<int> SumPageSizesAsync(IList<Uri> uris)
{
    int total = 0;
    foreach (var uri in uris) {
        statusText.Text = string.Format("Found {0} bytes ...", total);
        var data = await new WebClient().DownloadDataTaskAsync(uri);
        total += data.Length;
    }
    statusText.Text = string.Format("Found {0} bytes total", total);
    return total;
}

再看看在 JavaScript 中的使用方法:

async function httpRequest(value) {
  const res = await axios.post({ ...value });
  return res;
}

好的设计总是会想借鉴的,不寒碜。

其实在前端领域,也有不少类 Async/Await 的实现,其中不得不提到的就是知名网红之一的老赵写的 wind.js ,站在今天的角度看,windjs 的设计和实现不可谓不超前。

3.1. Async/Await 是如何实现的?

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

这里引用阮一峰老师的描述:

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

前文有一个 Generator 函数,依次读取两个文件。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面代码的函数 gen 可以写成 async 函数,就是下面这样。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

相对于 Generator 的改进主要集中集中在:

到这里大家会发现,Async/Await 本质也是 Promise 的语法糖:Async 函数返回了 Promise 对象。

来看下实际 Babel 转化的代码,方便大家理解下

async function test() {
  const img = await fetch('tiger.jpg');
}

// babel 转换后
'use strict';

var test = function() {
    var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
        var img;
        return regeneratorRuntime.wrap(function _callee$(_context) {
            while (1) {
                switch (_context.prev = _context.next) {
                    case0:
                        _context.next = 2;
                        return fetch('tiger.jpg');

                    case2:
                        img = _context.sent;

                    case3:
                    case'end':
                        return _context.stop();
                }
            }
        }, _callee, this);
    }));

    return function test() {
        return _ref.apply(this, arguments);
    };
}();

function _asyncToGenerator(fn) {
    return function() {
        var gen = fn.apply(this, arguments);
        return new Promise(function(resolve, reject) {
            function step(key, arg) {
                try {
                    var info = gen[key](arg);
                    var value = info.value;
                } catch (error) {
                    reject(error);
                    return;
                }
                if (info.done) {
                    resolve(value);
                } else {
                    return Promise.resolve(value).then(function(value) {
                        step("next", value);
                    }, function(err) {
                        step("throw", err);
                    });
                }
            }
            return step("next");
        });
    };
}

不难看出最终还是转换成基于 Promise 的调用,但是本来的三行代码被转换成 52 行代码,在一些场景下就带来了成本。

例如 Vue3 并没有采用?.(可选链式操作符符号)的原因:

虽然使用?很简洁,但是实际打包下反而更加冗余了,增加了包的体积,影响 Vue3 的加载速度,这也是 Async/Await 这类简洁语法的痛点。

暂时不考虑深层次的运行性能,仅仅考虑代码使用方式来看,Async/Await 是否完美无缺?

以 “请求 N 次重试” 的实现为例:

/**
 * @description: 限定次数来进行请求
 * @example: 例如在5次内获取到结果
 * @description: 核心要点是完成tyscript的类型推定,其次高阶函数
 * @param T 指定返回数据类型,M指定参数类型
 */

export default function getLimitTimeRequest<T>(task: any, times: number) {
  // 获取axios的请求实例
  let timeCount = 0;
  async function execTask(resolve, reject, ...params: any[]): Promise<void> {
    if (timeCount > times) {
      reject(newError('重试请求失败'));
    }
    try {
      const data: T = await task(...params);
      if (data) {
        resolve(data);
      } else {
        timeCount++;
        execTask(resolve, reject, params);
      }
    } catch (error) {
      timeCount++;
      execTask(resolve, reject, params);
    }
  }
  return function <M>(...params: M[]): Promise<T> {
    return new Promise((resolve, reject) => {
      execTask(resolve, reject, ...params);
    });
  };
}

常见的实现思路是将 Promise 的 Resolve、Reject 的句柄传递到迭代函数中,来控制 Promise 的内部状态转化,那如果用 Async/Await 如何做?很明显并不好做,暴露了它的一些不足:

当然,站在 EMCA 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。

  1. 总结

针对前端异步的处理方案,Promise 和 Async/Await 都是优秀的处理方案,但是美中不足的是有一定的不足,随着前端工程化的深入,一定会有更好的方案来迎合解决这些问题,大家不要失望,未来还是可期的。

从 Promise 和 Async/Await 的演进和纠结中,大家实际能够感到前端人对 JavaScript 世界的辛苦耕作和奇思妙想,这种思维和方式也可以沉淀到我们日常的需求开发中去,善于求索,辩证的去使用它们,追求更加极致的方案。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8