WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

キャプチャリングとバブリング

by WebSurfer 2012年12月1日 00:14

JavaScript や jQuery を使ったプログラミングで、DOM イベントのバブリングという言葉を耳にします。本などを読んでもピンと来なかったので、理解するためにサンプルを作って動かしてみました。あまり面白くないかもしれませんが、せっかく作ったので書いておきます。

イベントのキャプチャリングとバブリング

上の画像のサンプルは、実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。ソースコードはこの記事の下の方に記載しています。

DOM イベントは、イベントの原因となったオブジェクトで発生するだけでなく、キャプチャリングとバブリングというイベントの伝播があります。ここが .NET Framework のイベントとは異なっていて、自分が理解に苦しんだところです。

どこかの DOM オブジェクトでイベントが発生すると、window オブジェクト(ブラウザによっては document オブジェクト)とイベントが起きたオブジェクトの間を、DOM ツリーの親子関係を順にたぐって、イベントが伝播していきます。

伝播は 3 つのフェーズに分かれており、Capturing Phase(捕捉フェーズ)⇒ Target Phase(対象フェーズ)⇒ Bubbling Phase(浮上フェーズ)という順番になります。それぞれのフェーズの説明は以下の通りです。

  1. Capturing Phase では、window ⇒ document ⇒ その中の親 ⇒ 子 ⇒ 孫 ⇒ ひ孫 ・・・といった具合に、親子関係を親側から順にたぐって、イベントが起きたオブジェクトの親まで(「親まで」という点に注意)の各オブジェクトにイベントが送信されていきます。
  2. Target Phase では、イベントの原因となったオブジェクトにイベントが送信されます。
  3. Bubbling Phase では、イベントを発生させたオブジェクトの親から(「親から」という点に注意)順に浮上していき、window に達するまで各オブジェクトに順にイベントが送信されていきます。

window からイベントを発生させたオブジェクトの間の、親子関係にある任意のオブジェクトにリスナーを登録しておけば、そのオブジェクトにイベントが送信されたタイミングで必要な処置ができます。

オブジェクトにリスナーを登録する方法は、(1) 当該オブジェクトの addEventListener メソッド を呼び出す、(2) HTML 要素の属性を利用する、(3) 当該オブジェクトのプロパティを利用する、の 3 つがあります。

注意しなければならないのは、上記の (1) 以外の方法では、Capturing Phase でイベントを捕捉できないことです。

さらに注意しなければならないのは、IE8 以前では addEventListener メソッドはサポートされていないことです。代わりに attachEvent メソッドが使えますが、これは Capturing Phase でイベントを捕捉できません。(attachEvent メソッドの詳細は後述します)

IE9 は DOM Level 3 Events をサポートしているそうなので(詳しくは MSDN の IEBlog DOM Level 3 Events support in IE9 を参照)、addEventListener メソッド を使用可能です。

addEventListener メソッドを利用してリスナーを登録し、Capturing Phase、Target Phase、Bubbling Phase でイベントを補足するサンプルを書いてみました。そのソースコードをアップしておきます。

なお、Target Phase では、addEventListener メソッドで第 3 引数 useCapture を false に指定して登録したリスナーのみが呼び出されることになっているそうですが、自分が試した限りでは、useCapture を true に指定して登録したリスナーまでもが呼び出されてしまいました。

検証に使ったブラウザは、IE9、Firefox 17.0, Chrome 23.0.1271.95 m, Opera 12.02, Safari 5.1.7 で、すべて同じ結果になりました。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Event Capturing and Bubbling</title>
    <style type="text/css">
        #div1 { border: 1px solid black; width: 200px; 
            height: 100px; }
        #table1 { border: 1px solid red; width: 150px; }
        td { border: 1px solid green; }
        #span1 { background-color: yellow; }
    </style>
    <script type="text/javascript">
    //<![CDATA[

        // IE9, Mozzilla 用リスナー。
        function listener1(event) {
            // イベントが発生すると event オブジェクトが生成
            // され、その参照が第一引数に渡される。それから
            // target プロパティでイベントを発生させたオブジ
            // ェクトを、eventPhase プロパティでフェーズ情
            // 報を取得できる。this はアタッチしたオブジェク
            // トへの参照となる。
            var e = document.getElementById("result");
            var phase = "";
            if (event.eventPhase === 1) {
                phase = "capturing phase";
            } else if (event.eventPhase === 2) {
                phase = "target phase";
            } else if (event.eventPhase === 3) {
                phase = "bubbling phase";
            }
            var str = "fired by: " + event.target.id + 
                      ", listened at: " + this.id + 
                      ", during " + phase + "<br />";
            e.innerHTML += str;
        }

        // IE6-8 用リスナー。
        function listener3(element) {
            // attachEvent を使うと、this が参照するオブジェ
            // クトは window になってしまい、アタッチしたオ
            // ブジェクトへの参照は取得できない。止むを得な
            // いので、オブジェクトへの参照は引数として渡す。
            // Mozilla 系ではリスナーの第一引数には event オ
            // ブジェクトへの参照が渡されるので注意。
            var e = document.getElementById("result");
            var str = 
                "fired by: " + window.event.srcElement.id +
                ", listened at: " + element.id + "<br />";
            e.innerHTML += str;
        }

        // Bubbling Phase のリスナーをアタッチ。
        function attachForBubbling(element) {
            var e = document.getElementById("result");
            if (element.addEventListener) {
                // Bubbling Phase のリスナーをアタッチするに
                // は第三引数を false に設定する。
                element.addEventListener('click', listener1, 
                    false);
                e.innerHTML += "listener1 attached to " +
                    element.id +
                    " by addEventListener" + 
                    " w/ useCapture=false<br />";
            } else if (element.attachEvent) {
                // アタッチするオブジェクトへの参照をリスナ
                // ーの引数に渡すため、以下のように匿名関数
                // を使う。ただし、匿名関数を使うとデタッチ
                // できなくなることに注意。
                element.attachEvent('onclick', 
                    function () { listener3(element) });
                e.innerHTML += "listener3 attached to " + 
                    element.id + " by attachEvent<br />";
            }
        }

        // Capturing Phase のリスナーをアタッチ。
        // (IE9, Mozilla のみ) 
        function attachForCapturing(element) {
            var e = document.getElementById("result");
            if (element.addEventListener) {
                // Capturing Phase 用リスナーをアタッチするに
                // は第三引数を true に設定する。
                element.addEventListener('click', listener1, 
                    true);
                e.innerHTML += "listener1 attached to " +
                    element.id +
                    " by addEventListener" + 
                    " w/ useCapture=true<br />";
            }
        }

        // リスナーを各オブジェクトにアタッチ。
        window.onload = function () {
            var element = document.getElementById("div1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("table1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("td1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("span1");
            attachForBubbling(element);
            attachForCapturing(element);
        }
    //]]>
    </script>    
</head>
<body>
    <div id="div1">
        <table id="table1">
            <tr>
                <td id="td1">
                    <span id="span1">span1</span>
                </td>
            </tr>
            <tr>
                <td id="td2">
                    <span id="span2">span2</span>
                </td>
            </tr>
        </table>
    </div>
    <input type="button" value="Clear Results" 
        onclick="javascript:result.innerHTML = '';"/>
    <br />
    <div id="result"></div>
</body>
</html>

上でも述べましたように、IE6-8 では、addEventListener メソッドはサポートされていませんが、代わりに attachEvent メソッド がリスナーを登録するのに使えます。

IE6-8 をサポートするためには以下のようにします。上のサンプルコードでもこのようにして IE6-8 でリスナーを登録しています。

if (element.addEventListener) {
    element.addEventListener('click', listener, false);
} else if (element.attachEvent){
    element.attachEvent('onclick', listener);
}

attachEvent メソッドには以下のデメリットがあるので注意してください。

  1. リスナー内の this で取得できるのが、window オブジェクトへの参照になる。(addEventListener メソッドの場合はリスナーをアタッチしたオブジェクトへの参照になる)
  2. Capturing Phase ではイベントを補足できない。(Bubbling Phase と Target Phase では補足可能)

上記 1 の問題に対応するため、サンプルコードでは、リスナーの引数にアタッチするオブジェクトへの参照を渡しています。さらに、attachEvent メソッドの引数に匿名関数を使って、リスナーを登録しています。

Tags: , , ,

JavaScript

IE8 と traget frame

by WebSurfer 2010年8月24日 22:42

IE7 では問題なかった異なるウィンドウの target frame への表示が、IE8 ではうまくいきません。<=(やり方の問題でした。下の方に書いた追記参照)

親ウィンドウの iframe に子ウィンドウの更新結果を表示

確証はありませんが、IE8 では異なるウィンドウはプロセスが異なることが原因のように思われます。

GridView と iframe をもつ親ページ (Parent.aspx)、DetailsView を持つ子ページ (Child.aspx) を例にとって、シナリオを説明します。

  1. 親ウィンドウを開き Parent.aspx を要求。
  2. Parent.aspx は、DB からデータを取得し、GirdView にレコード一覧を表示。
  3. GridView 上でレコードを選択すると、親ウィンドウは開いたまま、子ウィンドウを開いて Child.aspx を要求。
  4. Child.aspx は、選択されたレコードの詳細を DetailsView に Edit モードで表示。
  5. 子ウィンドウでレコードを編集し、ボタンクリックで Child.aspx にポストバックして DB を更新。
  6. Child.aspx は、ReadOnly モードの DetailsView の形で、更新後のレコードを応答。
  7. Child.aspx の応答を、開いたままの親ウィンドウの Parent.aspx の iframe に表示。
  8. 子ウィンドウを閉じる。

問題は、上記 7 の、親ウィンドウを開いたままにしておいて、その iframe に子ウィンドウで行った更新結果を表示するところです。

これは、Parent.aspx の iframe の name を、Child.aspx の form 要素の target に設定するということで可能なはずです。

しかしながら、IE8 では新たに別ウィンドウが開いてそれに Child.aspx が表示され、target としたはずの iframe には何も表示されません。

以前 IE7 で試した時はうまくいきました(今は環境がないので試せませんが、間違いないです)。Firefox 3.6.8, Opera 10.61(この時点での最新版)は、以前と同様、期待通りに動きます。

どうも IE8 では、親ウィンドウと子ウィンドウのプロセスが違うということが問題のようです。つまり、親ウィンドウの iframe の name を、子ウィンドウのプロセスが認識できず、そのような名前の iframe はないと判断されるので、新たに別ウィンドウを開いて Child.aspx を表示するのではないかと思います。

ただ、ググって見ても上記のような話は見当たらないので、ひょっとして何か思い違いをしている可能性はゼロではなさそうですけど。(汗)

検証に使ったサンプルも書いておきます。DB は Microsoft が提供しているサンプル、Northwind の Employee テーブルを使用しています。

Parent.aspx
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
  {
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
      Button btn = (Button)e.Row.FindControl("Button1");
      int id = (int)((DataRowView)e.Row.DataItem)["EmployeeID"];
      btn.OnClientClick = 
        "javascript:window.open('042_Child.aspx?employeeid=" + 
        id.ToString() + "&mode=edit', null, " + 
        "'menubar=no, scrollbars=yes, status=no, width=500, height=600'); " + 
        "return false;";            
    }          
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
    <div>
      <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
        ConnectionString="<%$ ConnectionStrings:Northwind %>" 
        SelectCommand=
          "SELECT EmployeeID, LastName + ' ' + FirstName AS Name 
           FROM Employees">
      </asp:SqlDataSource>
      <asp:GridView ID="GridView1" 
        runat="server" 
        AutoGenerateColumns="False" 
        DataKeyNames="EmployeeID" 
        DataSourceID="SqlDataSource1" 
        EnableModelValidation="True" 
        OnRowDataBound="GridView1_RowDataBound">
        <Columns>
          <asp:BoundField DataField="EmployeeID" 
            HeaderText="ID" 
            InsertVisible="False" 
            ReadOnly="True" 
            SortExpression="EmployeeID" />
          <asp:BoundField DataField="Name" 
            HeaderText="名前" 
            ReadOnly="True" 
            SortExpression="Name" />
          <asp:TemplateField HeaderText="選択">
            <ItemTemplate>
              <asp:Button ID="Button1" 
                runat="server" 
                CommandName="Select" 
                Text="Select" />
            </ItemTemplate>
          </asp:TemplateField>
        </Columns>
      </asp:GridView>
    </div>
    <hr />
    <iframe name="DetailedList" height="450px" width="350px">
    </iframe>
  </form>
</body>
</html>
Child.aspx
<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  protected void Page_Load(object sender, EventArgs e)
  {
    string employeeid = Request.QueryString["employeeid"];
    string mode = Request.QueryString["mode"];
        
    if (Page.IsPostBack)
    {
      mode = "readonly";
    }

    if (!String.IsNullOrEmpty(employeeid) && 
      !String.IsNullOrEmpty(mode))
    {
      Panel1.Visible = true;
      Panel2.Visible = false;
      Label1.Text = employeeid;
            
      if (mode == "edit")
      {                
        DetailsView1.DefaultMode = DetailsViewMode.Edit;
        ((Button)DetailsView1.FindControl("Button1")).OnClientClick =
          "javascript:setTimeout('self.close()', 100);";
        ((Button)DetailsView1.FindControl("Button2")).OnClientClick =
          "self.close(); return false;";
      }
      else
      {
        DetailsView1.DefaultMode = DetailsViewMode.ReadOnly;
      }
    }
    else
    {
      Panel1.Visible = false;
      Panel2.Visible = true;
    }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" target="DetailedList" runat="server">
    <asp:Panel ID="Panel1" runat="server">
      <h1>詳細表示</h1>
      <span>
        選択された Employee ID: <asp:Label ID="Label1" runat="server" />
      </span>
      <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
        ConnectionString="<%$ ConnectionStrings:Northwind %>" 
        SelectCommand=
         "SELECT [EmployeeID], [LastName], [FirstName], [Title], 
            [TitleOfCourtesy], [Address], [City], [Region], 
            [PostalCode], [Country], [HomePhone] 
          FROM [Employees] 
          WHERE ([EmployeeID] = @EmployeeID)" 
        UpdateCommand=
         "UPDATE [Employees] 
          SET [LastName] = @LastName, [FirstName] = @FirstName, 
            [Title] = @Title, [TitleOfCourtesy] = @TitleOfCourtesy, 
            [Address] = @Address, [City] = @City, [Region] = @Region, 
            [PostalCode] = @PostalCode, [Country] = @Country, 
            [HomePhone] = @HomePhone 
          WHERE [EmployeeID] = @EmployeeID" >
        <SelectParameters>
          <asp:ControlParameter ControlID="Label1" 
            Name="EmployeeID" 
            PropertyName="Text" 
            Type="Int32" />
        </SelectParameters>
        <UpdateParameters>
          <asp:Parameter Name="LastName" Type="String" />
          <asp:Parameter Name="FirstName" Type="String" />
          <asp:Parameter Name="Title" Type="String" />
          <asp:Parameter Name="TitleOfCourtesy" Type="String" />
          <asp:Parameter Name="Address" Type="String" />
          <asp:Parameter Name="City" Type="String" />
          <asp:Parameter Name="Region" Type="String" />
          <asp:Parameter Name="PostalCode" Type="String" />
          <asp:Parameter Name="Country" Type="String" />
          <asp:Parameter Name="HomePhone" Type="String" />
          <asp:Parameter Name="EmployeeID" Type="Int32" />
        </UpdateParameters>
      </asp:SqlDataSource>
      <asp:DetailsView ID="DetailsView1" 
        runat="server" 
        AutoGenerateRows="False" 
        DataKeyNames="EmployeeID" 
        DataSourceID="SqlDataSource1" 
        EnableModelValidation="True">
        <Fields>
          <asp:BoundField DataField="EmployeeID" 
            HeaderText="EmployeeID" 
            InsertVisible="False" 
            ReadOnly="True" 
            SortExpression="EmployeeID" />
          <asp:BoundField DataField="LastName" 
            HeaderText="LastName" 
            SortExpression="LastName" />
          <asp:BoundField DataField="FirstName" 
            HeaderText="FirstName" 
            SortExpression="FirstName" />
          <asp:BoundField DataField="Title" 
            HeaderText="Title" 
            SortExpression="Title" />
          <asp:BoundField DataField="TitleOfCourtesy" 
            HeaderText="TitleOfCourtesy" 
            SortExpression="TitleOfCourtesy" />
          <asp:BoundField DataField="Address" 
            HeaderText="Address" 
            SortExpression="Address" />
          <asp:BoundField DataField="City" 
            HeaderText="City" 
            SortExpression="City" />
          <asp:BoundField DataField="Region" 
            HeaderText="Region" 
            SortExpression="Region" />
          <asp:BoundField DataField="PostalCode" 
            HeaderText="PostalCode" 
            SortExpression="PostalCode" />
          <asp:BoundField DataField="Country" 
            HeaderText="Country" 
            SortExpression="Country" />
          <asp:BoundField DataField="HomePhone" 
            HeaderText="HomePhone" 
            SortExpression="HomePhone" />          
          <asp:TemplateField ShowHeader="False">
            <EditItemTemplate>
              <asp:Button ID="Button1" 
                runat="server" 
                CausesValidation="True" 
                CommandName="Update" 
                Text="更新">
              </asp:Button>
               
              <asp:Button ID="Button2" 
                runat="server" 
                CausesValidation="False" 
                CommandName="Cancel" 
                Text="キャンセル">
              </asp:Button>
            </EditItemTemplate>
          </asp:TemplateField>          
        </Fields>
      </asp:DetailsView>
    </asp:Panel>
    <asp:Panel ID="Panel2" runat="server">
      <p>クエリ文字列 employeeid または mode が指定されていません。</p>
    </asp:Panel>
  </form>
</body>
</html>

---------- 2010/8/29 追記 ----------

思い違いというか、や���方の問題でした。

Visual Studio で、[デバッグ] → [デバッグ開始] または [デバッグなしで開始] で IE8 を起動して試していたのでダメでした。

普通に IE8 を起動して、アドレスバーに url を直打ちして試したら、期待通り子ウィンドウのポストバック結果が親ウィンドウの iframe に表示されました。

お騒がせしましたが、違うことが認識できて、一つ利口(?)になったということで・・・(苦笑)

でも、何で違うんでしょう???

Tags: , ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar