我的MVVM框架 v0.1發布


如果經常瀏覽我博客的人就發現,我三個月前就搞鼓過一下什么MVVM與MVC的比較,knockout.js與ember.js。然后就沒動靜了,因為之后一個月,我的MVVM就孵化出來,開發代碼很逆天也很齷鹺,就叫avalon(WPF開發代號)。我覺得,都是好東西,相互借鑒是沒所謂,只要造福IT民工!

說說MVVM的重要性,它雖然是從MVC中衍生出來的,但其雙向綁定機制是特有的,完全是面向界面開發而生,這是傳統的MVC比不了。最近我拉了一伙人在搞我的mass UI就遇到這問題了。

//由mass UI開發團隊的Hodor 提供
define('panel',[
	'$node',
	'$event',
	'$css',
	'$flow',
	'./ejs'
], function(){
	$.ui = $.ui||{}
	 var defaults = {
	 	showHead	: true,
	 	showFoot	: true,
	 	closeAble 	: true,
	 	parent		: 'body',
	 	content 	: {
		 	title 		: 'title',
		 	body  		: 'body',
		 	foot  		: ''
	 	},
	 	css 		: {
	 		width		: 400,
	 		height		: 200
	 	}
	 };
	$.ui.Panel = $.factory({
        inherit: $.Flow,
        init: function(opts) {
        	this.setOptions ("data", defaults, opts )
        	var self = this;
        	self.template = $.ejs.compile(
	            '<div class="panel_wrap">\
	                <% if( data.showHead ){ %>\
		                <div class="panel_header">\
		                    <div class="panel_title">\
		                    	<%= data.content.title %>\
		                    </div>\
		                    <% if( data.closeAble ){ %>\
		                    <span class="panel_closer"></span>\
		                    <% } %>\
		                </div>\
	                <% } %>\
	                <div class="panel_body">\
	                	<%= data.content.body %>\
	                </div>\
	                <% if( data.showFoot ){ %>\
		                <div class="panel_foot">\
		                	<%= data.content.foot %>\
		                </div>\
	                <% } %>\
	            </div>');
           	self.show();
        },
        show : function() {
            this.fire ( 'beforeshow' )
            this.ui && this.ui.remove();
        	this.ui = $(this.template( this.data ))
            	.appendTo( this.parent )
            	.css     ( this.css    )
            	.show    ();
            this.fire ( 'show' )
        },
        hide : function() {
        	this.ui && this.ui.hide().remove();
        	this.ui = undefined;
            this.fire ( 'hide' );
        },
        set : function( keyChain, val ) {//每改一個屬性就重刷整個視圖,因此不能容納子控件,除非我們多做一些額外工作
		    var keys = keyChain.split('.');
		    var key;
		    var ret = this;
		    while( keys.length > 1){
		    	key = keys.shift();
		    	ret[key] = ret[key] != undefined ? 
		    			   ret[key] :
		    			   {};
		    	ret = ret[key];
		    }
		    ret[keys.shift()] = val;
		    this.show();
		    return this;
		}
    });
})

遇到的問題與其他UI團隊一樣,其實你看一下jquery UI就知了。一個控件,它肯定有視圖層,這里我們是用ejs v10來生成。生成HTML插入到DOM算是完成了一半,但當我們要修改這個控件的一些屬性,一些與視圖相關的屬性就遇到麻煩了。比如說title屬性,如果是jquery ui,它肯定先到這個控件的ID,然后再找包含這個title的元素節點,再替換掉它的文本。用jquery是這樣寫出:

 setTilte: function(newTitle){
     $(this.UIid).find("jquery-panel-title").text(newTitle);//更新視圖
     this.title = newTitle;//同時同步對應的屬性
 }
 getTitle: function(){
     return this.title;
 }

如果一個控件涉及的視圖屬性越多,控件這樣的訪問器就越多!長此以往不是辦法!像日歷組件,你可以看到它是多么臃腫。

這時該到MVVM出馬了。它把這些與視圖顯示相關的屬性全部收集到一起,包括基於這些屬性的屬性,比如說fullName是基於lastName與firstName,它是通過函數生成的,這個在模型(M)里是不存在的,但它存於視圖模型(VM)中。由於雙向綁定的存在,我們修改了VM的一個值,它立即自動刷新視圖中對應的位置的值,這過程完全不需要動用到選擇器!

基於選擇器的方式是最不可靠的,因為視圖的變更最頻繁,某一天PM說要多加一功能或不要一功能,於是就多幾個父節點或少幾個子節點,把HTML的層級關系搞得亂七八糟,我們的選擇器尋找就得重寫一次!

因此對於這些DOM操作,我們最好也封裝一層,這是比jquery的DOM操作函數更高層的封裝,目的是讓控件開發者遠離視圖層,他們只需要關注於模型層。如果我們把DOM操作看成增刪改查,那么原始的DOM API相當於直接用二進制手段暴力去改數據庫,而像jQuery等主流庫提供了強大的選擇器引擎與DOM操作函數,相當於發明SQL,而像knockout.js這樣的MVVM框架擁有尖端的依賴鏈機制、雙向綁定,讓智能的集化操作,事務與鎖成為可能,是DOM級別的“ORM系統”!

順便一提,在VM中,所有屬性都是函數,每個函數都是讀寫結合,像jquery的html, attr, text那樣便捷!這有出於兼容IE678的考量,因為它們不支持Object.defineProperty這樣的屬性描述符(或支持不良好,如IE8)。

我的MVVM v1完全是向knockout.js致敬的,用法與它的一模一樣,不過代碼量少了許多。具體教程與實可以看以下鏈接:

最近看了許多MVVM的實現,功力大增,估計v2秒換胎脫骨,更精簡更高效,敬請期待!


define("avalon",["data","attr","event","fx"], function(){
    /* JS UI Component 最終還是通過 HTML 來描述界面,當 js object 的數據發生變化或者執行某個動作時,
    需要通知到對應的html,使其發生相應變化。於是js object 需要得到他在頁面上對應的html的句柄,
    通常做法,是在創建html的時候將createElement返回的句柄保存在js object 內部的某個變量中,
    或者賦值給html eLement一個唯一的ID,js object 根據這個ID來找到對應的HTML Element。同樣,
    當htm elementl的事件(例如onclick)要通知到相對應的 js object 或者回調js object的某個
    方法或屬性時,也需要得到該js object的一個引用。我的意思是建立一種統一的規則,js object
    和他相對應的 html 能通過這種規則互相訪問到對方。 建立這個關聯以后,實現js object和
    對應 html 的數據邦定和數據同步等問題就簡單多了
     */

    var disposeObject = {}
    var cur, ID = 1;
    var registry = {}
    var dependent = {}

    var fieldFns = {
        ensure : function(d){
            if(this.list.indexOf(d) == -1){
                this.list.push(d);
            }
        },
        lock : function(){
            this.locked = true;
        },
        unlock : function(){
            delete this.locked;
        },
        notify : function(){//通知依賴於field的上層$.computed更新
            var list = this.list || [] ;
            if( list.length ){
                var safelist = list.concat(), dispose = false
                for(var i = 0, el; el = safelist[i++];){
                    delete el.cache;//清除緩存
                    if(el.locked === true)
                        break
                    if(el.dispose === true || el() == disposeObject ){//通知頂層的computed更新自身
                        el.dispose = dispose = true
                    }
                }
                if( dispose == true ){//移除無意義的綁定
                    for (  i = list.length; el = list[ --i ]; ) {
                        if( el.dispose == true ){
                            el.splice( i, 1 );
                        }
                    }
                }
            }
        }
    }
    $.avalon = {
        //為一個Binding Target(節點)綁定Binding Source(viewModel)
        setBindings: function( source, node ){
            node = node || document.body; //確保是綁定在元素節點上,沒有指定默認是綁在body上
            //開始在其自身與孩子中綁定
            return setBindingsToElementAndChildren( node, source, true );
        },
        //取得節點的數據隱藏
        hasBindings: function( node ){
            var str = node.getAttribute( "data-bind" );
            return typeof str === "string" && str.indexOf(":") > 1
        },
        //將字符串變成一個函數
        evalBindings: function(expression, level){
            var body = "return (" + expression + ")";
            for (var i = 0; i < level; i++) {
                body = "with(sc[" + i + "]) { " + body + " } ";
            }
            return  Function( "sc", body );
        },
        //轉換數據隱藏為一個函數
        parseBindings : function( node, context ){
            var jsonstr = $.normalizeJSON( node.getAttribute("data-bind"), true, context );
            var fn = $.avalon.evalBindings( jsonstr, 2 );//限制為兩層,減少作用鏈的長度
            return fn;
        },
        //開始收集依賴
        detectBegin: function( field ){
            var uuid = $.avalon.register( field )
            if( cur ){
                cur[uuid] = field
            }
            //用於收集依賴
            var prev = cur;
            cur = dependent[uuid];
            cur.prev = prev
        },
        //添加依賴到鏈中
        detectAdd: function( field ){
            if(cur){
                var uuid = $.avalon.register( field )
                cur[ uuid ] = field;
            }
        },
        //結束依賴收集
        detectEnd: function( field ){
            var deps = dependent[ field.observableID ] || {};
            cur = deps.prev;
            for(var key in deps){
                if(deps.hasOwnProperty(key) && (key != "prev")){
                    var low = registry[ key ];
                    low.ensure(field)
                }
            }
        },
        //注冊依賴
        register: function( field ){
            var uuid = field.observableID
            if(!uuid || !registry[uuid] ){
                field.observableID  = uuid = "observable" +(++ID);
                registry[uuid] = field;//供發布者使用
                dependent[uuid] = {};//收集依賴
                field.list = []
                $.mix(field, fieldFns);
            }
            return uuid;
        }
    }

    $.observable = function( val ){
        var cur = val;
        function field( neo ){
            $.avalon.detectAdd(field)
            if( arguments.length ){//setter
                if(cur !== neo ||  Array.isArray(cur)  ){
                    cur = neo;
                    field.notify()
                }
            }else{//getter
                return cur;
            }
        }
        field.toString = field.valueOf = function(){
            return cur;
        }
        field();
        return field;
    }
   
    $.computed = function( obj, scope ){
        var getter, setter, cur//構建一個至少擁有getter,scope屬性的對象
        if(typeof obj == "function"){//getter必然存在
            getter = obj
        }else if( typeof obj == "object" && obj ){
            getter = obj.getter
            setter = obj.setter;
            scope =  obj.scope;
        }
        function field( neo ){
            if( arguments.length ){
                if(typeof setter === "function"){
                    field.lock()
                    //setter會喚起其依賴的$.observable與$.computed重新計算自身,但它們也會觸發其上級更新自身
                    //由於自身已經先行更新了,沒有再計算一次
                    neo = setter.apply( scope, arguments );
                    field.unlock()
                }
            }else{
                if( "cache" in field ){//getter
                    neo = field.cache;//從緩存中讀取,防止遞歸讀取
                }else{
                    neo = getter.call( scope );
                    field.cache = neo;//保存到緩存
                }
            }
            if(cur !== neo || Array.isArray(cur) && (JSON.stringify(cur) != JSON.stringify(neo)) ){
                cur = neo
                field.notify()
            }
            return cur;
        }
        field.toString = field.valueOf = function(){
            return cur;
        }
        $.avalon.detectBegin( field )
        field();
        $.avalon.detectEnd( field )
        return field;
    }
    $.observableArray = function(array){
        if(!arguments.length){
            array = []
        }else if(!Array.isArray){
            throw "$.observableArray arguments must be a array"
        }
        var field = $.observable(array);
        makeObservableArray(field);
        return field;
    }
    function makeObservableArray( field ){
        ("pop,push,shift,unshift,slice,splice,sort,reverse,remove,removeAt").replace( $.rword, function( method ){
            field[method] = function(){
                var array = this(), n = array.length
                Array.prototype.unshift.call(arguments, array);
                $.Array[method].apply( $.Array, arguments );
                if( /sort|reverse|splice/.test(method) ){
                    field.notify()
                }else if( array.length != n  ){
                    field.notify()
                }
            }
        });
    }
    //template - name
    //foreach - data
    //value - data
    //options - data
    //event - handler
    //MVVM三大入口函數之一
    $.applyBindings = $.setBindings = $.avalon.setBindings;
    var parseBindings = $.avalon.parseBindings;
    //dataFor與contextFor是為事件的無侵入綁定服務的
    $.contextFor = function(node) {
        switch (node.nodeType) {
            case 1:
                var context = $._data(node,"bindings-context");
                if (context) return context;
                if (node.parentNode) return $.contextFor(node.parentNode);
                break;
            case 9:
                return void 0
        }
        return void 0;
    };
    $.dataFor = function(node) {
        var context = $.contextFor(node);
        return context ? context['$data'] : void 0;
    };
    //在元素及其后代中將數據隱藏與viewModel關聯在一起
    function setBindingsToElementAndChildren( node, source, setData ){
        if ( node.nodeType === 1  ){
            var continueBindings = true;
            if( $.avalon.hasBindings( node ) ){
                continueBindings = setBindingsToElement(node, source, setData ) 
            }
            if( continueBindings ){
                var elems = getChildren( node )
                elems.length && setBindingsToChildren( elems, source, setData )
            }
        }
    }
    //viewModel類
    $.viewModel = function(current, parent){
        $.mix( this,current );
        if ( parent) {
            $.mix( this, parent );
            this['$parentContext'] = parent;
            this['$parent'] = parent['$data'];
            this['$parents'] = (parent['$parents'] || []).slice(0);
            this['$parents'].unshift( this['$parent'] );
        } else {
            this['$parents'] = [];
            this['$root'] = current;
        }
        this['$data'] = current;
    }
    $.viewModel.prototype = {
        extend : function(source){
            return $.mix( this,source )
        },
        alias: function( neo, old){
            if(this[ neo ]){
                this[ this[neo] ] = this[old]
            }
            return this;
        }
    }
    //為當前元素把數據隱藏與視圖模塊綁定在一塊
    function setBindingsToElement( node, context, setData ){
        //如果bindings不存在,則通過getBindings獲取,getBindings會調用parseBindingsString,變成對象
        var callback = parseBindings( node, context )//保存到閉包中
        context = context instanceof $.viewModel ? context : new $.viewModel( context );
        if( setData ){
            $._data(node,"bindings-context",context)
        }
        var getBindings = function(){//用於取得數據隱藏
            try{
                return callback( [ node, context ] )
            }catch(e){
                $.log(e)
            }
        }
        var bindings = getBindings();
        var continueBindings = true;
        for(var key in bindings){
            var adapter = $.bindingAdapter[key];
            if( adapter ){
                if( adapter.stopBindings ){
                    continueBindings = false;
                }
                associateDataAndUI( node, bindings[key], context, key, getBindings)
            }
        }
        return continueBindings;
    }
    //setBindingsToChildren的第三第四參數是為了實現事件的無侵入綁定
    function setBindingsToChildren(elems, context, setData, force){
        for(var i = 0, n = elems.length; i < n ; i++){
            var node = elems[i]
            setBindingsToElementAndChildren( node, context, setData && !force );
            if( setData && force ){//這是由foreach綁定觸發
                $._data(node,"bindings-context", context)
            }
        }
    }
    //有一些域的依賴在定義vireModel時已經確認了
    //而對元素的操作的$.computed則要在bindings中執行它們才知
    function associateDataAndUI(node, field, context, key, getBindings){
        var adapter = $.bindingAdapter[key], initPhase = 0, cur;
        function symptom(){//這是依賴鏈的末梢,通過process操作節點
            if(!node){
                return disposeObject;//解除綁定
            }
            if(typeof field !== "function"){
                var bindings = getBindings();//每次都取一次,因為viewModel的數據已經發生改變
                field = bindings["@mass_fields"][key];
            }
            if(initPhase === 0){
                cur = field();
                adapter.init && adapter.init(node, cur, field, context, symptom);
            }
            var neo = field();
            if( key == "case"){//這個應該如何處理更好呢?
                if(field  === context.$switch){//$default;
                    neo = !context.$switch.not;
                }else{
                    //如果前面有一個通過,那么它將不會進入$default分支;
                    neo = context.$switch() == neo;
                    if( neo ){
                        context.$switch.not = true;
                    }
                }
            }
            if(initPhase === 0 ||  cur != neo || Array.isArray(cur)   ){//只要是處理bool假值的比較
                cur = neo;
                adapter.update && adapter.update(node, cur, field, context, symptom);
            }
            initPhase = 1;
        }
      
        $.computed( symptom, context.$data );
    }
    var inputOne = $.oneObject("text,password,textarea,tel,url,search,number,month,email,datetime,week,datetime-local")
    //一個數據綁定,負責界面的展示,另一個是事件綁定,負責更高層次的交互,比如動畫,數據請求,
    //從現影響viewModel,導致界面的再渲染
    $.bindingAdapter = {
        text: {
            update:  function( node, val ){
                val = val == null ? "" : val+""
                if(node.childNodes.length === 1 && node.firstChild.nodeType == 3){
                    node.firstChild.data = val;
                }else{
                    $( node ).text( val );
                }
            }
        },
        value:{
            init: function(node, val, field){
                node.value = val;
                if(/input|textarea/i.test(node.nodeName) && inputOne[node.type]){
                    $(node).on("input",function(){
                        field(node.value)
                    });
                }
               
            }
        },
        html: {
            update:  function( node, val ){
                $( node ).html( val )
            },
            stopBindings: true
        },
        visible: {
            update:  function( node, val ){
                node.style.display = val ? "" : "none";
            }
        },
        enable: {
            update:  function( node, val ){
                if (val && node.disabled)
                    node.removeAttribute("disabled");
                else if ((!val) && (!node.disabled))
                    node.disabled = true;
            }
        },

        "class": {
            update:  function( node, val ){
                if (typeof val == "object") {
                    for (var className in val) {
                        var shouldHaveClass = val[className];
                        toggleClass(node, className, shouldHaveClass);
                    }
                } else {
                    val = String(val || '');
                    toggleClass(node, val, true);
                }
            }
        } ,
        // { text-decoration: someValue }
        // { color: currentProfit() < 0 ? 'red' : 'black' }
        style: {
            update:  function( node, val ){
                var style = node.style, styleName
                for (var name in val) {
                    styleName = $.cssName(name, style) || name
                    style[styleName] = val[ name ] || "";
                }
            }
        },
        attr: {
            update:  function( node, val ){
                for (var name in val) {
                    $.attr(node, name, val[ name ] )
                }
            }
        },
        click: {
            init: function( node, val, field, context ){
                $(node).bind("click",function(e){
                    field.call( context, e )
                });
            }
        },
        "switch":{
            init:function( node, val, field, context){
                context.$switch = field;
                context.$default = field
                setBindingsToChildren( node.childNodes, context )
            },
            update:function(node, val, field, context){
                delete context.$switch.not;//每次都清空它
            },
            stopBindings: true
        },
        checked: {
            init:  function( node, val, field, context ){
                if(context.$hoist && context.$hoist.nodeType == 1 ){
                    var expr =  node.tagName +"['data-bind'="+node.getAttribute("data-bind") +"]";
                    context.$hoisting && $(context.$hoist).delegate( expr,"change", function(){
                        field(node.checked);
                    });
                }else{
                    $(node).bind("change",function(){
                        field(node.checked);
                    });
                }
            },
            update:function(node, val ){
                if ( node.type == "checkbox" ) {
                    if (Array.isArray( val )) {
                        node.checked = val.indexOf(node.value) >= 0;
                    } else {
                        node.checked = val;
                    }
                } else if (node.type == "radio") {
                    node.checked = ( node.value == val );
                }
            }
        }
    }
    //if unless with foreach四種bindings都是使用template bindings
    "if,unless,with,foreach,case".replace($.rword, function( type ){
        $.bindingAdapter[ type ] = {
            update : function(node, val, field, context, symptom){
                if(type == "case" && (typeof context.$switch != "function" )){
                    throw "Must define switch statement above all";
                }
                $.bindingAdapter['template']['update'](node, val, function(){
                    switch(type){//返回結果可能為 -1 0 1 2
                        case "case":
                        case "if":
                            return !!val - 0;//1
                        case "unless":
                            return !val - 0;//0
                        case "with":
                            return 2;//2
                        default:
                            return -1;
                    }
                }, context, symptom);
            },
            stopBindings: true
        }
    });



    var Tmpl = function(t){
        this.template = t
        this.nodes = $.slice(t.childNodes)
    }
    Tmpl.prototype.recovery = function(){
        this.nodes.forEach(function( el ){
            this.template.appendChild(el)
        },this);
        return this.template
    }

    $.bindingAdapter[ "template" ] = {
        update: function(node, data, field, context, symptom){
            var ganso = symptom.ganso//取得最初的那個節點的內部作為模塊
            if( !symptom.ganso ){//緩存,省得每次都創建
                //合並文本節點數
                node.normalize();
                //保存模板
                ganso = node.ownerDocument.createDocumentFragment();
                while((el = node.firstChild)){
                    ganso.appendChild(el)
                }
                symptom.ganso = ganso;
                //復制一份出來放回原位
                var first = ganso.cloneNode(true);
                symptom.references = [ new Tmpl( first ) ];//先取得nodes的引用再插入DOM樹
                node.appendChild( first );
                symptom.prevData = [{}];//這是偽數據,目的讓其update
                
            }
            //  console.log("===============")
            var code = field(),  el;
            first = symptom.references[0];
            // console.log(code)
            if( code > 0 ){ //處理with if bindings
                template = first.recovery();
                var elems = getChildren( template );
                node.appendChild( template );  //顯示出來
                if( elems.length ){
                    if( code == 2 ){//處理with bindings
                        context = new $.viewModel( data, context )
                    }
                    return setBindingsToChildren( elems, context, true )
                }
            }else if( code == 0){//處理unless bindings
                first.recovery();
            }
            if( code < 0  && data && isFinite(data.length) ){//處理foreach bindings
                var scripts = getEditScripts( symptom.prevData, data, true ), hasDelete
                //obj必須有x,y
                for(var i = 0, n = scripts.length; i < n ; i++){
                    var obj = scripts[i], tmpl = false;
                    switch(obj.action){
                        case "update":
                            tmpl = symptom.references[ obj.x ];//這里要增強
                            break;
                        case "add":
                            tmpl =  new Tmpl( ganso.cloneNode(true) );
                            symptom.references.push( tmpl );
                            break;
                        case "retain":
                            //如果發生刪除操作,那么位於刪除元素之后的元素的索引值會發生改變
                            //則重置它們
                            if(obj.x !== obj.y){
                                tmpl = symptom.references[ obj.x ];
                                tmpl.index(obj.y);
                                tmpl = null;
                            }
                            break;
                        case "delete":
                            tmpl = symptom.references[ obj.y ];
                            $(tmpl.nodes).remove();
                            hasDelete = tmpl.destroy = true;
                            tmpl = null;
                            break;
                    };
                    if(tmpl){
                      
                        (function( k, tmpl ){
                            var template = tmpl.template
                            if(!template.childNodes.length){
                                tmpl.recovery();//update
                            }
                            tmpl.index = $.observable(k)
                            var subclass = new $.viewModel( data[ k ], context);
                            subclass.extend( {
                                $index:  tmpl.index
                            // $item: data[ k ]
                            } )
                            //                            .alias("$itemName", "$data")
                            //                            .alias("$indexName", "$index");
                               
                            elems = getChildren( template );
                            node.appendChild( template );
                            if(elems.length){
                                setBindingsToChildren(elems, subclass, true, true );
                            }
                        })(obj.y || 0, tmpl);
                    }
                }
                symptom.prevData = data.concat();
                if(hasDelete){
                    symptom.references = symptom.references.filter(function(el){
                        return !el.destroy
                    })
                };
               
            }
            return void 0
        },
        stopBindings: true
    }

    $.bindingAdapter.disable = {
        update: function( node, val ){
            $.bindingAdapter.enable.update(node, !val);
        }
    }
    var getChildren = function(node){
        var elems = [] ,ri = 0;
        for (node = node.firstChild; node; node = node.nextSibling){
            if (node.nodeType === 1){
                elems[ri++] = node;
            }
        }
        return elems;
    }
    $.bindingAdapter["css"] = $.bindingAdapter["class"]
    var toggleClass = function (node, className, shouldHaveClass) {
        var classes = (node.className || "").split(/\s+/);
        var hasClass = classes.indexOf( className) >= 0;//原className是否有這東西
        if (shouldHaveClass && !hasClass) {
            node.className += (classes[0] ? " " : "") + className;
        } else if (hasClass && !shouldHaveClass) {
            var newClassName = "";
            for (var i = 0; i < classes.length; i++)
                if (classes[i] != className)
                    newClassName += classes[i] + " ";
            node.className = newClassName.trim();
        }
    }

    var getEditScripts = (function () {
        // 一個簡單的Levenshtein distance算法
        //編輯距離就是用來計算從原串(s)轉換到目標串(t)所需要的最少的插入,刪除和替換的數目,
        //在NLP中應用比較廣泛,如一些評測方法中就用到了(wer,mWer等),同時也常用來計算你對原文本所作的改動數。
        //http://www.cnblogs.com/pandora/archive/2009/12/20/levenshtein_distance.html
        //https://gist.github.com/982927
        //http://www.blogjava.net/phyeas/archive/2009/01/10/250807.html
        //通過levenshtein distance算法返回一個矩陣,matrix[y][x]為最短的編輯長度
        var getEditDistance = function(from, to, table){
            var matrix = [], fn = from.length, tn = to.length;
            // 初始化一個矩陣,行數為b,列數為a
            var i, j, td;
            for(i = 0; i <= tn; i++){
                matrix[i] = [i];//設置第一列的值
                table && table.insertRow(i)
            }
            for(j = 0; j <= fn; j++){
                matrix[0][j] = j;//設置第一行的值
                if(table){
                    for(i = 0; i <= tn; i++){
                        td = table.rows[i].insertCell(j);
                        if(isFinite(matrix[i][j])){
                            td.innerHTML = matrix[i][j];
                            td.className = "zero";
                        }
                    }
                }
            }
            // 填空矩陣
            for(i = 1; i <= tn; i++){
                for(j = 1; j <= fn; j++){
                    if( to[i-1] == from[j-1] ){
                        matrix[i][j] = matrix[i-1][j-1];//保留
                    } else {
                        matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, //更新
                            matrix[i][j-1] + 1, // 插入
                            matrix[i-1][j] + 1); //刪除
                    }
                    if(table){
                        td = table.rows[i].cells[j];
                        td.innerHTML = matrix[i][j];
                    }
                }
            }
            $.log(matrix.join("\n"));
            return matrix;
        };
        //返回具體的編輯步驟
        var _getEditScripts = function(from, to, matrix, table){
            var x = from.length, y = to.length, scripts = [], _action;
            if(x == 0 || y == 0){//如果原數組為0,那么新數組的都是新增的,如果新數組為0,那么我們要刪除所有舊數組的元素
                var n =  Math.max(x,y), action = x == 0 ? "add" : "delete";
                for( var i = 0; i < n; i++ ){
                    scripts[scripts.length] = {
                        action: action,
                        x: i,
                        y: i
                    }
                }
            }else{
                while( 1 ){
                    var cur = matrix[y][x];
                    if( y == 0 && x == 0){
                        break;
                    }
                    var left = matrix[y][x-1]
                    var diagon = matrix[y-1][x-1];
                    var top = matrix[y-1][x];
                    action = "retain"//top == left && cur == diagon
                    var min = Math.min(top, diagon, left);
                    var td =  table && (table.rows[y].cells[x]);
                    x--;
                    y--;
                    if( min < cur ){
                        switch(min){
                            case top:
                                action = "add";
                                x++;
                                break;
                            case left:
                                action = "delete";
                                y++;
                                break;
                            case diagon:
                                action = "update";
                                if(_action){
                                    action = _action;
                                    _action = false;
                                }
                                break;
                        }
                    } else{
                        switch(min){
                            case top:
                                _action = "add";
                                x++;
                                break;
                            case left:
                                _action = "delete";
                                y++;
                                break;
                        }
                    }
                    if(table){
                        td.className = action;
                    }
                    scripts[scripts.length] = {
                        action:action,
                        x:x,
                        y:y
                    }
                }
            }
            scripts.reverse();
            return scripts
        }

        return function( old, neo, debug ){
            if(debug){
                debug = document.createElement("table");
                document.body.appendChild(debug);
                debug.className = "compare";
            }
            var matrix = getEditDistance( old, neo, debug );
            return _getEditScripts( old, neo, matrix, debug );
        }
    })();

    //normalizeJSON及其輔助方法與變量
    void function(){
        var restoreCapturedTokensRegex = /\@mass_token_(\d+)\@/g;
        function restoreTokens(string, tokens) {
            var prevValue = null;
            while (string != prevValue) { // Keep restoring tokens until it no longer makes a difference (they may be nested)
                prevValue = string;
                string = string.replace(restoreCapturedTokensRegex, function (match, tokenIndex) {
                    return tokens[tokenIndex];
                });
            }
            return string;
        }
        //https://github.com/SteveSanderson/knockout/wiki/Asynchronous-Dependent-Observables 偉大的東西
        //https://github.com/rniemeyer/knockout-kendo 一個UI庫
        //https://github.com/mbest/js-object-literal-parse/blob/master/js-object-literal-parse.js
        function parseObjectLiteral(objectLiteralString) {
            var str = objectLiteralString.trim();
            if (str.length < 3)
                return [];
            if (str.charAt(0) === "{")// 去掉最開始{與最后的}
                str = str.substring(1, str.length - 1);
            // 首先用占位符把字段中的字符串與正則處理掉
            var tokens = [];
            var tokenStart = null, tokenEndChar;
            for (var position = 0; position < str.length; position++) {
                var c = str.charAt(position);//IE6字符串不支持[],開始一個個字符分析
                if (tokenStart === null) {
                    switch (c) {
                        case '"':
                        case "'":
                        case "/":
                            tokenStart = position;//索引
                            tokenEndChar = c;//值
                            break;
                    }//如果再次找到一個與tokenEndChar相同的字符,並且此字符前面不是轉義符
                } else if ((c == tokenEndChar) && (str.charAt(position - 1) !== "\\")) {
                    var token = str.substring(tokenStart, position + 1);
                    tokens.push(token);
                    var replacement = "@mass_token_" + (tokens.length - 1) + "@";//對應的占位符
                    str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
                    position -= (token.length - replacement.length);
                    tokenStart = null;
                }
            }
            // 將{},[],()等括起來的部分全部用占位符代替
            tokenEndChar = tokenStart = null;
            var tokenDepth = 0, tokenStartChar = null;
            for (position = 0; position < str.length; position++) {
                var c = str.charAt(position);
                if (tokenStart === null) {
                    switch (c) {
                        case "{": tokenStart = position; tokenStartChar = c;
                            tokenEndChar = "}";
                            break;
                        case "(": tokenStart = position; tokenStartChar = c;
                            tokenEndChar = ")";
                            break;
                        case "[": tokenStart = position; tokenStartChar = c;
                            tokenEndChar = "]";
                            break;
                    }
                }
                if (c === tokenStartChar)
                    tokenDepth++;
                else if (c === tokenEndChar) {
                    tokenDepth--;
                    if (tokenDepth === 0) {
                        var token = str.substring(tokenStart, position + 1);
                        tokens.push(token);
                        replacement = "@mass_token_" + (tokens.length - 1) + "@";
                        str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
                        position -= (token.length - replacement.length);
                        tokenStart = null;
                    }
                }
            }
            //拆解字段,還原占位符的部分
            var result = [];
            var keyValuePairs = str.split(",");
            for (var i = 0, j = keyValuePairs.length; i < j; i++) {
                var pair = keyValuePairs[i];
                var colonPos = pair.indexOf(":");
                if ((colonPos > 0) && (colonPos < pair.length - 1)) {
                    var key = pair.substring(0, colonPos);
                    var value = pair.substring(colonPos + 1);
                    result.push({
                        'key': restoreTokens(key, tokens),
                        'value': restoreTokens(value, tokens)
                    });
                } else {//到這里應該拋錯吧
                    result.push({
                        'unknown': restoreTokens(pair, tokens)
                    });
                }
            }
            return result;
        }
        function ensureQuoted(key) {
            var trimmedKey = key.trim()
            switch (trimmedKey.length && trimmedKey.charAt(0)) {
                case "'":
                case '"':
                    return key;
                default:
                    return "'" + trimmedKey + "'";
            }
        }
        // var e =  $.normalizeJSON("{aaa:111,bbb:{ccc:333, class:'xxx', eee:{ddd:444}}}");
        $.normalizeJSON = function (json, insertFields, extra) {//對鍵名添加引號,以便安全通過編譯
            var keyValueArray = parseObjectLiteral(json),resultStrings = [] ,keyValueEntry, propertyToHook = [];
            for (var i = 0; keyValueEntry = keyValueArray[i]; i++) {
                if (resultStrings.length > 0)
                    resultStrings.push(",");
                if (keyValueEntry['key']) {
                    var key = keyValueEntry['key'].trim();
                    var quotedKey = ensureQuoted(key), val = keyValueEntry['value'].trim();
                    resultStrings.push(quotedKey);
                    resultStrings.push(":");
                    //                    if(insertFields === true && key === "foreach"){//特殊處理foreach
                    //                        var array = val.match($.rword);
                    //                        val = array.shift();
                    //                        if(array[0] === "as"){//如果用戶定義了多余參數
                    //                            extra.$itemName = array[1];
                    //                            extra.$indexName = array[2];
                    //                        }
                    //                    }
                    if(val.charAt(0) == "{" && val.charAt(val.length - 1) == "}"){
                        val = $.normalizeJSON( val );//逐層加引號
                    }
                    resultStrings.push(val);
                    if(insertFields == true){//用函數延遲值部分的執行
                        if (propertyToHook.length > 0)
                            propertyToHook.push(", ");
                        propertyToHook.push(quotedKey + " : function() { return " + val + " }")
                    }
                } else if (keyValueEntry['unknown']) {
                    resultStrings.push(keyValueEntry['unknown']);//基於跑到這里就是出錯了
                }
            }
            resultStrings = resultStrings.join("");
            if(insertFields == true){
                resultStrings += ' , "@mass_fields": {'+ propertyToHook.join("") + '}'
            }
            return "{" +resultStrings +"}";
        }
    }();

});

    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM