1️⃣

【4言語】 ToDoApp つくってみた、い!~JavaScript編~

に公開3件

はじめに

以下の記事の続きです。
https://zenn.dev/haru98da/articles/210824b54a93d8

今回はJavaScript編です。

コード本編

const fs = require("fs");

const FNAME = "todos.json";

const readline = require("readline");

const rl = readline.createInterface({
	input: process.stdin,
	output: process.stdout
});

function load() {
	if (!fs.existsSync(FNAME)) return [];
	try {
		const data = fs.readFileSync(FNAME, "utf-8");
		return JSON.parse(data);
	} catch (e) {
		console.log("Failed to load,starting fresh:", e);
		return [];
	}
}

function save(todos) {
	try {
		fs.writeFileSync(FNAME, JSON.stringify(todos, null, 2), "utf-8");
	} catch (e) {
		console.log("Failed to save:", e);
	}
}

function listTodos(todos) {
	if (todos.length === 0) {
		console.log("No todos.");
		return;
	}
	todos.forEach((t, i) => {
		const status = t.done ? "[x]" : "[ ]";
		console.log(`${i + 1}.${status} ${t.text}`);
	});
}

function addTodo(todos, input) {
	const text = input.trim();
	if (!text) {
		console.log("empty-ignored");
		return;
	}
	todos.push({ text, done: false });
	console.log("added");
}

function deleteTodo(todos, index) {
	if (index < 1 || index > todos.length) {
		console.log("out of range");
		return;
	}
	const removed = todos.splice(index - 1, 1);
	console.log("Removed", removed[0].text);
}

function toggleDone(todos, index) {
	if (index < 1 || index > todos.length) {
		console.log("out of range");
		return;
	}
	todos[index - 1].done = !todos[index - 1].done;
	console.log("Toggled.");
}

function main() {
	let todos = load();
	function menu() {
		console.log("\n1:add 2:list 3:delete 4: toggle done 0:exit");
		rl.question(">> ", (cmd => {
			if (cmd === "1") {
				rl.question("Add :", (text) => {
					addTodo(todos, text);
					save(todos);
					menu();
				});
			} else if (cmd === "2") {
				listTodos(todos);
				menu();
			} else if (cmd === "3") {
				rl.question("Delete index: ", (num) => {
					deleteTodo(todos, Number(num));
					save(todos);
					menu();
				});
			} else if (cmd === "4") {
				rl.question("Toggle index: ", (num) => {
					toggleDone(todos, Number(num));
					save(todos);
					menu();
				});
			} else if (cmd === "0") {
				save(todos);
				console.log("Bye.");
				rl.close();
			} else {
				console.log("Unknown command");
				menu();
			}
		}));
	}
	menu();
}

main();

解説編

  1. 道具の準備~ Modules ~
  • require : 外部の機能を取り込むための文言
  • fs : FileSystem の略。PC 内のファイルを読み書きするのに使う
  • readline : ターミナルでユーザからの入力を受け取るのに使う
    • process.stdin : キーボード入力
    • process.stdout : 画面出力
  1. ファイルの読み書きと JSON
  • JSON.parse/JSON.stringify : プログラムの中では、オブジェクト、ファイルに保存するときはテキストにする必要がある
  • fs.writeSync/fs.readFileSync : ここで読み書き
  1. 配列メソッド
  • push : 配列の末尾にデータを足す
  • forEach : 要素を 1 つずつ取り出して処理する(for 文に近い)
  • splice : 指定したインデックスから指定した数の要素を削除。そして、削除された要素を配列として返す
  1. 再起とコールバック
  • main 関数の中に menu 関数があり、その中でさらに menu()を呼びだす(再帰)
  • rl.question は、質問をしてユーザが Enter を押した後で、{}の中のコードが動く(コールバック)
  1. try-catch
  • ファイルの中身が壊れていたり、JSON として読み込めなかったりする場合、クラッシュさせずに、空のリストとして再開させる
  1. ユーザへの配慮
  • 入力チェック(ガード処理) : 空っぽの TODO を追加させない、存在しない番号を消そうとしたら止める

追加解説

Q.頻出する"Sync"って何?
A.同期処理の事、その処理が終わるまで、次の行に進まずに待機する。確実にその処理を行ってから、次に進みたいときに使う。

Q.同期/非同期,コールバックって何?
A.同期処理: ある処理が完了するまで、次の処理は必ず待つ。
非同期処理: 待たずに別行動。
コールバック: 非同期処理における、時間のかかる処理の完了報告の仕組み。コールバック関数の中に、待っていた処理を記述することが多い。

Q.fs.existsSyncはなぜ非推奨なのか
A.同期処理であるため、ここの処理で長期ストップしてしまうと、アプリケーション全体のパフォーマンスが低下する可能性があるため。このため、同期版のfsメソッドはメイン処理では使わないことが多い。
(ベストプラクティスとしては、try-catchもしくは、fs.promises.access()を使用する。)

Discussion

junerjuner

コールバック: 非同期処理における、時間のかかる処理の完了報告の仕組み。コールバック関数の中に、待っていた処理を記述することが多い。

Promise.withResolvers を使って Promise に変換しがちですね。複数回呼ばれるなら ReadableStream の内部コントローラを使って for await ... に変換しがち

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers

https://qiita.com/juner/items/258eae705abd8d5c47e9

くわだよ。くわだよ。

コメントありがとうございます!
Promiseがよく理解できてなかったので、調べなおしてみました!!

そもそもpromiseとは、
非同期処理のためのオブジェクト。より読みやすく管理しやすくするために導入された。

let promise = new Promise(function(resolve,reject){
	//executor
});

この時のresolveとrejectがコールバック

  • resolve(value) : ジョブが正常に終了した場合。結果のvalueを待つ
  • reject(error) : エラーが発生した場合

流れ
executorは自動的に実行を行い、ジョブの実行を試みる。
ジョブが完了したとき、resolveが呼ばれる
エラーの場合にはrejectが呼ばれる

Promise.with.Resolvers()とは、
Promise APIに追加された静的メソッド。"非同期処理をラップするpromiseを手動で作成し、
その制御を外部で行う"ことがより簡潔に記述できる

従来のpromiseとPromise.with.Resolvers()

  • 従来のPromiseだと、resolve/rejectを外部から利用するためには、関数スコープ外の変数に代入する必要があった。
  • Promise.with.Resolvers()を使用すると、Promiseの作成と、その解決/可否の制御関数をより簡潔に分割して取得できる。
  • Promise.with.Resolvers()は比較的新しい機能であるため、実行環境に注意が必要
  • Promiseコンストラクタ内で完結する単純な非同期処理をラップする際には、new Promise()を使うほうが読みやすい場合がある

なるほど...?
理解できたようなあいまいなような...
参考資料も添付してくださって本当にありがとうございます!!
まだまだ勉強頑張ります!!

junerjuner

例えば function a(callback) な 一回呼ぶと 必ず1回 非同期に callback が実行される API がある場合

function b(value) {
  // doing...
  a(b);
}
a(b);

みたいに書きますが、 Promise.withResolvers() を使うとこう書けます。

while(true) {
   const { promise, resolve } = Promise.withResolvers();
   a(resolve);
   const value = await promise;
   // doing...
}

例えば function c(callback) な 一回呼ぶと 複数回非同期に callback が実行される API がある場合

function d(value) {
  // doing...
}
c(d);

みたいに書きますが、 ReadableStreamの内部コントローラをうまい様にすると……
ちょっと実装書くと長くなるので readable-stream-with-safe-resolvers を使います。

import { withSafeResolvers } from "readable-stream-with-safe-resolvers";
const { enqueue, stream } = withSafeResolvers();
c(enqueue);
for await (const value of stream) {
   // doing...
}

の様にそれぞれネストを浅く、直列的に書くことができます。


ちなみに Promise.withResolvers() の実装自体はとても簡単なので必要に応じて手癖で ポリフィルを書いたりします。

function withResolvers() {
    let resolve, reject;
    const promise = new Promise((res,rej) => [resolve,reject] = [res,rej]);
    return {resolve,reject,promise};
}