Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

Prettier は結局何をやっているのか?コードの動作は変わってしまわないのか?

結論: AST を作って、それを元にコードをフォーマットしているから、プログラムの動作は一切変わらない

結論をもう書いたので、意味が分かった人はここより下は不要。ちょっとだけ用語とかの解説をする。

そもそも AST とは?

AST とは Abstract Syntax Tree のこと。超簡単にいえばインデントなどを消して、動作に関係する箇所だけを抜き出した構文木のこと。

たとえば以下の3つはいずれも同じ AST になる。

(1 + 2) * 3
(1+2)*
3
(((1+2))*3)

上の図はJAVASCRIPT AST VISUALIZER で生成した AST。3つとも同じ AST が生成される。このように、AST にはインデントや余分なカッコなどの余計な情報が一切含まれていない。

AST が同じなので、Prettier にかけてみると同じコードが出力される。Prettier にかけた結果がこちら

つまり Prettier は一旦正規化してからコードを生成している

AST について分かってしまえば Prettier が何をやっているかはすぐ理解できる。元のコードから AST を生成して(=正規化)、それをもとに整形コードを組み立てているのだ。一旦 AST の状態にすることでインデントなど整形に関する情報をそぎ落とし、そこから組み立てることで一貫性のある整形済みコードが生まれる。

以上の説明から分かる通り、Prettier はプログラムの動作に関係ない部分だけ変更している。 AST に変換するときもそうだし、AST からコードを生成するところもそうだ。コードのフォーマットに関する箇所しか変更していない。

ちなみに: Prettier は純粋な AST を組み立ててる訳ではない

Prettier はあくまで AST が変わらないように変換するだけで、内部では空白情報を持った AST を使っているそうです。

参考資料

Prettier のしくみ - Speaker Deck

↑ Prettier の内部動作については、Prettier コントリビュータの方のこの資料が分かりやすい。

スマートホームをスマホから操作した〜〜〜い!!

いちいち「Alexa、リビングの電気をつけて」みたいに言うの、かったるくないですか?僕は仕事で使ってるMacの充電器をスマートプラグで管理してるので、いちいち「Alexa、充電つけて」と言わないと充電ができません。これが地味に面倒だったので、スマホのホーム画面から家電のオンオフを操作できるようにしました。

方法: Google アシスタントで家電の操作ルーチンを作成する

大雑把な方針は、まずルーチンを作成して、そのショートカットをホーム画面に追加する。これで、ホーム画面から1タップで家電を操作できる。

まずルーチンの作成について。

f:id:panda_noir:20220306123930j:plain

↑Google home を起動するとこういう画面のはず。ルーチンを選択する。

f:id:panda_noir:20220306124000j:plain

ルーチン一覧の下に追加ボタンがあるのでタップする。

f:id:panda_noir:20220306124020j:plain

開始条件は「音声コマンド」を選択して任意のフレーズを入れる。あとはアクションを設定して保存する。

ホーム画面にアイコンを追加する

AOS であればルーチンのショートカットをホーム画面に追加できる。

iOS はもう一工夫いるので、iOSについてのみ解説する。

ショートカットを使ってホーム画面にアイコンを追加する(iOSのみ)

まず「ショートカットアプリ」をインストールして起動する。そしてショートカット追加ボタンをタップする。

f:id:panda_noir:20220306124502j:plain

↑するとこのような画面になる。この画面でまず「アクションを追加」をタップして「Googleで調べる」コマンドを追加する。question のところにルーチン作成時に設定したフレーズを入れる。

これで準備完了なので、あとはホーム画面に追加すればOK。

f:id:panda_noir:20220306123539j:plain

↑共有メニューにホーム画面に追加という項目がある。

ちなみに、ショートカットのウィジェットからも操作可能。僕はこっちを使ってる。 f:id:panda_noir:20220306123535j:plain

touchcancel イベントをハンドリングしないと iPad で大変なことになるぞ

結論: iPad は4本指で上にスワイプするとホーム画面に戻る便利機能があります。これをブラウザで行うと touchstart → touchmove → touchcancel の順で発火します。 touchend が永遠に発火しません。

結論で言いたいことは完結してるので、以下はその補足説明です。

iPad の4本指ジェスチャーについて

iPad で4本指まとめて上へスワイプすると、ホームへ戻るかマルチタスク画面を開けます。これがすごく便利で毎日100回くらいやってます。マジです。

4本指以外にも3本指のジェスチャもたくさんあります。さて、もうお分かりかと思いますが、iPad に対応するにはマルチタップ前提でアプリを作る必要があります。

4本指で上ヘスワイプしたときの挙動

4本指でスワイプしようとすると、touchstartがまず発火します。次にtouchmoveが発火します。最後にtouchcancelが発火して終了です。 touchendは呼ばれません

touchstart イベントは3本分がまとめて1回で発火したりします。touchcancel は1回で4本分すべてキャンセルされます。何度か試してみましたがいずれも touchcancel は1回だけでした。

というわけで、touchcancel イベントは iPad に対応する場合は必ず設定しましょう、という話でした。

検証用ページ

今回、これを検証するにあたって touch event をログ表示する簡易ページを作りました。実際に試してみてください。

condescending-browser-ykh54i - CodeSandbox

高FPSをたたき出すために UncontrolledComponent にしてみたけど、そんなに変わらなかった話

タイトルの通りです。React の再レンダリングを介さずにDOMを直接更新する方が高FPSを出せるか実験してみたけど、そんなに変わらなかったという話です。

ことの発端

ストップウォッチを作っていて、こんな感じの ControlledComponent を書いていました。とてもオーソドックスなコードです。

type TimerState = 'idoling' | 'working';

// ControlledStopwatch は prop で受け取った情報を描画するだけ。内部にステートを持たない
const ControlledStopwatch: FC<{
  onStart: (startAt: number) => void;
  onFinish: (endAt: number) => void;
  currentTime: number | null;
  state: TimerState;
}> = ({ onStart, onFinish, state, currentTime }) => {
  return (
    <div>
      {state === 'idoling' ? (
        <button onClick={() => onStart(performance.now())}>start</button>
      ) : (
        <button onClick={() => onFinish(performance.now())}>stop</button>
      )}
      {currentTime && <div>{currentTime / 1000} seconds</div>}
    </div>
  );
};

const Controlled: FC = () => {
  const [state, setState] = useState<TimerState>('idoling');
  const startAt = useRef(0);
  const [currentTime, setCurrentTime] = useState<number | null>(null);
  const fps = 120;
  useEffect(() => {
    if (state === 'working') {
      // 1000/fps ミリ秒ごとに表示を更新する
      const id = setInterval(() => {
        setCurrentTime(performance.now() - startAt.current);
      }, 1000 / fps);
      return () => clearInterval(id);
    }
  }, [fps, state]);

  return (
    <div className='App'>
      <ControlledStopwatch
        currentTime={currentTime}
        state={state}
        onStart={(val: number) => {
          setState('working');
          startAt.current = val;
        }}
        onFinish={(endAt: number) => {
          setState('idoling');
          console.log(endAt - startAt.current);
        }}
      />
    </div>
  );
};

このストップウォッチは、1000/fps ミリ秒ごとに setCurrentTime を行い、React の再レンダリングをしています。しかし、書き換えるべき箇所は時刻の部分のみです。ならば 再レンダリングはややコストが高いのでは? と思ったのが検証しようと思ったきっかけです。

UncontrolledComponent で書き直してみる。

UncontrolledComponent は state をコンポーネント内部で持っている(親がステートをコントロールできない)コンポーネントです。一見すると親でステートを持てないのはデメリットにしか見えませんが、逆にいえば 「親がステートを管理しなくていい」コンポーネント です。

親は子のレンダリングに関して一切の責任を負わないため、再レンダリングせずに直接 DOM を更新して書き換えても問題ありません。そのため、高FPSが求められる作業に向いていそうに思いました。結論をいうと大差なかったんですが。

というわけで書き直したデモがこちらになります。

codesandbox.io

結果: 目に見えた差はなかった

結果は上のデモをみてわかる通り、 目でみてわかるレベルの差はありませんでした 。60fpsで録画して確認してみたところ、どちらも60fps以上はバッチリ出ていたので、差を視認するのはかなり難しいはずです。

というわけで、 高いFPSを求めて UncontrolledComponent に書き直すのはそんなに意味なさそう という検証でした。