我在主干中遇到了模糊和点击事件的问题。我有一个视图(下面的代码),它创建了一个带有按钮的小搜索条目div。我打开这个div,把焦点放在entry字段上。如果有人点击关闭(模糊),我会通知父视图关闭此视图。如果他们点击按钮,我会开始搜索。
模糊行为运行良好,但是当我点击按钮时,我也会得到模糊事件,但无法得到点击事件。我的结构正确吗?
顺便说一句,其他一些帖子建议在div中添加计时器,以防在点击事件触发之前关闭div。我可以完全评论结束,但仍然只得到模糊事件。在某种先到先得的基础上,这些公司一次只解雇一个吗?
PB_SearchEntryView = Backbone.View.extend({
template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
events: {
"click button": "find",
"blur #part": "close"
},
initialize: function(args) {
this.dad = args.dad;
},
render: function(){
$(this.el).html(this.template());
return this;
},
close: function(event){ this.dad.close(); },
find: function() {
alert("Find!");
}
});
我不确定问题出在哪里,但这是jsbin代码。
尽管这个问题已经存在了11年,但使用当前版本的Backbone、Undercore和jQuery仍然有可能遇到这种情况,并且解决方案仍然相同。
让我们从一个可运行片段中的问题再现开始。下面,我复制了问题中的代码,并添加了一些模拟代码。要复制,请单击";运行代码片段";,然后通过单击文本输入的内部手动聚焦文本,然后单击按钮。您将看到输入字段和按钮再次消失,alert('Find!')
行未运行:
var PB_SearchEntryView = Backbone.View.extend({
template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
events: {
"click button": "find",
"blur #part": "close"
},
initialize: function(args) {
this.dad = args.dad;
},
render: function(){
$(this.el).html(this.template());
return this;
},
close: function(event){
this.dad.close();
},
find: function() {
alert("Find!");
}
});
var MockDadView = Backbone.View.extend({
initialize: function() {
this.search = new PB_SearchEntryView({
dad: this
}).render();
},
render: function() {
this.$el.append(this.search.el);
return this;
},
close: function() {
this.search.remove();
}
});
var view = new MockDadView().render();
view.$el.appendTo(document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.6/underscore-umd-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/backbone@1.4.1/backbone-min.js"></script>
原因如下。用户的一个动作通常会触发许多不同类型的事件。在这种情况下,点击按钮可能会触发以下所有事件:
- CCD_ 2在CCD_
<input>
元素上的change
(仅当用户在单击按钮之前键入该元素时;换句话说,如果blur
前面有input
事件)- CCD_ 8在CCD_
- CCD_ 10在CCD_
- CCD_ 12在CCD_
- 父
<form>
元素上的submit
,如果有的话(因为我们没有设置<button>
的type
属性,它默认为submit
)
上述顺序是标准化的,因为此顺序对于实现用户友好的交互式表单最有用。特别地,blur
1上的blur
和change
在<button>
上的任何事件之前触发,因为这些事件是在采取进一步的表单处理步骤之前验证用户输入的潜在好机会。同样,<button>
事件在整个<form>
的submit
之前触发,因为您可能希望在最终实际提交表单之前进行最终的完整表单验证或其他预处理
浏览器会立即调用这些事件的处理程序,并且允许调用堆栈在触发下一个用户事件之前完全展开。这是JavaScript著名的事件循环。因此,当我们点击上面有问题的片段中的按钮时,会发生以下事情:
blur
事件触发- 调用了我们的
PB_SearchEntryView
实例的close
方法 - 调用了我们的
<input>
0的close
方法 - 调用了我们的
PB_SearchEntryView
实例的remove
方法 - 我们的
PB_SearchEntryView
实例的HTML元素从DOM
中删除,其事件绑定也被删除(这是Backbone.View
的remove
原型方法的默认行为) - 上面调用的方法以相反的顺序返回
<button>
上的click
事件永远不会被触发,因为该元素已在步骤5中从DOM中删除- (即使
click
事件在步骤7中被触发,我们的PB_SearchEntryView
实例的find
方法仍然不会被调用,因为事件绑定在步骤5中被撤消。)
因此,如果我们仍然希望在处理完blur
之后调用find
方法,我们需要防止同时调用remove
方法。一种简单有效的方法是延迟对this.dad.close
的调用。合适的延迟是大约50毫秒;这足够长,可以确保<button>
的click
事件首先被触发,足够短,用户不会注意到。我们甚至可以取消延迟的调用,以便在点击按钮时保持搜索视图打开。在下面的片段中,我更改了close
和find
方法来说明如何做到这一点,并添加了注释来解释机制:
var PB_SearchEntryView = Backbone.View.extend({
template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
events: {
"click button": "find",
"blur #part": "close"
},
initialize: function(args) {
this.dad = args.dad;
},
render: function(){
$(this.el).html(this.template());
return this;
},
close: function(event){
// Short way to refer to this.dad.
var dad = this.dad;
// The method of this.dad we want to be
// invoked later. When saving it to a
// variable, we need to bind it so that
// the `this` variable points to this.dad
// when it is invoked.
var askDadToClose = dad.close.bind(dad);
// Invoke dad's close method with a 50 ms
// delay. Save a handle that enables us
// to cancel the call in the find method.
this.willClose = setTimeout(askDadToClose, 50);
},
find: function() {
alert("Find!");
// If you still want the search view to
// close after performing the search,
// remove the `if` block below.
if (this.willClose != null) {
clearTimeout(this.willClose);
delete this.willClose;
}
}
});
var MockDadView = Backbone.View.extend({
initialize: function() {
this.search = new PB_SearchEntryView({
dad: this
}).render();
},
render: function() {
this.$el.append(this.search.el);
return this;
},
close: function() {
this.search.remove();
}
});
var view = new MockDadView().render();
view.$el.appendTo(document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.6/underscore-umd-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/backbone@1.4.1/backbone-min.js"></script>
奖励材料
该问题还询问了代码的结构是否正确。下面是这个片段的第三个版本,我在其中做了三个我认为会有所改进的更改:
- 子视图(
PB_SearchEntryView
)不再知道其父视图。它不是调用父级上的方法,而是简单地触发一个事件。由应用程序的其他组件来侦听和处理该事件。这减少了组件之间的耦合,通常使应用程序更易于测试和维护。(对于长距离通信的松耦合,我建议使用主干网无线电。) - 与其在
<button>
上侦听click
事件,不如在<form>
上侦听submit
事件。当提交表单正是我们正在做的事情时,这更符合语义,也更合适。这也允许用户在键入<input>
元素时按下回车键来触发相同的事件处理程序。这确实需要<button type=submit>
位于<form>
元素内部,这就是为什么我们将视图的tagName
设置为form
- 让视图自己负责渲染。通常,视图本身最清楚何时更改其内部HTML结构,通常,无论是在首次创建时,还是在其模型或集合更改时(如果有的话)。让外部组件负责渲染通常是一种反模式,除非渲染非常昂贵(例如,如果渲染地图或复杂的可视化)
var PB_SearchEntryView = Backbone.View.extend({
// This is a form, so it can be submitted.
tagName: 'form',
template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
events: {
// Handle submit rather than a button click.
"submit": "find",
"blur #part": "close"
},
initialize: function(args) {
// self-render on initialize
this.render();
},
render: function(){
this.$el.html(this.template());
return this;
},
close: function(event){
// Like before, we save a method for later,
// but this time it is our own trigger
// method and we bind two additional
// arguments. This view does not need to
// know what happens with its 'close' event.
var triggerClose = this.trigger.bind(this, 'close', this);
// Delay as before.
this.willClose = setTimeout(triggerClose, 50);
},
find: function(event) {
// Since this is now a proper submit
// handler, we need to prevent the default
// behavior of reloading the page.
event.preventDefault();
alert("Find!");
if (this.willClose != null) {
clearTimeout(this.willClose);
delete this.willClose;
}
}
});
var MockDadView = Backbone.View.extend({
initialize: function() {
// This view owns an instance of the above
// view and knows what to do with its
// 'close' event.
this.search = new PB_SearchEntryView();
this.search.on('close', this.close, this);
// self-render on initialize
this.render();
},
render: function() {
this.$el.append(this.search.el);
return this;
},
close: function() {
this.search.remove();
}
});
var view = new MockDadView();
view.$el.appendTo(document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.6/underscore-umd-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/backbone@1.4.1/backbone-min.js"></script>
至少在Safari中,使用上面的代码片段会有一个小问题:如果我通过预编译返回键来提交表单,find
方法仍然会被调用,但表单也会被删除。在这种情况下,我怀疑Safari在关闭警报后触发了一个新的blur
事件。解决这个问题就足够回答另一个问题了!