---
# System prepended metadata

title: 非同步的故事

---

# 非同步的故事

## callback

用 node.js 讀檔時，得提供一個 callback function ，好取得結果：

```javascript
fs.access('./webpack.config.js', fs.constants.F_OK, (err) => {
    console.log(`${file} ${err ? 'does not exist' : 'exists'}`);
});
```

如果要讀好幾次檔才能決定該怎麼做：

```javascript
fs.access(filepath1, fs.constants.F_OK, (err) => {
    if (err) return;
    fs.readFile(filepath1, (err) => {
        if (err) return;
        fs.access(filepath2, fs.constants.F_OK, (err) => {
            if (err) return;
            fs.readFile(filepath2, (err, file2) => {
                if (err) return;
                console.log(file1, file2);
            });
        });
    });
});
```

那 callback hell 就出現了。

---

可以把這樣的行為包裝起來：

```javascript
function read2File(filepath1, filepath2, cb) {
    fs.access(filepath1, fs.constants.F_OK, (err) => {
        if (err) return cb(err);
        fs.readFile(filepath1, (err) => {
            if (err) return cb(err);
            fs.access(filepath2, fs.constants.F_OK, (err) => {
                if (err) return cb(err);
                fs.readFile(filepath2, (err, file2) => {
                    if (err) return cb(err);
                    cb(undefined, [file1, file2]);
                });
            });
        });
    });
}
```

## `Promise`

有人為 node 的 fs functions 做了 `Promise` 版，例如 [`fs-extra`](https://www.npmjs.com/package/fs-extra) ：

```javascript
fsex.pathExists(filepath)
    .then(exists =>
        fsex.readFile(filepath)
            .then(file =>
                console.log(file)
            )
    );
```

最棒的是， Promise/A+ 規定的 `Promise::then` 保證傳回來的是另外一個 `Promise` ，所以可以這樣寫：

```javascript
fsex.pathExists(filepath)
    .then(exists => fsex.readFile(filepath))
    .then(file => console.log(file));
```

對排版很偏執的人甚至可以寫成：

```javascript
Promise.resolve(filepath)
    .then(filepath => fsex.pathExists(filepath))
    .then(exists => fsex.readFile(filepath))
    .then(file => console.log(file));
```

錯誤很好處理：

```javascript
function show(obj) {
    console.log(obj);
    return obj;
}

function panic(fallback) {
    return function(err) {
        console.error(err);
        return fallback;
    }
}

function readConfig(configPath) {
    return fsex.readFile(configPath)
        .then(show)
        .catch(panic(defaultConfig))
    // 回傳了什麼呢？
}
```

也很好遞迴：

```javascript
function delay(time) {
    return function(obj) {
        return new Promise((resolve) => setTimeout(resolve, time, obj));
    }
}

function neverGiveUp(configPath) {
    return fsex.readFile(configPath)
        .catch((err) => Promise.resolve()
            .then(delay(5000))
            .then(neverGiveUp(configPath))
        );
}
```

可惜當結果彼此相依時，還是得蓋金字塔：

```javascript
function read2File(filepath1, filepath2) {
    return fsex.readFile(filepath1)
        .then(file1 =>
            fsex.readFile(filepath2)
                .then(file2 => [file1, file2])
        );
    // 回傳了什麼呢？
}

function read2File(filepath1, filepath2) {
    return fsex.readFile(filepath1)
        .then(file1 => {
            return Promise.all([file1, fsex.readFile(file1.configPath)])
        });
    // 回傳了什麼呢？
}

Promise.all([fsex.readFile(filepath1), fsex.readFile(filepath2)])
```

## async/await

async/await 語法出現後，可以把用上 `Promise` 的程式，寫得像本來 sync 的程式一樣：

```javascript
async function read2File(filepath1, filepath2) {
    const file1 = await fsex.readFile(filepath1);
    const file2 = await fsex.readFile(filepath2);
    return [file1, file2];
    // 回傳了什麼呢？
}

const x = await read2File('/etc/log/system.log', '/home/caasi/.vimrc');
// Promise<[string, string]>
```

但要注意的是， `await` 後，發生的就是未來的事了，也因為這樣， `async` function 傳回的一定是個 `Promise` 。

還可以用熟悉的 `try/catch` 處理錯誤：

```javascript
async function readConfig(configPath) {
    let config;
    try {
        config = await fsex.readFile(configPath)
        console.log(config);
    } catch (err) {
        console.error(err);
        config = defaultConfig;
    }
    return config;
    // 回傳了什麼呢？
}
```

## continuation-passing style

把計算結果以 callback 傳遞的程式，又稱為 CPS(continuation-passing style) 程式，比較一下：

```javascript
const add = (a, b) => a + b;

const add_ = (a, b, cb) => cb(a + b);
```

礙於 function signature 的形狀，如果想一直 `add_` 下去：

```javascript
add_(2, 3, (x) =>
    add_(5, x, (y) =>
        add_(7, y, (z) =>
            console.log(z)
        )
    )
);
```

金字塔就跑出來了。

---

不過在 js 中有 first-class function ，可以把 `add_` 的形狀從：

```javascript
const add_ = (a, b, cb) => cb(a + b);
```

改成：

```javascript
const add$ = (a, b) => (cb) => cb(a + b);

function add$(a, b) {
    return function(cb) {
        return cb(a + b);
    }
}
```

然後：

```javascript
const then$ = (cont, f) => (cb) => cont(res => f(res)(cb));

let cont = add$(2, 3);
cont = then$(cont, (x) => add$(5, x));
cont = then$(cont, (y) => add$(7, y));

cont((z) => console.log(z));
```

更極端一點：

```javascript
const add$$ = (a) => (b) => (cb) => cb(a + b);

let cont = add$$(2)(3);
cont = then$(cont, add$$(5));
cont = then$(cont, add$$(7));

cont(console.log);
```

等到未來 js 加上 [`|>`(pipeline operator)](https://github.com/tc39/proposal-pipeline-operator) ，可以這樣寫：

```javascript
// cont 和 f 交換了位置
const then$$ = (f) => (cont) => (cb) => cont(res => f(res)(cb));

const cont =
    add$$(2)(3)
        |> then$$(add$$(5))
        |> then$$(add$$(7))

cont(console.log);
```

和前面傳回 `Promise` 的程式，就長得很像了。
