1. Preface

So far in front-end development, asynchronous problems have experienced the despair of Callback Hell, the normative melee of Promise/Deffered, the invincibility of Generator, and now the acceptance of Async/Await by the public, in which Promise and Async/Await are still active in code. Their perceptions and evaluations have been reversed many times, and they have their own fans, creating a love-hate relationship that continues to this day, and the thoughts and inspirations behind them are still worth pondering.

Advance notice.

The goal of this article is not to start a debate, nor to promote either approach as the only best practice for front-end asynchrony, but to explore the hidden consensus underneath the controversy, based on an introduction to the knowledge and anecdotes behind Promise and Async/Await.

2. Promise

Promise is a solution for asynchronous programming that is more sensible and powerful than the traditional callback hell.

A Promise is simply a container that stores the result of a time that will only end in the future. Syntactically, a Promise is an object from which messages for asynchronous operations can be retrieved, and it provides a unified API so that all kinds of asynchronous operations can be handled in the same way. Its internal state is as follows.

promise

The flow between states is irreversible and the code is written as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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(() => {});

It is easier to understand from a syntactic point of view, but the beauty is the lack of brevity, the inability to break points and the redundant anonymous functions.

2.1. How is a Promise implemented?

When I first started out, I researched the topic “How to implement a Promise” and tried to write the following code.

 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
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);
});

Although this code sucks, I have to say that it was quite satisfying at the time, and later it turned out that it could not solve the problem of asynchronous registration.

 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
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

Those interested in this section can look up the standard implementation for themselves, but the process of exploring it really does evoke an interest in the basics, which is the reason this article went digging into it in the first place.

Next, let’s look at Async/Await.

3. Async/Await

Async/Await is not a new concept, and indeed it is.

It was formally introduced in 2012 with the release of version 5.0 of Microsoft’s C# language, and then in Python and Scala. After that, the Async/Await specification was formally introduced in ES 2016, which is the subject of our discussion today.

Here is a sample code for using Async/Await in C#.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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;
}

And look at how it is used in JavaScript.

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

In fact, there are quite a few Async/Await-like implementations in the front-end domain

3.1. How is Async/Await implemented?

The ES2017 standard introduced the async function to make asynchronous operations more convenient.

Here is a quote from Mr. Yifeng Ruan’s description.

What is the async function? In a nutshell, it’s syntactic sugar for the Generator function.

The preceding section has a Generator function that reads two files in turn.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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());
};

The function gen in the above code could be written as an async function, as in the following.

1
2
3
4
5
6
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

A quick comparison shows that the async function replaces the asterisk (*) with async and the yield with await, and that’s it.

The main improvements over Generator are focused on.

  • Built-in executors
  • better semantics
  • Promise return values

As you will see here, Async/Await is essentially the syntactic sugar of Promise: the Async function returns a Promise object.

Let’s take a look at the actual Babel transformed code to make it easier to understand

 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
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");
        });
    };
}

It’s easy to see that the call is eventually converted to a Promise based call, but the original three lines of code are converted to 52 lines of code, which in some scenarios imposes a cost.

For example, Vue3 does not use ? (optional chain operator notation) for the following reasons.

vue3

Although the use of ? s simplicity, the actual packaging is more redundant, increasing the size of the package and affecting the loading speed of Vue3, which is a sore point with the simplicity of Async/Await syntax.

Ignoring the deeper runtime performance for the moment, is Async/Await perfect, just in terms of how the code is used?

Take the “request N retries” implementation as an example.

 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
/**
 * @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);
    });
  };
}

A common implementation idea is to pass the handles of the Resolve and Reject of a Promise into the iteration function to control the internal state transformation of the Promise, but what if you use Async/Await? Obviously not very well, exposing some of its shortcomings.

  • Lack of complex control processes, such as always, progress, pause, resume, etc.
  • Internal state is not controlled and error catching relies heavily on try/catch
  • Lack of interrupt methods and the inability to abort

Of course, some of the requirements may be rare from the EMCA specification’s point of view, but if they were included in the specification, it would reduce the headache for front-end programmers when choosing an asynchronous process control library.

4. Summary

Promise and Async/Await are both excellent solutions for front-end asynchronous processing, but they have some shortcomings. As front-end engineering progresses, there will be better solutions to address these issues, so don’t be disappointed, the future is still looking bright.

From the evolution of Promise and Async/Await, we can actually feel the hard work and whimsy of front-end people in the world of JavaScript, and this kind of thinking and approach can also be sunk into our daily requirements development, so that we can use them in a dialectical way to pursue more extreme solutions.