2011/05/24


部下にも何度も説明してて、この辺がC言語のポインタみたいな鬼門なのかなーとか思いながら。
javascriptでhtml内のonclickの内容を書き換えようとしています.. - 人力検索はてな

javascriptでhtml内のonclickの内容を書き換えようとしていますが 変更後の関数に変数を渡すと、変更後の関数(load)自体が実行されてしまい、うまくいきません 現在は以下のように、onclickの内容を変更しようとしていますが、 関数を実行させずに、html内のonclickの内容だけ書き換える場合はどのようにしたらいいですか?

document.getElementById('box').onclick = (function(id){ load(id) })(userid);

http://q.hatena.ne.jp/1305849029
こういうコードになった経緯を考えると、その間違いがなぜ起こったかわかりやすいし、人力検索で回答貰った後で再度間違う事も少ないと思う。

おそらくだが、この人はこのコールバック関数登録をループかなんかの中で行おうとしたんじゃないかと(勝手ながら)思った。 <script>
window.onload = function() {
    var elem = document.getElementsByTagName('a');
    for (var n = 0; n < elem.length; n++) {
        elem[n].onclick = function() {
            alert(n);
        }
    }
}
</script>
<body>
<a href="#">foo1</a>
<a href="#">foo2</a>
<a href="#">foo3</a>
</body>
このHTMLでfoo1,foo2,foo3をクリックするとどうなるだろう。0,1,2と答えた人は、この質問者と同じ間違いを起こすだろう。答えは全て3が表示される。
これが何故起きて、どう解決すべきかが分かると今後の理解も早いと思う。

ループのインデックスであるnは、コールバック関数から見ると外のスコープにある。つまりコールバック関数が呼び出された時にはループは3回回ってしまっていて、結果どれをクリックしても3が表示される。
じゃぁどうするか。
var elem = document.getElementsByTagName('a');
for (var n = 0; n < elem.length; n++) {
    var f = n
    elem[n].onclick = function() {
        alert(f);
    }
}
こうじゃない?って答えた人は×。それ何も変わってないから、どれクリックしてもまた3だよって答えた人も×。答えは全て2。
なぜ前回は3が表示されたのか。それはforループが回りきってループを外れる条件に達した、つまり3になったから。ではなぜ今回は2なのか。ループは0,1,2の時にしか実行されなかったから。
わかりますよね。

じゃぁこれ、どうやって個々に0,1,2の値を表示させるのよ...となる。答えは聞いてない!答えは複数ある。
人によってはeval()使っちゃう人もいるだろうし、昔のコードでは結構見た。未だにそんなコードの保守をやらされる場合もある。出来るだけモダンな書き方したいですよね。
問題はスコープでしたよね。毎回同じスコープが実行されちゃってるのが問題で、コールバック関数から見ると全て同じ変数を見てしまっているのが問題。
そこでループの度にスコープを作ってあげるのです。以下の様なコードを見ることが多いと思います。 (function() {
    // ...
})()
関数を文として扱わず、式として呼び出しているんですよね。最近はこんな書き方もある様です。
+function() {
    // ...
}()
これで新しいスコープが出来ます。でもこれだけでは解決しません。この作ったスコープで変動しない値を保持してあげます。
var elem = document.getElementsByTagName('a');
for (var n = 0; n < elem.length; n++) {
    (function() { // ココが決め手
        var f = n
        elem[n].onclick = function() {
            alert(f);
        }
    })() // ココが決め手
}
変数fにn渡してます。このループが3回実行されると3回スコープが作られ、それぞれにfというスコープ内変数が宣言されます。これにより個々のイベントハンドラから0,1,2が参照出来る様になるという事です。
ちなみにvar宣言したくない人はよく for (var n = 0; n < elem.length; n++) {
    (function(n) {
        elem[n].onclick = function() {
            alert(n);
        }
    })(n)
}
こう書いたりもします。
今回の問題、よくカウントダウンタイマを作ろうとして for (var n = 0; n < 10; n++) {
    setTimeout(function() {
        document.getElementById('timer').innerHTML = '残り' + (10 - n) + '秒'
    }, 1000 * n)
}
こう書いちゃう人もいます。理屈は同じですよね。気をつけて。
Posted at by



2010/07/14


JScript.NETはMicrosoftが.NET Framework向けに拡張したJavasSriptで、Common Language Runtime(CLR)上で動作するJavaScript実装です。
CLR上ではC#、VB.NET等が動作しますが、JScript.NETは言語がJavaScriptという事もあって実はC#やVB.NET等と同等に使えない機能がいくらかあります。
JavaScriptは動的言語です。オブジェクトにプロパティを生やしてメソッドにしたり、prototypeを弄ったり、eval()で関数を生成したりも出来ます。つまりコンパイルするとは言えど、型が動的に変えられる言語です。
その為、.NET Frameworkの機能の一つであるDelegateが使えません。.NET FrameworkのThreadはDelegateという関数型拘束によりスレッドを安全に呼び出せる様になっています。この関数型拘束に緩い拘束なJavaScript(JScript)の関数を渡す事は出来ないのです。
MSDNでもJScriptのDelegate利用制限が書かれています。
ThreadStart デリゲート

[JScript] JScript では、.NET Framework のデリゲートを利用することができます。ただし、独自に定義することはできません。

http://msdn.microsoft.com/ja-jp/library/system.threading.threadstart(VS.71).aspx
Delegate.CreateDelegate()というメソッドも存在しますが、こちらもJScriptからは利用出来ません。
じゃぁDelegateを引数に取るThreadはJScriptから作れないじゃないか!そう思っておられる方が殆どだと思います。

いえ、出来ます。

import System
import System.Reflection
import System.CodeDom.Compiler
import System.Threading
import Microsoft.CSharp

/**
 * createThread()
 * @param   f   thread function
 * @param   a   argument passing to function
 * @return  thread object
 */
var createThread = (function() {
    var source = [
        "using System.Threading;",
        "public class ThreadInvoker {",
        "  private Microsoft.JScript.Closure func;",
        "  private object arg = null;",
        "  public ThreadInvoker(Microsoft.JScript.Closure func, object arg) { this.func = func; this.arg = arg; }",
        "  private void invokerThread() { func.Invoke(null, new object[]{this.arg}); }",
        "  public Thread createInvoker() { return new Thread(invokerThread); }",
        "}",
    ].join("\n");

    var cp = CodeDomProvider.CreateProvider("CSharp");
    var cps = new CompilerParameters();
    cps.ReferencedAssemblies.Add("Microsoft.JScript.dll");
    cps.GenerateInMemory = true;
    var cr = cp.CompileAssemblyFromSource(cps, source);
    var asm = cr.CompiledAssembly;
    return function(f, a) {
        var tt : Type = asm.GetType("ThreadInvoker");
        var args = new System.Object[2];
        args[0] = f;
        args[1] = a;
        return tt.InvokeMember(
            "createInvoker",
            BindingFlags.InvokeMethod,
            null,
            Activator.CreateInstance(tt, args),
            null);
    };
})();
C#上で書かれたスレッドインボーカをメモリ内でコンパイルして、それにJScriptの関数、正しくはクロージャを渡します。
ここから先はCLRなので、どうとでも出来るはずでC#からMicrosoft.JScript.Closureのメソッドを叩く事も出来ます。
これを使えば
var th = createThread(function(v) {
    for (var n = 1; n <= 6; n++) {
        Thread.Sleep(200);
        print("thread" + n + " " + v);
    }
}, "foo");

th.Start();

Thread.Sleep(200);
print("main1");
th.Suspend();
Thread.Sleep(200);
print("main2");
Thread.Sleep(200);
print("main3");
th.Resume();
こんなコードで thread1 foo
main1
main2
main3
thread2 foo
thread3 foo
thread4 foo
thread5 foo
thread6 foo
こんな動作をさせる事も出来ます。JavaScript(JScript)でスレッドが使えると色々とやりたくなってきます。JScriptでサーバアプリなんて事も簡単に出来そうですね。
ちなみに、このcreateThread()を使うと、ウェブサーバなんかも書けたりします。試しに書いてみたらコードの少なさ(createThreadは除いて)に少し驚きました。JScript.NET & C# ばんざい!
import System
import System.Reflection
import System.CodeDom.Compiler
import System.Threading
import Microsoft.CSharp

/**
 * createThread()
 * @param   f   thread function
 * @param   a   argument passing to function
 * @return  thread object
 */
var createThread = (function() {
    var source = [
        "using System.Threading;",
        "public class ThreadInvoker {",
        "  private Microsoft.JScript.Closure func;",
        "  private object arg = null;",
        "  public ThreadInvoker(Microsoft.JScript.Closure func, object arg) { this.func = func; this.arg = arg; }",
        "  private void invokerThread() { func.Invoke(null, new object[]{this.arg}); }",
        "  public Thread createInvoker() { return new Thread(invokerThread); }",
        "}",
    ].join("\n");

    var cp = CodeDomProvider.CreateProvider("CSharp");
    var cps = new CompilerParameters();
    cps.ReferencedAssemblies.Add("Microsoft.JScript.dll");
    cps.GenerateInMemory = true;
    var cr = cp.CompileAssemblyFromSource(cps, source);
    var asm = cr.CompiledAssembly;
    return function(f, a) {
        var tt : Type = asm.GetType("ThreadInvoker");
        var args = new System.Object[2];
        args[0] = f;
        args[1] = a;
        return tt.InvokeMember(
            "createInvoker",
            BindingFlags.InvokeMethod,
            null,
            Activator.CreateInstance(tt, args),
            null);
    };
})();

import System.Net
import System.Net.Sockets

var listener = new System.Net.Sockets.TcpListener(IPAddress.Parse("127.0.0.1"), 8080);
listener.Start();

while (true) {
    createThread(function(client) {
        var stream = client.GetStream();
        var buf = new System.Byte[client.ReceiveBufferSize];
        var bytesRead = stream.Read(buf, 0, buf.length);
        var str = [
            "HTTP/1.0 200 OK",
            "Content-Type: text/html",
            "",
            "<html><body>",
            "<b>hello world</b>: " + (new Date).toString(),
            "</body></html>",
        ].join("\r\n");
        var bytes = System.Text.Encoding.Default.GetBytes(str);
        stream.Write(bytes, 0, bytes.length);
        stream.Close();
        client.Close();
    }, listener.AcceptTcpClient()).Start();
}
マルチスレッドウェブサーバです。カッコイー!
Posted at by



2010/03/25


twitter stream APIとは、twitterのステータス更新に対してキーワードでtrackしたり、あるグループ内に属するステータス更新をフィルタしたり出来るAPIなのだけど、実際にはchunkedなストリームが流れて来ているのであって、これを使ったWebアプリを作る際にはlong pollを使うのが良い。ただしクライアントサイドでjavascriptを処理する際に
  • サーバサイドでステータス更新をFIFOに溜め込む
  • クライアントからリクエストをブロック(long poll)しFIFOからステータスを送出する
  • クライアントサイドでlong pollを行い画面を更新する
  • 再度サーバへリクエストを投げる
を繰り返すのであれば、せっかくストリームなのにアプリサーバとの接続を切ってしまう事になる。出来ればクライアントからWebアプリもストリーミングとしたい。しかしながらサーバからのステータス更新を受け取るのであれば、JSON/JSONPなアクションが必要になる。
ここで使える!と思ったのがmultipart/mixedです。

multipart/mixedはboundaryで区切られたボディ部を繰り返し送出する際に用いられる仕組みで、実はXHR(XMLHttpRequest)はこのmultipart/mixedなレスポンスを受け取る事が出来る。
受け取るとは言っても、実際にはreadyStateが変化する...でしかないのだけど、これを扱うのに良いライブラリがある。
diggで使われているDUI.jsとStream.jsです。
digg's stream at master - GitHub

Alpha repository for DUI.Stream. When it stabilizes, it will be merged back into DUI

http://github.com/digg/stream
このstreamライブラリは内部でreadyStateが3になるのを待ち、タイマを張ってboundaryを監視するものです。つまり1 body部を扱う事になるので、使う際にはmime typeを指定する事になります。
var s = new DUI.Stream();
s.listen('text/html', function(payload) {
  ...
});
s.load('/push');
この仕組みを使って、twitterからのストリームをクライアントにmultipart/mixedで送出し、かつクライアントも接続を保持したままレスポンスを受け取るのです。接続を切らないので、ほぼタイムロス無しのリアルタイムと言って良いでしょう。
今日はこれをsinatraを使って実装してみました。出来れば一度動かしてみて下さい。これまでリクエストを繰り返し送っていたアプリケーションと比べてスムーズにデータを受け取っているのが分かって頂けるかと思います。
上記diggのライブラリを作業フォルダのstatic/jsフォルダ配下に置き、以下のファイル(fast-twitter-stream.rb)を作業フォルダ直下に置きます。
require 'rubygems'
require 'sinatra'
require 'json'  
require 'tweetstream'  
require 'pit'

# doesn't work on thin and webrick
set :server, 'mongrel'
set :public, File.dirname(__FILE__) + '/static'

config = Pit.get("twitter.com", :require => {
    "username" => "your username in twitter",
    "password" => "your password in twitter"
})

get '/' do
  <<HTML
  <html> <head>
    <title>Server Push</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js"></script>
    <script type="text/javascript" src="/js/DUI.js"></script>
    <script type="text/javascript" src="/js/Stream.js"></script>
    <script type="text/javascript">
    $(function() {
      var s = new DUI.Stream();
      s.listen('text/javascript', function(payload) {
        var status = eval(payload);
        $('#content').append('<p>' + status.text + '</p>');
      });
      s.load('/push');
    });
    </script>
  </head>
  <body>
    <h1>Server Push</h1>
    <div id="content"></div>
  </body>
</html>
HTML
end

get '/push' do
  boundary = '|||'
  response['Content-Type'] = 'multipart/mixed; boundary="' + boundary + '"'

  MultipartResponse.new(boundary, 'text/javascript')
end

class MultipartResponse
  def initialize(boundary, content_type)
    @boundary = boundary
    @content_type = content_type
  end

  def each
    TweetStream::Client.new(config[:username], config[:password]).sample do |status|  
      yield "--#{@boundary}\nContent-Type: #{@content_type}\n(#{status.to_json})"
    end  
  end
end
元となったコードはyoupyさんの物を参考にさせて頂きました。

ブラウザから"http://localhost:4567/"にアクセスすると、もの凄い勢いでステータスが追加されていくのが分かるかと思います。しかも切断していないのでかなり高速です。
お試しの効果には個人差があります。

このmultipart/mixed、結構使えるなーと思いました。得にリアルタイムな画像ストリーミングとして送出したり、リアルタイム電光掲示板の様な物にも使えるかと思います。

今回のサンプルもgithubに置いてあります。
mattn's fast-twitter-stream at master - GitHub

fast twitter stream client using multipart/mixed.

http://github.com/mattn/fast-twitter-stream
よろしければご参考までに。

Posted at by