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