「大規模なJavaScript開発の話」のsetTimeoutを使った遅延描画について

こちらのスライドで紹介されているjsの遅延描画についてのコードを読んで、自分がsetTimeoutの挙動について、よくわかってなさそうだったので調べてみました。

大規模なJavaScript開発の話
http://www.slideshare.net/terurou/javascript-13711976


以下が遅延描画を考慮していないコードのサンプルになります。(sample1)

        function Box(){
        }

        Box.prototype = {
            setWidth: function(width){
                this._width = width;
                this._draw()
            },
            setHeight: function(height){
                this._height = height;
                this._draw()
            },
            _draw: function(){
	        //描画処理
                console.log(this._width + ":" + this._height)
            }
        };

コンソールからsetWidthをforで回して10回連続で呼んだ場合、_drawが10回処理されることが確認できます。



こちらがスライドで紹介されているinvalidateのサンプルです(sample2)。 適当にlog入れたりしてちょっと変えてあります。

        function Box2(){}

        Box2.prototype = {
            setWidth: function(width){
                this._width = width;
                this.invalidate()
            },
            setHeight: function(height){
                this._height = height;
                this.invalidate()

            },
            invalidate: function(){
                console.log('invalidate');
                if(!this._drawer) {
                    var self = this;
                    this._drawer = function(){
                        console.log('_drawer');
                        self._draw();
                        self._drawer = null;
                    };
                    setTimeout(this._drawer, 0)
                }
            },
            _draw: function(){
                console.log(this._width + ":" + this._height)
            }
        }


同じようにコンソールからsetWidthをforで回して10回連続で呼んだ場合、invalidateが10回呼ばれ、_drawerが一回だけ処理されていることが確認できます。



試しにsetTimeoutを外してみます。(sample3)

            invalidate: function(){
                console.log('invalidate');
                if(!this._drawer) {
                    var self = this;
                    this._drawer = function(){
                        console.log('_drawer');
                        self._draw();
                        self._drawer = null;
                    };
                    //setTimeout(this._drawer, 0)
                    this._drawer();
                }
            },


invalidate -> _drawer -> drawの呼び出しが10回連続で行われてしまいました。



なぜこのような動きになるのでしょうか?

あちこち見て調べてみた結果、こちらのエントリにsetTimeoutの挙動についてわかりやすく書いてありました。

setTimeoutは、指定した時間後に指定した関数を呼び出すタイマーの働きをしますが、それとは別の利点があります。setTimeoutで指定した関数は、必ず一連の処理が終わってから実行されることが保障されています。

JavaScriptのタイマー処理 setTimeoutとその活用


「必ず一連の処理が終わってから実行されることが保障されています」が大事なんですね。

つまり上記のsample2の場合、invalidateが連続10回呼ばれているが、_drawerはsetTimeout付きで呼ばれているため、invalidateが全部終わってから実行される

invalidate * 10 -> _drawer -> draw


sample3のようにsetTimeoutを外すと、invalidate -> _drawer -> drawが順番に呼ばれるだけになってしまう

(invalidate -> _drawer -> draw) * 10


おまけでクロージャーについて

            invalidate: function(){
                if(!this._drawer) {
                    var self = this;
                    this._drawer = function(){
                        self._draw();                
                        self._drawer = null;
                    };
                    setTimeout(this._drawer, 0)
                }
            },

invalidateでthisをselfという名前の変数に保存しているのは、setTimeoutで実行される関数(_drawer)がグローバルスコープで実行されるから。
つまり、クライアントサイドjavascriptの場合、_drawer内のthisはwindowオブジェクトになります。
なのでクロージャを使ってBoxオブジェクトへの参照を保持しているわけですね。
(スライドではthis._draw();this._drawer = null;になってるけど、、typo?)


勉強になった。