上一节主要是说viewModel各个域中相互通知,本节开始介绍viewModel与节点的相互通知。
我们在body上添加如下HTML片断:
The name is <
span
data-bind
=
"text: fullName"
id
=
"node"
></
span
>
|
然后将第一节提到的$.applyBindings疯狂删减到这样:
$.applyBindings =
function
(model, node){
var
str = node.getAttribute(
"data-bind"
);
str =
"{"
+str+
"}"
var
bindings = eval(
"0,"
+str);
for
(
var
key
in
bindings){
//如果直接eval肯定会报错,因为它找到fullName
console.log(key)
}
}
window.onload =
function
(){
var
model =
new
MyViewModel();
var
node = document.getElementById(
"node"
);
$.applyBindings(model, node)
}
|
意料中的失败,因为fullName在window中找不到。knockoutjs里面有一个叫buildEvalWithinScopeFunction处理此问题:
$.buildEvalWithinScopeFunction =
function
(expression, scopeLevels) {
var
functionBody =
"return ("
+ expression +
")"
;
for
(
var
i = 0; i < scopeLevels; i++) {
functionBody =
"with(sc["
+ i +
"]) { "
+ functionBody +
" } "
;
}
return
new
Function(
"sc"
, functionBody);
}
|
然后将applyBindings 改成这样:
$.applyBindings =
function
(model, node){
var
str = node.getAttribute(
"data-bind"
);
str =
"{"
+str+
"}"
var
fn = $.buildEvalWithinScopeFunction(str,2);
var
bindings = fn([node,model])
console.log(bindings.text == model.fullName)
//到这里我们就把viewModel与节点关联起来了
}
|
在data-bind定义两个东西,一个是viewModel中的域,另一个是对应的操作,在这里是text!在knockout中有一个叫ko.bindingHandlers的对象,里面储放着各种操作,格式如下:
ko.bindingHandlers[
'event'
] = {
'init'
:
function
(element, valueAccessor, allBindingsAccessor, viewModel) { }
};
ko.bindingHandlers[
'submit'
] = {
'init'
:
function
(element, valueAccessor, allBindingsAccessor, viewModel) { }
};
ko.bindingHandlers[
'visible'
] = {
'update'
:
function
(element, valueAccessor) { }
}
ko.bindingHandlers[
'enable'
] = {
'update'
:
function
(element, valueAccessor) { }
};
ko.bindingHandlers[
'disable'
] = {
'update'
:
function
(element, valueAccessor) { }
};
ko.bindingHandlers[
'value'
] = {
'init'
:
function
(element, valueAccessor, allBindingsAccessor) { },
'update'
:
function
(element, valueAccessor) { }
};
ko.bindingHandlers[
'options'
] = {
'update'
:
function
(element, valueAccessor, allBindingsAccessor) { }
};
ko.bindingHandlers[
'selectedOptions'
] = {
'init'
:
function
(element, valueAccessor, allBindingsAccessor) { },
'update'
:
function
(element, valueAccessor) { }
};
ko.bindingHandlers[
'text'
] = {
'update'
:
function
(element, valueAccessor) {
ko.utils.setTextContent(element, valueAccessor());
}
};
ko.bindingHandlers[
'html'
] = {
'init'
:
function
() {
return
{
'controlsDescendantBindings'
:
true
};
},
'update'
:
function
(element, valueAccessor) {
var
value = ko.utils.unwrapObservable(valueAccessor());
ko.utils.setHtml(element, value);
}
};
init可以猜测是用于第一次绑定元素时调用的,update是每次viewModel调用的。
现在我们是玩玩,不用大动干戈。
$.applyBindings =
function
(model, node){
var
str = node.getAttribute(
"data-bind"
);
str =
"{"
+str+
"}"
var
fn = $.buildEvalWithinScopeFunction(str,2);
var
bindings = fn([node,model]);
for
(
var
key
in
bindings){
if
(bindings.hasOwnProperty(key)){
var
fn = $.bindingHandlers[
"text"
][
"update"
];
fn(node,bindings[key])
}
}
}
$.bindingHandlers = {}
$.bindingHandlers[
"text"
] = {
'update'
:
function
(node, observable) {
var
val = observable()
val = val ==
null
?
""
: val+
""
;
if
(
"textContent"
in
node){
//优先考虑标准属性textContent
node.textContent = val;
}
else
{
node.innerText = val;
}
//处理IE9的渲染BUG
if
(document.documentMode == 9) {
node.style.display = node.style.display;
}
}
}
window.onload =
function
(){
var
model =
new
MyViewModel();
var
node = document.getElementById(
"node"
);
$.applyBindings(model, node);
}
到这里,我们就可以把Planet Earth正确地显示在span中,但当viewModel中的FullName发生改变时,span并没有发生改变,缘由是我们没有把它们绑在一起。很简单,我们把$.applyBindings里面的逻辑都整进一个$.computed 中就行了。
var
validValueType = $.oneObject(
"Null,NaN,Undefined,Boolean,Number,String"
)
$.dependencyDetection = (
function
() {
var
_frames = [];
return
{
begin:
function
(ret) {
_frames.push(ret);
},
end:
function
() {
_frames.pop();
},
collect:
function
(self) {
if
(_frames.length > 0) {
self.list = self.list || [];
var
fn = _frames[_frames.length - 1];
if
( self.list.indexOf( fn ) >= 0)
return
;
self.list.push(fn);
}
}
};
})();
$.valueWillMutate =
function
(observable){
var
list = observable.list
if
($.type(list,
"Array"
)){
for
(
var
i = 0, el; el = list[i++];){
el();
}
}
}
$.observable =
function
(value){
var
v = value;
//将上一次的传参保存到v中,ret与它构成闭包
function
ret(neo){
if
(arguments.length){
//setter
if
(!validValueType[$.type(neo)]){
$.error(
"arguments must be primitive type!"
)
return
ret
}
if
(v !== neo ){
v = neo;
$.valueWillMutate(ret);
//向依赖者发送通知
}
return
ret;
}
else
{
//getter
$.dependencyDetection.collect(ret);
//收集被依赖者
return
v;
}
}
value = validValueType[$.type(value)] ? value : void 0;
ret(arguments[0]);
//必须先执行一次
return
ret
}
$.computed =
function
(obj, scope){
//为一个惰性函数,会重写自身
//computed是由多个$.observable组成
var
getter, setter
if
(
typeof
obj ==
"function"
){
getter = obj
}
else
if
(obj &&
typeof
obj ==
"object"
){
getter = obj.getter;
setter = obj.setter;
scope = obj.scope;
}
var
v
var
ret =
function
(neo){
if
(arguments.length ){
if
(
typeof
setter ==
"function"
){
//setter不一定存在的
if
(!validValueType[$.type(neo)]){
$.error(
"arguments must be primitive type!"
)
return
ret
}
if
(v !== neo ){
setter.call(scope, neo);
v = neo;
$.valueWillMutate(ret);
//向依赖者发送通知
}
}
return
ret;
}
else
{
$.dependencyDetection.begin(ret);
//让其依赖知道自己的存在
v = getter.call(scope);
$.dependencyDetection.end();
return
v;
}
}
ret();
//必须先执行一次
return
ret;
}
function
MyViewModel() {
this
.firstName = $.observable(
'Planet'
);
this
.lastName = $.observable(
'Earth'
);
this
.fullName = $.computed({
getter:
function
() {
return
this
.firstName() +
" "
+
this
.lastName();
},
setter:
function
(value) {
var
lastSpacePos = value.lastIndexOf(
" "
);
if
(lastSpacePos > 0) {
// Ignore values with no space character
this
.firstName(value.substring(0, lastSpacePos));
// Update "firstName"
this
.lastName(value.substring(lastSpacePos + 1));
// Update "lastName"
}
},
scope:
this
});
}
$.buildEvalWithinScopeFunction =
function
(expression, scopeLevels) {
var
functionBody =
"return ("
+ expression +
")"
;
for
(
var
i = 0; i < scopeLevels; i++) {
functionBody =
"with(sc["
+ i +
"]) { "
+ functionBody +
" } "
;
}
return
new
Function(
"sc"
, functionBody);
}
$.applyBindings =
function
(model, node){
var
nodeBind = $.computed(
function
(){
var
str =
"{"
+ node.getAttribute(
"data-bind"
)+
"}"
var
fn = $.buildEvalWithinScopeFunction(str,2);
var
bindings = fn([node,model]);
for
(
var
key
in
bindings){
if
(bindings.hasOwnProperty(key)){
var
fn = $.bindingHandlers[
"text"
][
"update"
];
var
observable = bindings[key]
$.dependencyDetection.collect(observable);
//绑定viewModel与UI
fn(node, observable)
}
}
},node);
return
nodeBind
}
$.bindingHandlers = {}
$.bindingHandlers[
"text"
] = {
'update'
:
function
(node, observable) {
var
val = observable()
val = val ==
null
?
""
: val+
""
;
if
(
"textContent"
in
node){
//优先考虑标准属性textContent
node.textContent = val;
}
else
{
node.innerText = val;
}
//处理IE9的渲染BUG
if
(document.documentMode == 9) {
node.style.display = node.style.display;
}
}
}
window.onload =
function
(){
var
model =
new
MyViewModel();
var
node = document.getElementById(
"node"
);
var
nodeBind = $.applyBindings(model, node);
$.log(
"+++++++++++++++++++++++++++"
)
$.log(model.fullName.list[0] == nodeBind);
$.log(model.lastName.list[0] == model.fullName);
$.log(model.firstName.list[0] == model.fullName);
// $.log(model.lastName.list[0] == model.fullName)
setTimeout(
function
(){
model.fullName(
"xxx yyy"
)
},1500)
setTimeout(
function
(){
model.fullName(
"111 222"
)
},3000)
}
大家可以下载回来看看效果:点我