Javascript MVC 学习笔记(二) 控制器和状态

今天进入第二个部分:控制器。

控制器和状态

从以往的开发经验来看,我们都是将状态保存在服务器的session或者本地cookie中,但Javascript应用往往被限制在单页面,所以我们也可以将状态保存在客户端的内存里面。保存在内存还意味着能带来更快的界面响应。

在MVC里面,状态都是保存在控制器里的,控制器相当于应用中视图和模型的纽带。当加载页面的时候,控制器将事件处理程序绑定在视图里,并适时处理回调,以及和模型必要的对接。

控制器是模块化的而且非常独立,理想状况下不应该定义任何全局变量,而是应该定义为完全解耦的功能组件。所以我们通过模块模式将控制器放在一个立即处理的匿名函数里并传递全局参数进去,避免在内部访问全局变量时需要遍历作用域,也清晰地表明了这个模块使用了哪些全局变量。

(function($){
    var mod = {};

    //改变参数的上下文并立即执行
    mod.load = function(func){
        $($.proxy(func, this));
    };

    //点击事件
    mod.assetsClick = function(){
        //处理点击
        console.log("click!");
    }

    //调用load方法
    mod.load(function(){
        //为元素添加单击事件
        this.view = $("#view");
        this.view.find(".assets").click(
            $.proxy(this.assetsClick, this)
        );
    });
})(jQuery);

假设页面内有如下元素

<div id="view">
    <button class="assets">按钮</button>
</div>

单击按钮后,将输出”click”,这也暗示了控制器和视图的联系,其实书的这一部分讲的就是如何在控制器内保存状态,根据状态的不同改变不同的视图。

(注)jQuery.proxy(method, context)

$.proxy方法是jQuery中的代理方法,它接收两个参数,返回一个新method,该方法始终保持context上下文。

比如在按钮的监听里:

$("#btn").click(function(){
    //这里的this代表按钮
});
$("#btn").click(
    $.proxy(function(){
        //这里的this代表window
    }, window);
);

(注解完)

抽象出库

现在将控制器抽象成库,并添加一些新的方法,这样就能在外部或别的模块中重用它了。

(function($, exports){
    //如果通过构造方法传递参数,则调用include方法
    var mod = function(includes){
        if(includes){
            this.include(includes);
        }
    };
    //改变原型对象名(便于调用)
    mod.fn = mod.prototype;

    //定义自己的代理方法,上下文始终指向控制器自己
    mod.fn.proxy = function(func){
        return $.proxy(func, this);
    };

    //立即执行函数
    mod.fn.load = function(func){
        $(this.proxy(func));
    };

    //为构造器添加方法
    mod.fn.include = function(ob){
        $.extend(this, ob);
    };

    //将构造器暴露出全局,在外界也可以访问
    exports.Controller = mod;
})(jQuery, window);

在别的地方需要用到控制器的时候,就调用Controller即可:

(function($, Controller){
    //创建控制器
    var mod = new Controller();

    //改变view的类名
    mod.toggleClass = function(e){
        //jQuery中的toggleClass方法代表如果存在此类名则删除,不存在则添加
        this.view.toggleClass("over");
    }

    //页面加载完立即执行
    mod.load(function(){
        //绑定页面元素,添加监听
        this.view = $("#view");
        this.view.mouseover(this.proxy(this.toggleClass));
        this.view.mouseout(this.proxy(this.toggleClass));
    });
})(jQuery, Controller)

在以上的使用中,通过匿名函数立即调用的方法并不是在DOM加载之后载入的,而是在生成DOM之前,然而控制器的load方法又是在页面文档加载完成之后才进行回调。我们可以对控制器进行进一步改写,在DOM生成之后统一载入控制器。

//定义一个全局对象
var exports = this;
(function($){
    var mod = {};
    //提供一个create方法,生成控制器
    mod.create = function(includes){
        //create方法返回的就是result,一种控制器
        var result = function(){
            //创建控制器实例的时候调用初始化方法
            this.init.apply(this, arguments);
        }
        result.fn = result.prototype;
        result.fn.init = function(){};

        result.proxy = function(func){
            return $.proxy(func, this);
        };
        result.fn.proxy = result.proxy;

        result.include = function(ob){
            $.extend(this.fn, ob);
        };
        result.extend = function(ob){
            $.extend(this, ob);
        };
        if(includes){
            result.include(includes);
        }
        return result;
    };

    exports.Controller = mod;
})(jQuery);

在创建控制器的时候必须手动指定init方法,在init方法为dom元素添加监听:

$(function(){
    //创建控制器
    var ToggleView = Controller.create({
        init: function(view){
            this.view = $(view);
            this.view.mouseover(this.proxy(this.toggleClass));
            this.view.mouseout(this.proxy(this.toggleClass));
        },
        toggleClass: function(){
            this.view.toggleClass("over");
        }
    });

    //创建一个控制器的实例
    new ToggleView("#view");
});

在创建实例的时候将在构造函数里触发init事件,另外根据实例化的情况将视图传入控制器而不是写死在控制器内,我们就可以将控制器重用于不同的元素,同时保持代码最短。

访问视图

一种常见的模式是一个视图对应一个控制器,视图包含Id,而在控制器内使用视图的元素则使用class,这样和其他视图的元素不会产生冲突,比如上面的ToggleView传入了Id为view的元素,所以在view内的元素则使用类名进行访问。

init: function(view){
    this.view = $(view);
    this.form = this.view.find(".form");
}

但这意味着控制器中会有很多选择器,需要不断查找DOM,我们可以在控制器中专门开辟一个空间来存放选择器到变量的映射表:

elements: {
    "form.searchForm": "searchForm",
    "form input[type=text]": "searchInput"
}

有了这样的映射表之后,控制器的属性名(比如searchForm)就能和具体的元素(类名为searchForm的form)相对应了,并且在控制器实例化的时候创建他们:

$(function($){
    exports.SearchView = Controller.create({
        //视图中的元素使用类名查找
        elements: {
           "input[type=search]": "searchInput",
           "form": "searchForm"
        },

        init:function(element){
            //获取视图元素
            this.el = $(element);
            //根据映射表创建属性
            this.refreshElements();
            //为元素添加监听等
            this.searchForm.submit(this.proxy(this.search));
        },
        //事件处理函数
        search: function(e){
            console.log("Searching", this.searchInput.val());
        },

        //内部使用的选择器,将上下文指定为视图元素
        $: function(selector){
            return $(selector, this.el);
        },

        //创建视图内的元素
        refreshElements: function(){
            for(var key in this.elements){
                //key为选择器名,值为属性名
                this[this.elements[key]] = this.$(key);
            }
        }
    });
    //视图用id指定
    new SearchView("#users");
});

状态机

状态机是“有限状态机”的简称,本质上由两部分构成:状态和转换器。它只有一个活动状态,但也包含很多非活动状态。当活动状态之间相互切换的时候就会调用状态转换器。

比如应用中存在很多视图,它们的显示是相互独立的,一个视图用来显示联系人,一个视图用来编辑联系人,这两个视图一定是互斥关系,其中一个显示另一个一定隐藏,这个场景就非常适合引入状态机,因为它能确保每个时刻只有一个是激活的。

首先看一下状态机的思路,我们构造一个状态机:

var StateMachine = function(){};
StateMachine.fn = StateMechine.prototype;

StateMachine.fn.bind = function(){
    if(!this.o){
        this.o = $({});
    }
    //绑定自定义事件
    this.o.bind.apply(this.o, arguments);
}

StateMachine.fn.trigger = function(){
    if(!this.o){
        this.o = $({});
    }
    //触发自定义事件
    this.o.trigger.apply(this.o, arguments);
}

StateMechine.fn.add = function(controller){
    //为状态机绑定自定义事件
    this.bind("change", function(e, current){
        if(controller == current){
            controller.activate();
        }else{
            controller.deactivate();
        }
    });

    //为控制器创建激活方法
    controller.active = $.proxy(function(){
        this.trigger("change", controller);
    }, this);
}

(注)jQuery中的自定义事件

上述代码重点在于bind和trigger,在jQuery中,利用这两个方法可以很轻易地实现自定义事件:

var $obj = $({});
$obj.bind("myEvent", function(){
    console.log("自定义事件");
});
$obj.trigger("myEvent"); //"自定义事件"

bind代表绑定,trigger表示触发,你只需要一个jQuery对象就够了,所以上面创建了一个空的对象,使用$包装成了jQuery对象。

(注解完)

这个状态机的add()方法将传入的控制器添加至状态列表,并为状态机中的o绑定一个自定义的change事件(添加一个绑定一个),然后创建一个active()函数。当控制器调用active()的时候,触发所有的change事件,则除了调用的控制器将执行activate方法外,其他控制器全部执行deactivate方法:

//控制器1
var con1 = {
    activate: function(){
        console.log("con1 activate");
    },
    deactivate: function(){
        console.log("con1 deactivate");
    }
};
//控制器2
var con2 = {
    activate: function(){
        console.log("con2 activate");
    },
    deactivate: function(){
        console.log("con2 deactivate");
    }
};

var sm = new StateMachine();
sm.add(con1);
sm.add(con2);

con1.active(); //输出"con1 activate"和"con2 deactivate"

当然也可以直接通过状态机触发

sm.trigger("change", con1);

通过状态机的切换状态,我们可以结合控制器改变视图,当con1激活的时候显示一个视图,否则隐藏;con2激活的时候显示另一个视图,否则隐藏。这样就能根据状态的不同对视图进行切换。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-10 12:10:28

Javascript MVC 学习笔记(二) 控制器和状态的相关文章

Javascript MVC 学习笔记(一) 模型和数据

写在前面 最近在看<MVC的Javascript富应用开发>一书,本来是抱着一口气读完的想法去看的,结果才看了一点就傻眼了:太多不懂的地方了.只好看一点查一点,一点一点往下看吧,进度虽慢但也一定要坚持看完.本学习笔记是对书上所讲解内容的理解和记录. 笔记里的代码大多会按书上摘录下来,因为<MVC的Javascript富应用开发>是结合了JQuery库,所以对于JQuery中不太懂的知识点也会附在代码后面,也算是一些额外的收获. MVC概述 要学习MVC,首先得知道MVC是什么,MV

Javascript MVC 学习笔记(三) 视图和模板

模板 Javascript中模板的核心概念是,将包含模板变量的HTML片段和Javascript对象做合并,把模板变量替换为对象中的属性值. 书中讲到了几种库作为模板引擎,但是链接失效了,可以在这里下载,链接:jQuery.tmpl 使用方式很简单,看一下它的说明就行了.给定一个模板,通过给它添加数据就能生成渲染好的元素节点,然后就可以将它添加到页面中,主要使用方法如: //数据 var object = { url: "http://example.com", getName: fu

JavaScript正则表达式学习笔记之一 - 理论基础

自从年前得空写了两篇文章之后就开始忙了,这一忙就是2个月??.当时信誓旦旦说的定期写篇博客的计划也就泡汤了??,不过好在最近有空,顺便总结一下这两个月遇到的几个问题.第一个问题就是项目中用到了一些正则才发现之前被自己忽略的正则是时候补一补了.恰逢今天周六??,就把自己学习JavaScript正则表达式的笔记整理成文,写了这篇关于正则表达式理论基础的文章,希望本文能对有需要的同学提供帮助.号外:本文相对基础,大神请忽略??. 一. 基本概念 正则表达式是用于匹配字符串中字符组合的模式. 一种几乎可

Spring MVC 学习笔记(二):@RequestMapping用法详解

一.@RequestMapping 简介 在Spring MVC 中使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求,相当于Servlet中在web.xml中配置 <servlet>     <servlet-name>servletName</servlet-name>     <servlet-class>ServletClass</servlet-class> </servlet>

javascript面向对象学习笔记(二)——创建对象

javascript面向对象学习笔记(二)--创建对象 工厂模式 该模值抽象了创建具体对象de过程.用函数来封装噫特定接口创建对象的细节. function createPerson(name,age,job){ var o=new Object(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); }; return o; } var person1=createPerson("Chiaki&

.NET MVC学习笔记(一)

看了些关于MVC的资料,做一些MVC的笔记. 分解关注点 在MVC世界里有个很重要的观念--"分解关注点"(Separation of Concerns),指的是:当你进行软件开发时,可一直关注当前的对象,而不会受到同样的系统中其它对象的干扰,进而专注于完毕开发工作.如此一来,不但easy提升软件质量,也能够加快对代码的理解速度. 分解关注点观念的特征与优势 简化复杂度 大幅提升可维护性 更易于測试 对开发ASP.net MVC项目的建议 不要反复你自己: 好的软件不应该有太多反复的程

AJax 学习笔记二(onreadystatechange的作用)

AJax 学习笔记二(onreadystatechange的作用) 当发送一个请求后,客户端无法确定什么时候会完成这个请求,所以需要用事件机制来捕获请求的状态XMLHttpRequest对象提供了onreadyStateChange事件实现这一功能.这类似于回调函数的做法.onreadyStateChange事件可指定一个事件处理函数来处理XMLHttpRequest对象的执行结果,如: 复制代码 代码如下: ajaxObj=createAjaxObject(); var url="/MyTod

ASP.NET MVC学习系列(二)-WebAPI请求

继续接着上文 ASP.NET MVC学习系列(一)-WebAPI初探 来看看对于一般前台页面发起的get和post请求,我们在Web API中要如何来处理. 这里我使用Jquery 来发起异步请求实现数据调用. 继续使用上一文章中的示例,添加一个index.html页面,添加对jquery的引用. 一.无参数Get请求 一般的get请求我们可以使用jquery提供的$.get() 或者$.ajax({type:"get"}) 来实现: 请求的后台Action方法仍为上篇文章中的GetU

MVC学习笔记索引帖

[MVC学习笔记]1.项目结构搭建及单个类在各个层次中的实现 [MVC学习笔记]2.使用T4模板生成其他类的具体实现 [MVC学习笔记]3.使用Spring.Net应用IOC(依赖倒置) [MVC学习笔记]4.使用Log4Net来进行错误日志的记录 [MVC学习笔记]5.使用Controller来代替Filter完成登录验证(Session校验) [MVC学习笔记]6. 使用Memcache+Cookie解决分布式系统共享登录状态 [MVC学习笔记]7.使用极验验证来制作更高逼格的验证码