knockout.js的学习笔记3
上一节主要是说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) }
大家可以下载回来看看效果:点我
机器瞎学/数据掩埋/模式混淆/人工智障/深度遗忘/神经掉线/计算机幻觉/专注单身二十五年