-
Notifications
You must be signed in to change notification settings - Fork 2
canjs的数据绑定
在上一篇关于canjs的文章中提到了组件开发,其中对于tag: "grid"的组件中的定义方法有各种令人生僻的用法:
can.Component.extend({
tag: "grid",
scope: {
items: [],
waiting: true
},
template: "<table><tbody><content></content></tbody></table>",
events: {
init: function () {
this.A();
},
"{scope} deferreddata": "A",
A: function () {
var deferred = this.scope.attr('deferreddata'),
scope = this.scope;
if (can.isDeferred(deferred)) {
this.scope.attr("waiting", true);
this.element.find('tbody').css('opacity', 0.5);
deferred.then(function (items) {
scope.attr('items').replace(items);
});
} else {
scope.attr('items').attr(deferred, true);
}
},
"{items} change": function () {
this.scope.attr("waiting", false);
this.element.find('tbody').css('opacity', 1);
}
}
});
那现在我们翻开canjs的bindings.js的源码,可以看到如下针对输入框做的双向绑定代码:
// ### Value
// A can.Control that manages the two-way bindings on most inputs. When can-value is found as an attribute
// on an input, the callback above instantiates this Value control on the input element.
var Value = can.Control.extend({
init: function () {
// Handle selects by calling `set` after this thread so the rest of the element can finish rendering.
if (this.element[0].nodeName.toUpperCase() === "SELECT") {
setTimeout(can.proxy(this.set, this), 1);
} else {
this.set();
}
},
// If the live bound data changes, call set to reflect the change in the dom.
"{value} change": "set",
set: function () {
// This may happen in some edgecases, esp. with selects that are not in DOM after the timeout has fired
if (!this.element) {
return;
}
var val = this.options.value();
// Set the element's value to match the attribute that was passed in
this.element[0].value = (val == null ? '' : val);
},
// If the input value changes, this will set the live bound data to reflect the change.
"change": function () {
// This may happen in some edgecases, esp. with selects that are not in DOM after the timeout has fired
if (!this.element) {
return;
}
// Set the value of the attribute passed in to reflect what the user typed
this.options.value(this.element[0].value);
}
}),
从注释中得知:{value} change
触发的是数据绑定,如果数据发生变化,会反应到页面上。change
触发的是页面反向绑定,如果页面元素发生变化,会反应到数据上。对于Control来说,里面可以内置一个init
方法用于初始化。
另外该文件中还有针对radiobox/checkbox、select等的数据绑定,原理上和这也是一样的。
下面我们看一个双向绑定的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<title>two way bind demo</title>
</head>
<body>
<div id='out'></div>
<script src="../../../lib/steal/steal.js"></script>
<script>
steal("can/view/bindings",function(){
var HyperLoop = can.Map.extend({
travelTime: function(){debugger;
return this.attr('dist') / 962 /* km/h */
}
})
loop = new HyperLoop({
dist: 3429.19 /* Chi to SF */
})
var template = can.view.mustache(
"<p>Distance:\
<input can-value='dist'/>\
</p>\
<p>Time:\
{{travelTime}} hrs\
</p>")
$("#out").html( template(loop) );
})
</script>
</body>
该例子是在已知速度的情况下,根据行驶的距离算出行驶的时间。输入是距离,输出是时间。在输入距离后,首先触发{value} change事件使得dist数据发生变化,在dist数据发生变化后,触发change,使得界面上(模板中)的{{travelTime}}发生变化。
下面我们对数据绑定做更加详细的分析。
上文提到,can.map是一个可观察对象,对于里面的属性取值,如果变化了,会触发如下事件:
function (ev, attr, how, newVal, oldVal) {
// when a change happens, create the named event.
can.batch.trigger(this, {
type: attr,
batchNum: ev.batchNum,
target: ev.target
}, [newVal, oldVal]);
这个事件是在调用loop = new HyperLoop({dist: 3429.19 /* Chi to SF */})
做初始化can.map对象的时候就绑定上去了。对于loop,它__bindEvents属性中包含了两个对象,分别为change和dist,它们各自下面挂载这绑定的事件名称和关联的事件。
__bindEvents {...} Object
change [[object Object]] Object, (Array)
[0] {...} Object
handler function () {return fn.apply(context, arguments);} Object, (Function)
name "change" String
dist [[object Object]] Object, (Array)
[0] {...} Object
handler function(ev){if (compute.bound && (ev.batchNum === undefined || ev.batchNum !== batchNum) ) {// Keep the old value
var oldValue = readInfo.value;
// Get the new value
readInfo = getValueAndBind(func, co Object, (Function)
name "dist" String
这里面另一个可被观察的对象就是模板中的travelTime,这里就是传说中的动态绑定。
在解析模板过程中,{{travelTime}}会被解析为___v1ew.push(can.view.txt(1,'p',0,this,can.Mustache.txt({scope:scope,options:options},null,{get:"travelTime"})));
,解析完成后的执行阶段,canjs会为travelTime计算出它是否被其他地方引用到了,如果引用了则就会被绑定change事件,用以当travelTime发生变化时候,同步使其他引用到的地方也产生变化。这里它被dist引用到了。
再看下另一个特殊的东东:can-value='dist'。
在解析模板过程中,can-value='dist'会被解析为___v1ew.push("<p>Distance: <input can-value='dist'",can.view.pending({attrs: ['can-value'], scope: scope,options: options}),"/>")
,解析完成后的执行阶段,canjs会为can-value标识的做特殊回调处理。它位于binds.js中的can.view.attr("can-value", function (el, data) {})
方法。该方法中的最后一行代码new Value(el, {value: value});
就是为了实现双向数据绑定,因为Value这个类就是基于control的监控类,里面提供的就是上面我们说的var Value = can.Control.extend({...});
,里面提供了"{value} change": "set",
和"change": function (){}
。