Created
March 25, 2020 16:56
-
-
Save okunokentaro/b1ba46fbd42f7cb58c6adec33fb4f5bc to your computer and use it in GitHub Desktop.
Angular 2 @outputはObserverパターンなのかを調べてみた
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2015/12/24 にQiitaに投稿した記事のアーカイブです | |
--- | |
こんにちは、@armorik83です。 | |
--- | |
先週[`@Output`についての記事](http://qiita.com/armorik83/items/5f429cf7be4adb8c9126)を書いたが、この`@Output`はObserverパターンと言えるのかどうかを調べてみる。Observerパターンについては[Wikipedia](https://ja.wikipedia.org/wiki/Observer_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)、あと[Google](https://www.google.co.jp/search?q=observer%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3&oq=observer%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3&aqs=chrome..69i57j69i59.4391j0j4&sourceid=chrome&es_sm=119&ie=UTF-8)。Pub/Subパターンともいう。詳しくは割愛。 | |
# リスナ登録なのか、リスナ格納なのか | |
今回疑問に思ったのは、`@Output`属性に与えたメソッドは`addListener`的に追加されているのか、それともプロパティに格納されているだけなのかという点。次のようなコードで検証してみた。Angular 2のバージョンはbeta.0。 | |
```ts | |
import {Component, Output, EventEmitter} from 'angular2/core' | |
@Component({ | |
selector: 'child', | |
providers: [], | |
template: ` | |
<button (click)="onClick($event)">Click me</button> | |
`, | |
directives: [] | |
}) | |
class Child { | |
@Output() output = new EventEmitter(); | |
onClick($event: MouseEvent): void { | |
console.log(`=====`); | |
this.output.emit($event); | |
} | |
} | |
@Component({ | |
selector: 'my-app', | |
providers: [], | |
template: ` | |
<div> | |
<child (output)="onOutput()"></child> | |
<button (click)="changeBehavior()">Change!</button> | |
</div> | |
`, | |
directives: [Child] | |
}) | |
export class App { | |
one = () => console.log(1); | |
two = () => console.log(2); | |
constructor() { | |
this.onOutput = this.one; | |
} | |
changeBehavior(): void { | |
this.onOutput = this.onOutput === this.two ? this.one : this.two; | |
} | |
} | |
``` | |
"Click me"ボタンを連打するとログに"1"が並ぶ。そこで"Change!"ボタンを押したとき、その後のログ出力が"1", "2"となればaddListenerしていることになる。 | |
http://plnkr.co/edit/grfmkWVDdtXgWs4qdGNb?p=preview | |
## 結果は | |
ログ出力は"2"だけになった。 | |
```txt | |
===== | |
1 | |
===== | |
1 | |
===== | |
2 | |
``` | |
これによって`this.one`のリスナは破棄され、`this.two`が再登録されていることがわかる。あれ、これってObserverパターンって呼べるんだっけ? | |
## EventEmitterで検証 | |
参照を渡したから中の処理が変わったのかと不安になったので、以下のコードで検証してみた。 | |
```js | |
import {EventEmitter} from "events"; | |
const emitter = new EventEmitter(); | |
let listener = () => console.log(1); | |
console.log(`=====`); | |
emitter.on(`foo`, listener); | |
emitter.emit(`foo`); | |
console.log(`=====`); | |
listener = () => console.log(2); | |
emitter.emit(`foo`); | |
console.log(`=====`); | |
emitter.on(`foo`, listener); | |
emitter.emit(`foo`); | |
``` | |
結果は次の通り。 | |
```txt | |
===== | |
1 | |
===== | |
1 | |
===== | |
1 | |
2 | |
``` | |
だよね、これが知ってるObserverパターンだ。参照もしていない(`listener`を変更しても追従せず"1"しか出力しない)し、`emitter.on()`すると前に登録したものはそのままに、新たに追加される。 | |
# 本気出して追ってみた | |
では一体どうして`this.one`と`this.two`を入れ替えたら処理が入れ替わったのか、追跡してみる。まずはTemplateの`(event)="onEvent()"`構文をパースしている辺りが怪しいので調べる。 | |
## _parseAttr() | |
https://github.com/angular/angular/blob/851647334040ec95d1ab2f376f6b6bb76ead0d9a/modules/angular2/src/compiler/template_parser.ts#L344 | |
```ts | |
// ... | |
} else if (isPresent(bindParts[8])) { // match: (event) | |
this._parseEvent(bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs, | |
targetEvents); | |
} | |
// ... | |
``` | |
どんぴしゃっぽいのが居た。どうやら`bindParts`というのはAngular 2 Templateの様々なSugarを格納しておくところのようで、`[8]`にはイベント式(`()`のもの)が入るようである。`attr`はname, value, sourceSpanをもつオブジェクトで、sourceSpanには親Componentに関する情報が入っていた。残り二つはとりあえず放っておく。 | |
## _parseEvent() | |
https://github.com/angular/angular/blob/851647334040ec95d1ab2f376f6b6bb76ead0d9a/modules/angular2/src/compiler/template_parser.ts#L401 | |
その実装詳細は`_parseEvent()`に記述されている。この中でも`this._parseAction(expression, sourceSpan)`が重要なようだ。`this._parseAction()`は処理を`Parser#parseAction()`に委譲しているので、そちらを読む。 | |
## Parser#parseAction() | |
https://github.com/angular/angular/blob/851647334040ec95d1ab2f376f6b6bb76ead0d9a/modules/angular2/src/core/change_detection/parser/parser.ts#L70 | |
ここから怒涛のパース祭りが始まっている。もうだいぶ複雑なので詳細は省くが、つまるところASTを連れ回してガンガン処理していく。コードが複雑なだけで割と普通だ。 | |
## Parser#parseAccessMemberOrMethodCall() | |
https://github.com/angular/angular/blob/851647334040ec95d1ab2f376f6b6bb76ead0d9a/modules/angular2/src/core/change_detection/parser/parser.ts#L512 | |
どうやら`reflector`を呼び出すところが肝である。`this.reflector.method(id)`の`id`にはメソッド名(今回だと`onOutput`)が入る。 | |
## ReflectionCapabilities#method() | |
https://github.com/angular/angular/blob/851647334040ec95d1ab2f376f6b6bb76ead0d9a/modules/angular2/src/core/reflection/reflection_capabilities.ts#L173-L177 | |
```ts | |
method(name: string): MethodFn { | |
let functionBody = `if (!o.${name}) throw new Error('"${name}" is undefined'); | |
return o.${name}.apply(o, args);`; | |
return <MethodFn>new Function('o', 'args', functionBody); | |
} | |
``` | |
ここで`functionBody`に入るのはstring型、つまり文字列を`new Function()`に与えて関数を生成し、その中の`return o.${name}.apply(o, args);`にある`apply`で動作を実現している。`${name}`はテンプレートリテラルのため実際のname(ここでは`onOutput`)に置き換えられる。すなわち`this.onOutput.apply(this, args);`に等しい(`this`は`App`インスタンス)。 | |
テンプレートリテラル + `new Function()`の合わせ技によるリフレクションだ!キモい!キモいぞ!!キモすぎる!!! | |
呼び出し側はBreakpointが打ちづらく今回は追うのを断念したが、たぶんこの引数`o`に`App`インスタンスが投げられて呼ばれていると推測する。だから`addListener`ではなく処理入れ替えの挙動をとったんだね。 | |
# (追記)CSPの観点では | |
https://developer.mozilla.org/ja/docs/Security/CSP/Introducing_Content_Security_Policy | |
CSPによって`new Function()`が制限されている環境下で、この扱いがどうなるのかは追いきれなかった。 | |
> Angular2 Throws Security Exceptions under the CSP (Content Security Policy) #1744 | |
> https://github.com/angular/angular/issues/1744 | |
> This works as expected. To run the Angular 2 in CSP mode you have to switch into Dynamic mode or pre-generate change detectors offline. | |
issueによって指摘はされており、回答によると"Dynamic mode"もしくは"pre-generate change detectors offline"による手段でこれを解決できるように配慮されているらしいが、詳細は不明である。今後の動きを待とう。 | |
# わかったこと | |
- `@Output`はpub/subっぽく見えてもMethod call | |
- Angular 2の肝はいくつかある | |
- DI | |
- Change Detection | |
- Template Parser + Reflector | |
- それらを上手く包括するDecorators構文によるMetadata | |
Angular 2の深い部分を知りたければ、この辺りを読むと面白いと思った。 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment