共通リスナに参照をもたせる良い方法

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 3
  • VIEW 885

lazex

score 420

タイトルだけだと何がいいたいかよくわからないですが、
addEventListener でリスナを作るときに removeEventListener で解除するためにどこかに参照を保持する必要があります。
リスナ自体は同じ処理なのでひとつですが内部で別のインスタンスを参照させたいです。
しかし、リスナは event しか引数をとりません。
自分で 「event を受け取って、 event と インスタンスを渡してリスナを実行する」 関数を作っても結局それを addEventListener に登録する以上その参照を保持する必要があります。
その参照をオブジェクトのプロパティには可能な限りしたくないです。
理由は公開したくないのと、リスナをインスタンスごとに持たせるのがムダに思うからです(可能なら共通のものにしたい)。
公開しないというものは共通の WeakMap を 1 つ用意してインスタンスに対する任意のデータをもたせるようにして、そこに実際に登録したリスナを入れることにしました。
ただ、これはこれで手間な方法に思います。
もっと簡単な方法がありそうに思うのですが、思いつきません。
何か良いアイデアがありましたら、教えてください。


 具体例

コードがないと分かりづらいと思うので一例を載せます。

class X{
    setListener(){
        window.addEventListener("click", listener, false)
    }
    removeListener(){
        window.removeEventListener("click", listener, false)
    }
}

function listener(eve){
    x.prop = eve.target
}

export { X }

X のインスタンスそれぞれがリスナを追加します。
このままだと、 listener 内部で x が見つからないとエラーがでます。
x は各 X のインスタンスを指します。

x で参照できるように x を受け取る作りにします。

setListener(){
    const self = this
    this.listener = function(eve){
        listener.call(this, eve, self)
    }
    window.addEventListener("click", this.listener, false)
}
function listener(eve, x){ // 引数が追加
    x.prop = eve.target
}

ここで this.listener の関数がそれぞれのインスタンスに対して作らないといけないのと、それを X のインスタンスに保持しないといけないのが嫌な部分です。

X のインスタンスに持たさないためにこうしました。

setListener(){
    const self = this
    const listener_wrapper = function(eve){
        listener.call(this, eve, self)
    }
    wmap.set(this, listener_wrapper)
    window.addEventListener("click", listener_wrapper, false)
}
const wmap = new WeakMap()

たかだかリスナにオブジェクト参照をもたせたいだけでこんなに変更するのはなにか違う気がします。

書いてて無理そうな気もしてきたのですが、なにか方法がありそうにも思います。

 追記

最終的にどうなっている状態かがわかりづらいため全体のコードを追記します。
また、上記コードで listener が重複していたので修正しました。

const wmap = new WeakMap()

class X {
    setListener(){
        const self = this
        const listener_wrapper = function(eve){
            listener.call(this, eve, self)
        }
        wmap.set(this, listener_wrapper)
        window.addEventListener("click", listener_wrapper, false)
    }
    removeListener(){
        window.removeEventListener("click", wmap.get(this), false)
    }
}

function listener(eve, x){
    x.prop = eve.target
    console.log(x)
}

export { X }

補足すると EsModules のモジュールのため、 wmap はこのファイル外には公開されていません。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+5

 handleEvent

handleEvent を持つオブジェクトを指定してみては、どうでしょう。

Foo.prototype = {
    bar: function bar () {
    document.addEventListener('click'. this, false);
  },
    piyo: piyo,
    handleEvent: function handleEvent (event) {
      this.piyo();
    }
};

 Function.prototype.bind

Foo.prototype.bar = function bar () {
  document.addEventListener('click'. function handleClick (event) { this.method(); }.bind(this), false);
};

 アロー関数

Foo.prototype.bar = function bar () {
  document.addEventListener('click', (event) = > this.method(), false);
};

 プライベートプロパティを定義する

ECMAScript® 2017 (ES8) にはプライベートプロパティを作る方法が用意されていない為、WeakMap を利用します。

下記コードは、上記リンク先の createPivateMap() を利用したコードです。

const Clicker = (() => {
  var pm = createPivateMap();

  return class Clicker {
    constructor (message) {
      pm(this, {message: message});
    }
    addListener () {
      pm(this).listener = (event) => console.log(event.type, pm(this).message);
      document.addEventListener('click', pm(this).listener, false);
    }
    removeListener () {
      document.removeEventListener('click', pm(this).listener, false);
    }
  };
})();

const c1 = new Clicker('Hello, World!'),
      c2 = new Clicker('Hello, JavaScript!');

c1.addListener();
c2.addListener();
document.documentElement.click();
c1.removeListener();
c2.removeListener();
document.documentElement.click();

 listenerを共通化したい

理由は公開したくないのと、リスナをインスタンスごとに持たせるのがムダに思うからです(可能なら共通のものにしたい)。

無駄ではないと思います。
listenerを共通参照にするというのは、[JavaScript] プライベートプロパティを作る方法.mdのStatic版のコードにするという事です。
インスタンス毎に独立した処理に出来ず、removeEventListener は全てのインスタンスに影響してしまいます。
イベント周りの都合を踏まえると、listenerは各々、別の参照であるべきと考えます。

(2018/04/15 13:55追記)

最終的にどうなっている状態かがわかりづらいため全体のコードを追記します。

関数 listener の生成コストを無くしたい意図で書いたコードとお見受けしました。
前述のコードでは、コードの平易さ重視で関数をネストしましたが、私も自分でコードを書くときには関数を外出しするので気持ちは分かります。
(おそらく、「listener共通化」の本来の目的も生成コストにあるのではないでしょうか)

さて、実際に listener を共通化してみると問題点がはっきりします。

class X {
  addListener () {
    window.addEventListener("click", listener, false)
  }
  removeListener () {
    window.removeEventListener("click", listener, false)
  }
}

function listener (event) {
  console.log(event.type);
}

const x1 = new X(),
      x2 = new X();

x1.addListener();       // x1 で listener 追加
x2.addListener();       // x2 で listener 追加(上書き)
document.body.click();  // コンソールには "click" が「1回だけ」出力される
x2.removeListener();    // 上書きされた listener 一つを削除
document.body.click();  // x1, x2 共に click イベントハンドラが発動しない

内部的には以下のコードが実行されています。

window.addEventListener("click", listener, false);        // x1 で listener 追加
window.addEventListener("click", listener, false);        // x2 で listener 追加 (listener が x1 と同一なので、上書き処理)
document.body.click();                                                       // listener は一つしか追加されなかったので、コンソールには "click" が「1回だけ」出力される
window.removeEventListener("click", listener, false);    // x2 で listener 削除 (listener は1つしか登録されてないので全削除)
document.body.click();                                                       // 何も起きない

addEventListener から見れば、同じ listener を与えれば同じイベントハンドラと認識します。
従って、listener を共通化出来ないのです。

 まとめ

最終的に質問文に追記されたコードを元に今までのロジックを詰め込むと、下記コードになります。

const wmap = new WeakMap();

class X {
  addListener () {
    const listener = templateListener.bind(this);

    wmap.set(this, listener);
    window.addEventListener("click", listener, false);
  }
  removeListener(){
    window.removeEventListener("click", wmap.get(this), false);
  }
}

function templateListener (event) {
  this.prop = event.target;
  console.log(this);
}

const x1 = new X(),
      x2 = new X();

x1.addListener();
x2.addListener();
document.body.click();             // 2回発火
document.documentElement.click();  // 2回発火
x2.removeListener();
document.body.click();             // 1回発火

listener に任意の引数を指定したい場合は Function.prototype.bind の第二引数以降を利用するか、handleEvent プロパティ付のオブジェクトを渡して、プロパティ経由で取得します。

const wmap = new WeakMap()

class X {
  addListener () {
    const listener = createListener(this, templateListener);

    wmap.set(this, listener);
    window.addEventListener("click", listener, false);
  }
  removeListener(){
    window.removeEventListener("click", wmap.get(this), false);
  }
}

function createListener (thisArg, handleEvent) {
  return Object.assign(Object.create(Object.getPrototypeOf(thisArg)), thisArg, {handleEvent: handleEvent, thisArg: thisArg});
}

function templateListener (event) {
  this.thisArg.prop = event.target
  console.log(this);
}

const x1 = new X(),
      x2 = new X();

x1.addListener();
x2.addListener();
document.body.click();             // 2回発火
document.documentElement.click();  // 2回発火
x2.removeListener();
document.body.click();             // 1回発火

渡している this 値は new X とほぼ同一ですが、this.prop = event.target のようにインスタンスにプロパティ拡張する場合には参照値が異なるので、this.thisArg 経由で渡してやります。
プロパティ拡張を listener 内でやらなければ、this.adddListenerでメソッドは呼べますし、メソッド経由でプロパティ拡張をすれば、thisArg プロパティは不要ですが、設計思想の違いが現われるところですね。

それから、このコードは簡易版なので、enumerable 等の descriptor まではコピーしません。 Object.getOwnPropertyDescriptor を併用する事で、本来の this 値と同じ挙動となります。

Re: lazex さん

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/04/15 14:03

    To: lazex さん
    > addEventListener の機能がリスナとは別にオプションとして値を渡せばリスナの2つ目以降の引数として渡されるsetTimeoutのような機能があればよかったのに・・・
    引数束縛は Function.prototype.bind によって実装可能です。
    (ECMAScript に足りないのは、this 値束縛せずに、引数束縛する機能だと思っています)
    コードが複雑化する要因は private method がない事にあると思われますが、変数束縛とFunction#callでthis値を渡している事も原因の一つだと思います。

    listener共通化、質問文に追記されたコードを基にした改善案(まとめ)、を親記事に追記しました。

    キャンセル

  • 2018/04/15 14:13

    全体として、複数の問題が併発している状態で、全てをまとめて解決しようとしている為に混乱が生まれている気がします。
    問題点を一つずつリストアップして、一つずつ解決していくようにすると、整理しやすいと思います。

    キャンセル

  • 2018/04/15 16:24

    まとめありがとうございます。
    仮に同一リスナでCもできたとしても、実体が同じだと別インスタンスからリスナセットしたときに上書きになって削除時に問題がでますね
    ここは気づけていませんでした。
    そう考えれば納得いきます。
    ありがとうございました。

    キャンセル

+1

デザインパターンのObserverパターンが適用できると思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

 回答に誤りがありましたので参考にしないでください。

think49さんのご回答がすっきりしてていいと思いますが、一応別解を。

class X{
    constructor(){
        this.constructor.prototype.listener = listener.bind(this);
    }
    setListener(){
        window.addEventListener("click", this.listener, false);
    }
    removeListener(){
        window.removeEventListener("click", this.listener, false);
    }
}
function listener (eve){
    this.prop = eve.target;// this === x
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/04/13 15:21

    Object.getPrototypeOf(this).listener で普通に動作するみたい。
    インスタンス生成前だから動作しないだろう、と思い込んでた。

    キャンセル

  • 2018/04/13 22:46

    constructor は書き換え可能ですので、Object.getPrototypeOf の方が安全ですね。
    ところで、prototypeを上書きすると複数のインスタンスを生成した場合に最新のインスタンスにlistenerが上書きされ、最後のインスタンス以外はthis値が狂ってしまいます。
    書き換えるなら、this.listener であるべきではないかと。

    キャンセル

  • 2018/04/14 09:34

    bind は内部状態を変えるものではなく、this/引数を bind した新たな関数を作るので think49 さんの書いているように this.listener を書き換える必要があって、質問文で書いた setListener の変更をコンストラクタでしているだけで「xのプロパティとして公開される」のは一緒になってしまいます

    キャンセル

  • 2018/04/14 09:57

    あー、おっしゃるとおりでした。
    this を束縛しないとインスタンスを示せず、かといって束縛するならインスタンスに持たせるしかないのか……
    とはいえ、this.listener を bind すると今度は別関数になって removeEventListener でイベントを削除できないということですか。
    なかなか難問ですね。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.21%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る