论坛首页 AJAX版 JavaScript

闭包分析

浏览 1643 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
最后更新时间:2007-10-13
最近读了关于dustin diaz写的关于xhr包装的文章,有些心得,文章:

Making Ajax Elegantly Fast
Sunday, September 30th, 2007

Sometime ago I was messing around with various techniques for ways that we can optimize asynchronous calls to the server via XMLHttpRequest (and the ActiveX equivalent of XMLHTTP). I wrote about one of those techniques known as branching in an article for Digital Web Magazine called Seven JavaScript Techniques You Should Be Using Today back in April, however, there was a flaw in the function that when one request is made before another one is finished, you would run into an error since it is using the same reference to the instance object for the request. That’s cool in theory, but bad in practice. The benefit you get with that is that you never create more than one XMLHttpRequest object for all your requests. This is fine for light weight use, and when you know for sure that you won’t be making frequent requests simultaneously. But when does that ever happen? Really.

Here’s what that code looked like:

Outdated asyncRequest function
var asyncRequest = function() {
     function handleReadyState(o, callback) {
         var poll = window.setInterval(function() {
             if(o && o.readyState == 4) {
                 window.clearInterval(poll);
                 if ( callback ){
                     callback(o);
                 }
             }
         },
         50);
     }
     var http;
     try {
         http = new XMLHttpRequest;
     }
     catch(e) {
         var msxml = [
             'MSXML2.XMLHTTP.3.0',
             'MSXML2.XMLHTTP',
             'Microsoft.XMLHTTP'
         ];
         for ( var i=0, len = msxml.length; i < len; ++i ) {
             try {
                 http = new ActiveXObject(msxml[i]);
                 break;
             }
             catch(e) {}
         }
     }
     return function(method, uri, callback, postData) {
         http.open(method, uri, true);
         handleReadyState(http, callback);
         http.send(postData || null);
         return http;
     };
}();
Note that the function still branches within the body of the closure, and that branch happens only once. The main issue here is that we need another function that essentially branches the way get the XMLHttpRequest object, but without being run multiple times. In other words, this would be bad:

bad example for XMLHttpRequest
function getXHR() {
   var http;
   try {
     http = new XMLHttpRequest;
   } catch (ex) {
     var msxml = [
       'MSXML2.XMLHTTP.3.0',
       'MSXML2.XMLHTTP',
       'Microsoft.XMLHTTP'
     ];
     for (var i=0, len = msxml.length; i < len; ++i) {
       try {
         http = new ActiveXObject(msxml[i]);
         break;
       }
       catch(e) {}
     }
   }
}

The reason this is slow is because every request for a new XMLHttpRequest object we have to run through the same logic upon each call to this function. Instead we can use what Peter Michaux coined as the Lazy Function Definition Pattern, and apply that technique here. Our optimized (and final) code would then look like this:

Optmized Speedy Ajax Code
var asyncRequest = function() {
   function handleReadyState(o, callback) {
     if (o && o.readyState == 4 && o.status == 200) {
       if (callback) {
         callback(o);
       }
     }
   }
   var getXHR = function() {
     var http;
     try {
       http = new XMLHttpRequest;
        getXHR = function() {
          return new XMLHttpRequest;
        };
     }
     catch(e) {
       var msxml = [
         ‘MSXML2.XMLHTTP.3.0′,
         ‘MSXML2.XMLHTTP’,
         ‘Microsoft.XMLHTTP’
       ];
       for (var i=0, len = msxml.length; i < len; ++i) {
         try {
           http = new ActiveXObject(msxml[i]);
          getXHR = function() {
            return new ActiveXObject(msxml[i]);
          };
           break;
         }
         catch(e) {}
       }
     }
     return http;
   };
   return function(method, uri, callback, postData) {
     var http = getXHR();
     http.open(method, uri, true);
     handleReadyState(http, callback);
     http.send(postData || null);
     return http;
   };
}();

Take closer look at how the getXHR function is redeclared within main declaration to simply return the appropriate object back to the client when requested. And that folks, is Ajax served elegantly fast! Feel free to try it yourself :)

asyncRequest in use
asyncRequest('GET', 'foo.php', function(o) {
   console.log(o.responseText);
});
其实就是原来他写的函数不能做到多异步请求,原来的函数
var asyncRequest = function() {
     function handleReadyState(o, callback) {
         var poll = window.setInterval(function() {
             if(o && o.readyState == 4) {
                 window.clearInterval(poll);
                 if ( callback ){
                     callback(o);
                 }
             }
         },
         50);
     }
     var http;
     try {
         http = new XMLHttpRequest;
     }
     catch(e) {
         var msxml = [
             'MSXML2.XMLHTTP.3.0',
             'MSXML2.XMLHTTP',
             'Microsoft.XMLHTTP'
         ];
         for ( var i=0, len = msxml.length; i < len; ++i ) {
             try {
                 http = new ActiveXObject(msxml[i]);
                 break;
             }
             catch(e) {}
         }
     }
     return function(method, uri, callback, postData) {
         http.open(method, uri, true);
         handleReadyState(http, callback);
         http.send(postData || null);
         return http;
     };
}();
看,虽然返回了个function是在原来的闭包当中,虽然每次function都会产生不同的闭包,但是这个返回的function的上级闭包就是存着http变量的哪个closure在每次调用这个返回的function都是同一个,所以每次在上一个还没结素又调这个函数,就会导致http被冲掉,onreadystate open send什么的又从来一便,原来的当然就完了,那怎么解决呢?
说来也简单,其实就是把http放到返回的function里面就可以了,看后面的代码
var asyncRequest = function() {
   function handleReadyState(o, callback) {
     if (o && o.readyState == 4 && o.status == 200) {
       if (callback) {
         callback(o);
       }
     }
   }
   var getXHR = function() {
     var http;
     try {
       http = new XMLHttpRequest;
        getXHR = function() {
          return new XMLHttpRequest;
        };
     }
     catch(e) {
       var msxml = [
         ‘MSXML2.XMLHTTP.3.0′,
         ‘MSXML2.XMLHTTP’,
         ‘Microsoft.XMLHTTP’
       ];
       for (var i=0, len = msxml.length; i < len; ++i) {
         try {
           http = new ActiveXObject(msxml[i]);
          getXHR = function() {
            return new ActiveXObject(msxml[i]);
          };
           break;
         }
         catch(e) {}
       }
     }
     return http;
   };
   return function(method, uri, callback, postData) {
     var http = getXHR();
     http.open(method, uri, true);
     handleReadyState(http, callback);
     http.send(postData || null);
     return http;
   };
}();
虽然总体上变化很大,那是什么dustin迷上了什么lazy function,不是太知道,其实变化就是
     var http = getXHR();
     http.open(method, uri, true);
     handleReadyState(http, callback);
     http.send(postData || null);
     return http;
看,多了一行var http=getXHR()就是这个var把http包进返回的function的scope里面了,所以每次函数调用都是产生不同的scope所以就不会冲突了,对于什么lazy function我不是很感冒,感觉不如条件判断来得简单
   
最后更新时间:2007-10-13
lazy function的话就可以每次请求都不用判断了。
   
0 请登录后投票
最后更新时间:2007-10-13
如果你先判断的话,那至少要生成一个xhr对象才能知道用哪种方法能够生成。
   
0 请登录后投票
最后更新时间:2007-10-13
又见“惰性函数定义”……

其实这种用法,还是有意思的,比如ajax树状菜单节点的点击事件处理,我需要第一次点击展开的时候,上服务器获取子菜单的数据再显示,之后点击展开节点就只是简单地切换子菜单的可视性,那么用这种第一次运行后改变自身引用的方式就和划算,非常容易实现。

至于hax大大曾经说过,如果使用不当有可能导致内存泄露,是有一定的道理的。不过只要保证不要引用闭包外部的局部变量就好了
   
0 请登录后投票
最后更新时间:2007-10-14
我的观点比较绝对。所谓lazy func def pattern是个伪模式(或称反模式)。它没有提供任何值得一提的性能优势,反而种下诸多隐患。笨笨狗所举的例子也没有什么说服力。

浪费内存是一点,指望“保证不要引用闭包外部的局部变量”是不现实的,因为就算你不引用,当前所有的主流js引擎中,闭包也会隐式引用所有外层变量。苛刻的说,为了避免内存浪费,你必须确保在闭包之外没有任何中间变量,并且函数分支要写在你定义的函数之外(否则函数分支总会引用初始函数本身)。更重要一点是改变函数是一件危险的事情,因为开发者习惯认为函数具有不变性。特别是作为API的函数。对于本应是Const的函数,悄悄的改变一个名字所关联的引用,是很不好的。请参考《改写函数实际上违背了FP的无副作用的精神》

回头看看dustin的例子:

除了原本实现不能异步的bug之外,性能提升并不是一个好的理由,因为就算在IE里面最多throw两个exception出来,这点开销与一个network io相比,可忽略不计。请参考《JS优化原则》

当然,这样一个ajax基础功能,重构一下确实也是应该的,而且这样一个简单的函数,重构的成本并不高。遗憾的是最终重构出来的代码仍旧不值一晒。
   
0 请登录后投票
最后更新时间:2007-10-14
BTW,倒是在dustin原帖的回复中有些有趣和紧凑的getXHR实现。同志们可自己去找一找。
   
0 请登录后投票
最后更新时间:2007-10-14
就比如我给dustin回复的
var getXHR= function(){
if(window.XMLHttpRequest)
return function(){return new XMLHttpRequest}
else if(window.ActiveXObject)
return function(){
	var http;
	var msxml = [
        'MSXML2.XMLHTTP.3.0',
        'MSXML2.XMLHTTP',
        'Microsoft.XMLHTTP'
      ];
      for (var i=0, len = msxml.length; i < len; ++i) {
        try {
          http = new ActiveXObject(msxml[i]);
          getXHR = function() {
            return new ActiveXObject(msxml[i]);
          };
          break;
        }
        catch(e) {}
      }
	  return http;
}
}();
可惜dustin没发表看法
   
0 请登录后投票
最后更新时间:2007-10-14
我说的不是你这个。呵呵。
   
0 请登录后投票
最后更新时间:2007-10-14
hax 写道
BTW,倒是在dustin原帖的回复中有些有趣和紧凑的getXHR实现。同志们可自己去找一找。


我比较喜欢这个家伙的,呵呵:

var params = [
		['XMLHttpRequest', null],
		['ActiveXObject', 'MSXML2.XMLHTTP.3.0'],
		['ActiveXObject', 'MSXML2.XMLHTTP'],
		['ActiveXObject', 'Microsoft.XMLHTTP']
	];

	function getXHR(){
		do try {
			return new window[params[0][0]](params[0][1]);
		} catch(ex) {
			params.unshift();
		} while (true);
	};


最近在新的项目里,我优化了自己设计的树状菜单组件,倒是用到了运行时改变自己的事件处理函数,改天发出来让大家看看,该如何改进。
   
0 请登录后投票
最后更新时间:2007-10-14
嗯我说的就是这个。很简练,给人以愉悦。虽然存在死循环的可能。。。

下面是我自己随手写一个(未经测试)较为严谨而无趣的版本:
new function () {
  if (window.XMLHttpRequest) return;
  if (window.ActiveXObject) {
    var progIds = [
      'MSXML2.XMLHTTP.6.0',
      'MSXML2.XMLHTTP.3.0',
      'Microsoft.XMLHTTP'
    ];
    for (var i = 0; i < progIds.length; i++) {
      var f = new Function('return new ActiveXObject("' + progIds[i] + '")')
      try {
        f()
      } catch(e) {
        continue;
      }
      window.XMLHttpRequest = f;
      return;
    }
  }
  throw Error('Not support XHR');
}
   
0 请登录后投票
论坛首页 AJAX版 JavaScript

跳转论坛:
JavaEye推荐