歴史的遺物callback interfaceの紹介

これは[Chromium Browser アドベントカレンダー](https://qiita.com/advent-calendar/2017/chromium)の五日目の記事です。本記事では[Web IDL](https://heycam.github.io/webidl/)規格における[callback interface](https://heycam.github.io/webidl/#dfn-callback-interface)と[callback function](https://heycam.github.io/webidl/#idl-callback-functions)という2つのコールバック仕様を説明します。ウェブ開発者を想定読者としていますが、ウェブ開発の役には立たない内容となっていますので、トリビアとしてご活用ください。



# 二つのコールバックの仕組み

ウェブ開発をしている方々はなにかとコールバック関数を使う機会があると思います。一番有名なのは[`EventTarget`](https://dom.spec.whatwg.org/#interface-eventtarget).[`addEventListener`](https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener)でしょうか。

```javascript:
document.body.addEventListener('click', () => {
    console.log("you've clicked");
}, false);
```

他にもアニメーション処理を実装する時などには[`Window`](https://html.spec.whatwg.org/multipage/window-object.html#the-window-object).[`requestAnimationFrame`](https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#dom-window-requestanimationframe)を使うことでしょう。

```javascript:
window.requestAnimationFrame(() => {
    console.log("is this ... animating?");
});
```

どちらも同じコールバックに見えますが、実はこの二つは異なる仕様で実装されています。`EventTarget.addEventListener`はcallback *interface*という仕様で、`Window.requestAnimationFrame`はcallback *function*という仕様で実装されてます。次のコードを実行してみると違いを見ることができます。

```javascript:
obj = {
    handleEvent: () => { console.log("make a difference"); },
};

document.body.addEventListener('click', obj, false);
// => undefined
// |handleEvent| will be invoked when clicked.

window.requestAnimationFrame(obj);
// => throw a TypeError: |obj| is not a function.
```

`requestAnimationFrame`は例外を投げて動かないのに対して、`addEventListener`はきちんと動きます。クリックするとコンソールにテキストが表示されます。`addEventListener`はどうやら`obj`のプロパティである`handleEvent`を実行しているようです。`obj`は関数ではないのになぜエラーにならないのか？ `handleEvent`というプロパティが実行されるのはどうしてなのか？ 不思議ですね。次節で規格の詳細を少しだけ見てみましょう。



# callback interfaceの仕様

[`addEventListener`](https://dom.spec.whatwg.org/#interface-eventtarget)に関する仕様を抜き出してみました。

```webidl:EventTarget.addEventListener
interface EventTarget {
  void addEventListener(
      DOMString type,
      EventListener? callback,
      ...<snip>...);
};
```
```webidl:EventListener
callback interface EventListener {
  void handleEvent(Event event);
};
```

`addEventListener`の第2引数は`EventListener`型であることが分かります。`EventListener`型は`callback interface`というキーワードで定義されており、`handleEvent`という関数を持つように定義されています。これは第2引数のオブジェクトから`handleEvent`という名前のプロパティを読み出して、そのプロパティの値である関数を(`Event`型の引数を渡して)実行せよ、という意味です。

- `addEventListener`は第2引数に`EventListener`という名前のcallback interfaceを受け取るAPIだった。
- `EventListener`型のオブジェクトは`handleEvent`という名前のプロパティを持つこと、その値は`Event`型の引数を一つ受け取る関数であることが期待されていた。

ここで`handleEvent`というプロパティ名は各Web APIの規格で自由に命名してよく、決して固定されている訳ではありません。Web APIを策定している人たちで好きな名前を付けることができます。しかし実際にはほぼすべての規格で`handleEvent`という名前を使っています。わたしが見つけられた唯一の例外は[`NodeFilter`](https://dom.spec.whatwg.org/#interface-nodefilter)型が`acceptNode`という名前を使っている例だけでした。(`NodeFilter`許すまじ...)

上記の説明だけでは`addEventListener`の第2引数は必ず`handleEvent`というプロパティを持つオブジェクトでなければならないかのように見えますが、実際にはただの関数を渡しても動きます。これは以下のルールが[callback interfaceの規格](https://heycam.github.io/webidl/#es-user-objects)で定められているからです。

1. 以下の条件を満たすcallback interfaceを *single operation callback interface* と呼ぶ。
    1. interfaceの継承を使っていない
    1. attributeを定義していない
    1. regular operationがoverloadを除き一つしか存在しない
1. single operation callback interfaceで且つ与えられたオブジェクトが関数である場合には、その関数を呼び出す。

Web IDLの専門用語が大量に現れて難しそうですが、現実は随分と簡単です。single operation callback interfaceの条件に該当しないcallback interfaceはわたしの知る限り存在しません(Chromiumの実装の中にも見つかりません)。従って上記のルールは「関数を渡せば、その関数が呼び出される」という一言に集約されます[^non-single-operation-callback-interface]。尚、このルールは`handleEvent`プロパティの実行よりも優先します。

[^non-single-operation-callback-interface]: 将来的にsingle operation callback interfaceに該当しないcallback interfaceが追加される可能性はきわめて低いとわたしは考えています。複数のコールバックを必要とするWeb APIは複数のcallback functionを受け取るように定義するのが最近の動向です。

```javascript:
f = () => { console.log("wolf"); };
f.handleEvent = () => { console.log("ocelot"); };

document.body.addEventListener('click', f, false);
// Which of |f| or |f.handleEvent| will be invoked?
```

規格が正しく実装されていれば、コンソールには`"ocelot"`ではなく`"wolf"`と表示されるはずです。以上がcallback interfaceの基本仕様ですが、次節ではもう少しだけ細かい部分を見てみましょう。



## handleEventプロパティの読み出しはいつ行われるか？

`handleEvent`プロパティに保存されている関数はいつ読み出されるのでしょうか？ `addEventListener`を呼び出した時？ それともコールバックの実行の直前でしょうか？ これは後者だと[規格](https://heycam.github.io/webidl/#call-a-user-objects-operation)ではっきりと決められています。コールバックを呼び出す度に毎回プロパティを読み出すべし(=読み出した関数をキャッシュしてはいけない)、と決められています。これは同時に、コールバックを登録する際(`addEventListener`呼び出し時)には、プロパティが定義されている必要がないことにもつながります。

```javascript:
obj = {};

document.body.addEventListener('click', obj, false);
// no error at this point

document.body.click();
// nothing happens (or a TypeError on console)

obj.handleEvent = () => { console.log("cascade"); };
document.body.click();
// prints "cascade"

obj.handleEvent = () => { console.log("reverse"); };
document.body.click();
// prints "reverse"
```

ちなみに`handleEvent`プロパティの関数を実行する場合には、`obj`を`this`として渡すことが規格で決められています。

```javascript:continued
obj.message = "mills";
obj.handleEvent = function() {
    console.log(this.message);
    this.message = "mess";
};

document.body.click();
// prints "mills"
console.log(obj.message);
// prints "mess"
```



## callback interfaceの特徴を活かすには？

callback interfaceを受け取るWeb APIに関数ではないオブジェクトを渡してしまうと、`handleEvent`プロパティの読み出しがコールバックの呼び出し毎に行われます。プロパティ値はキャッシュもされず、毎回読み出されるので、これは実行時ペナルティになります。素直にただの関数をコールバックとして登録するのが一番良いでしょう。

オブジェクト指向プログラミングをしている方は、レシーバオブジェクトとメソッドのbindの役割を任せられると思うかもしれませんが、わたしはお勧めできません。メソッド名が`handleEvent`に固定されてしまいますし、JITコンパイルが効かなくなる[^out-of-jit-compile]ため遅いですし、必要に応じて自分でbindした方がよいです。

[^out-of-jit-compile]: Chromiumではcallback interfaceの実装はJavaScriptエンジンではなく、レンダリングエンジンで行ってます。JavaScript言語の機能ではなく、Web APIの機能だからです。その結果JavaScriptエンジンのJITコンパイルの対象外になっています。他のブラウザ実装でも効率が良くなることはないと思われます。



# まとめ

長々と書きましたが、コールバックには常に関数を登録するとよい、というのが本記事の結論です。callback interfaceの細かい仕様を覚えてしまった方は忘れてしまって構いません。トリビア以外に使い途がありませんから。ウェブ開発者の視点からはcallback interfaceとcallback functionを区別する必要もありません。どちらも関数を渡せば動きます。



# おまけ

callback functionも一応説明しておきます(忘れてた訳じゃないよ)。[`requestAnimationFrame`](https://html.spec.whatwg.org/multipage/window-object.html#window)の仕様は次のようになっています。

```webidl:Window.requestAnimationFrame
interface Window : EventTarget {
  unsigned long requestAnimationFrame(FrameRequestCallback callback);
};
```
```webidl:FrameRequestCallback
callback FrameRequestCallback = void (DOMHighResTimeStamp time);
```
```webidl:DOMHighResTimeStamp
typedef double DOMHighResTimeStamp;
```

`requestAnimationFrame`の引数は[`FrameRequestCallback`](https://html.spec.whatwg.org/multipage/window-object.html#framerequestcallback)型であることが分かります。`FrameRequestCallback`型は、`callback interface`というキーワードの代わりに、`callback`というキーワードで定義されており、関数型を定義しています。`void`型を返し[^callback-return-type]、`DOMHighResTimeStamp`型を引数に取る関数です。

[^callback-return-type]: コールバック関数はウェブ開発者がJavaScriptで書きますから、`undefined`を含め任意の値を返せます。「`void`型を返す」というのは、ブラウザはコールバック関数の返り値を無視する、という意味です。

`FrameRequestCallback`型は関数型なので、`requestAnimationFrame`に関数以外を引数に渡すと`TypeError`が発生します。callback interfaceのような細かい規則もなく、渡した関数が呼び出されるだけ[^this-object-of-callback-function]です。シンプルでいいですね。`NodeFilter`のような変種も存在しません。(`NodeFilter`許すまじ...)

[^this-object-of-callback-function]: callback function呼び出し時の`this`オブジェクトは各Web API毎に定義されます。Web APIの規格が何も指定していない場合、デフォルトでグローバルオブジェクトが`this`として渡されます。ここが唯一callback functionで注意を要する点でしょうか。



# 免責

この記事は私の個人的な意見に基づき書かれております。私の所属する組織、団体には一切の関係はありません。

また記事の内容の正しさは保証できません。特にウェブ規格はliving standardを採用しているため、随時更新されています。最新の情報はご自身でご確認ください。