# 非同步的故事
## 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` 的程式,就長得很像了。