上一節主要是說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) }
大家可以下載回來看看效果:點我