由DOM View e.target三贱客引发的思考
Liber 2013-11-14 17:40

今天在开发1050的时候,遇到了这么一个有意思的问题:

我有类SongModelSongView


  var SongModel = Backbone.Model.extend({
    ...
  });

  var SongView = Backbone.View.extend({
  
    ...
    
    render: function() {
      // render to Dom
    },
    
    events: {
      'click xxx': 'delete'
    },
    
    delete: function(e) {
      var tThis = this;
      this.model.destroy({
        wait: true,
        $btn: $(e.currentTarget),
        success: function() {
          tThis.remove();
        }
      });
    }
  
  });

然后:

  var songModel = new SongModel(someObj);
  var songView = new SongView({model: songModel});

页面进来的时候,执行代码,然后songView正常的显示在页面上。
当我点击删除的时候,songViewdelete事件被触发,然后发送一个Http Delete异步请求,后台执行删除,成功以后执行tThis.remove();

有意思的是,我为了统一代码的交互,用到了一个utils.js来执行一些同样的工作,如,每次发起异步请求before的时候,显示加载中...的进度条。

具体的实现方式,是通过jQueryajaxSetup以及ajaxSend, ajaxComplete, ajaxSuccess, ajaxError来实现的:

  
  var utils = {

    setGlobalAjaxSettings: function() {
    
      var tThis = this;
      
      ...
      
      $(document).ajaxSuccess(function(event, jqxhr, settings) {
        if(jqxhr.responseJSON&&!jqxhr.responseJSON.error) {
          tThis.success(event, jqxhr, settings);
        } else {
          tThis.error(event, jqxhr, settings);
        }
      });

    },
    
    success: function(event, jqxhr, settings) {
      if(settings.$btn) {
        var $alert = $(this.getAlertHtml('alert-success', '操作成功'));
        var $target = settings.$btn.closest('.row').find('*[tag="alert"]');
        this.renderAlert($target, $alert);
      }
    }
  
  }

所以,当我发起的Http Delete请求成功之后,会先执行songView里面的success回调的代码,然后执行utils里面通用的success回调的代码。

然后,有意思的事情就来了,我们注意到,前面发请求的时候,传入了一个$btn: $(e.currentTarget),当回调成功以后,会执行tThis.remove();
这时,按我之前错误的理解,在通用的回调里面,settings.$btn应该是undefined,而当我单步调试的时候发现,他竟然还是当前点击按钮的那个jQuery元素!
而且,它能一直parent()追溯到songViewel,再往上就是undefined了。

我顿时就来劲了,仔细想了一下,原来是这样:

如图所示,当我们执行var songView = new SongView({model: songModel});的时候,内存1被创建,它的内容存我们实例化的jQuery对象;
同时,内存2被创建,它的内容存1的地址,如图红色线所示。
而当我们render的时候,实际上是把2的地址存到DOM的内容中,如图绿色线所示。
接着,让我们执行请求的时候,传入了一个$btn: $(e.currentTarget),我们知道,在JavaScript中,参数的传递,不管是基本类型还是引用类型,都是传值;
所以,这时,内存4被创建,它的内容也存1的地址,如图蓝色线所示。 然而,当请求成功之后,执行:tThis.remove();,这时,内存2被销毁。红色线和绿色线同时也不存在了。
但此时,蓝色线还是存在的,所以,这就解释了我之前单步调试的时候遇到的现象。

那如果我们不执行tThis.remove();呢?,这时,我们执行var $target = settings.$btn.closest('.row').find('*[tag="alert"]');就能搜索到DOM树中其它的父元素。
因为内存2的地址被存在DOM树中,同时内存2的内容又指向内存1的地址,所以DOM树中就会显示内存1的内容,当内存4不断执行parent()的时候,实际上也是在DOM树中往上查找,最终也会找到window元素。

由此可见,DOM, View, e.target之间真是纠缠不清,也难怪我会亲切的称呼它们为三贱客了。