SVC (Subject-View-Controller)是目标——视图——控制器模型框架,应用于高度交互式的,低延迟的 web 应用程序。
原理:观察者模式定义了一种一对多的依赖关系,当一个对象改变时,其它所有的依赖对象自动发生改变。SVC 中的 Subject其实就是观察者模式中的被观察者,Subject的状态变化, 都会引发View 层次的变动,而SVC的View层次自然就对应观察者模式中的观察者。所以使用观察者模式可以使svc中的Subject和View得以分离。
引用的GitHub:https://github.com/joelzimmer/SVC/blob/master/svc.js
1.Subject
Subject:负责存储和处理数据。包含增加,删除和通知观察者的方法,当模型中的数据改变时,通知相应视图(观察者)做出改变。
由三部分组成
(1)Subject接口:实现简单的质量测试,通知视图,订阅/取消订阅功能
svc.Subject = Class.create({ // Our constructor just sets up the mapping from notifcations to functions. initialize: function (args) { this._notificationToObservers = $H(); }, // Equality test is just a strict equals against another `subject` at this point. isEqual: function (subject) { return this === subject; }, // Destroy self. Notify that we are dead, and clear out all subscribed functions. destroy: function () { this.notify('subject:destroy'); this._notificationToObservers = $H(); }, // Notify that a particular `notification` happened. The first variable passed along will be the subject. notify: function (notification) { var observers = this._notificationToObservers.get(notification); var args = $A(arguments); // Remove the notification from the arguments array. args.shift(); // Add the subject as the first argument. args.unshift(this); if (observers) { observers.invoke('apply', null, args); } }, // Add a subscription of a `f`unction call for a particular `notification`. subscribe: function (notification, f) { var observers = this._notificationToObservers.get(notification); if (observers) { observers.push(f); } else { this._notificationToObservers.set(notification, [f]); } }, // Remove a subscription of a `f`unction call for a particular `notification`. unsubscribe: function (notification, f) { var observers = this._notificationToObservers.get(notification); if (observers) { this._notificationToObservers.set(notification, observers.without(f)); } } });
(2)ModifiableSubject接口:是Subject接口的详细补充,实现设置属性,保存对象状态信息等功能
svc.ModifiableSubject = Class.create(svc.Subject, { // Create the object, setting the dirty flag to false. initialize: function ($super, args) { $super(args); this._dirty = false; // TODO: automatically do silent sets for all values in args that aren't reserved }, // Retrieve the particular value of a `property`. get: function (property) { return this[':' + property]; }, // Get all the `properties` for a ModifiableSubject. properties: function () { var properties = $A(); for (var property in this) { if (property.charAt(0) === ':') { properties.push(property.slice(1)); } } return properties; }, // Set the object to be `clean` again. Notifies `subject:clean`. clean: function () { this._dirty = false; this.notify('subject:clean'); }, // Set the object to be `dirty`. Notifies `subject:dirty`. dirty: function () { this._dirty = true; this.notify('subject:dirty'); }, // Set a `property` to a particular `value`. If the `silent` flag is set, then no notification will be // made. If it isn't set (or is not included), the notification will be `subject:change:<property>`. // Returns true if the value is changed. set: function (property, value, silent) { if (this.get(property) == value) { return false; } this[':' + property] = value; if (! silent) { this.notify('subject:change:' + property); this.dirty(); } return true; }, // Checks to see if the object has changes. isDirty: function () { return this._dirty; } });
(3)Collection接口:是Subject的组成部分,用来存储所有与之相关的被观察者数据,并维护数据。
svc.Collection = Class.create(svc.Subject, { // Our constructor expects one variable labeled `collection`, which can // be an array of subjects. It also can take a `sortFunction` that can be used to sort the collection initialize: function ($super, args) { $super(args); this._collection = $A(args.collection || []); this._sortFunction = args.sortFunction; if (this._sortFunction) { this._collection.sort(this._sortFunction); } }, // Get a value in the `Collection` from a particular `index`. Will throw errors for bad indices. at: function (index) { if (! this.inRange(index)) { throw 'svc.Collection.at: invalid index.'; } return this._collection[index]; }, // Get a particular `subject` from the `collection`. Uses the `subject`s isEqual property to get the object, // returns `null` if nothing is found. get: function (subject) { return this._collection.find( function (entry) { return entry.isEqual(subject); } ); }, // Return all items in the collection. getAll: function () { return this._collection; }, // Determines where a `subject` is in the `collection`. Uses the `subject`s isEqual property to get the object, // and will return -1 if not found. indexOf: function (subject) { var index = -1; this._collection.find( function (entry) { ++index; return entry.isEqual(subject); } ); return index === this.size() ? -1 : index; }, // Determines whether or not an index fits into the `collection`. inRange: function (index) { return index >= 0 && index < this.size(); }, // Returns the size of the collection. size: function () { return this._collection.length; }, // Add a `subject` to the `collection` if it isn't already in the `collection`. add: function (subject) { if (this.get(subject)) { return; } this._collection.push(subject); if (this._sortFunction) { this._collection.sort(this._sortFunction); } this.notify('collection:add', subject); subject.notify('collection:add'); }, // Remove all `subject`s from the `collection`. clear: function () { var cleared = this.getAll(); this._collection = $A(); cleared.invoke('notify', 'collection:clear'); this.notify('collection:clear'); }, // Remove a single `subject` from the `collection`. remove: function (subject) { var entry = this.get(subject); if (! entry) { return; } this._collection = this._collection.without(entry); entry.notify('collection:remove'); this.notify('collection:remove', entry); return entry; }, _each: function (iterator) { this._collection._each(iterator); } }); // Mixin `Enumerable` functionality. svc.Collection.addMethods(Enumerable);
2. View
View:负责数据的可视化表示,接受Subject的通知后更新视图(体现在订阅/取消订阅)。
(1)View接口:完成以上基本功能
svc.View = Class.create({ // Our constructor calls draw and subscribes a teardown method to the destroy event. It requires a // `subject` variable which will be the `Subject` the view subscribes to. initialize: function (args) { this._subject = args.subject; this._element = this.draw(); this._subscribedFunctions = $H(); this.subscribe('subject:destroy', this.tearDown.bind(this)); }, // This will be where you define the main element of a view, you can also define other elements // which can be stored as instance variables to reduce page wide scans. draw: function () { throw "View.js: draw must be defined in a subclass."; }, // This will need to go, but is required for now. update: function () { // stub method, needs to be impemented in subclasses as well. }, // The default method that gets called when the subject is destroyed. It removes the element // from the page and unsubscribes all events. Feel free to override it as needed. tearDown: function () { var element = this.getElement(); if (element) { element.hide(); var parentElement = element.parentNode; if (parentElement) { parentElement.removeChild(element); } } this.unsubscribeAll(); }, // Gets the main `element` of the view. getElement: function () { return this._element; }, // Get the `subject` the view is watching. getSubject: function () { return this._subject; }, // Subscribe a `function` to a particular `notification` issued by the subject. subscribe: function (notification, fn) { this._subscribedFunctions.set(notification, fn); this.getSubject() && this.getSubject().subscribe(notification, fn); }, // Unsubscribe from a particular `notification` issued by the `subject`. unsubscribe: function (notification) { var fn = this._subscribedFunctions.get(notification); this._subscribedFunctions.unset(notification); this.getSubject() && this.getSubject().unsubscribe(notification, fn); }, // Clear out all subscribed functions for the view. unsubscribeAll: function () { this._subscribedFunctions.keys().each(this.unsubscribe.bind(this)); } });
(2)ActionView接口:View接口的补充,根据用户输入修改视图
svc.ActionView = Class.create(svc.View, { // Our initializer takes an `action`, `controller`, and `events` along with the variables a `View` needs. The // `controller` is a `Controller` object and the `action` is the name of the function you wish to call // when the user completes the required action. Users define `events` that the view will respond to. initialize: function ($super, args) { this._action = args.action; this._controller = args.controller; // `field` gets set in draw() this._field = null; $super(args); // We store the fire function so it can be unregistered later. this._boundFireFunction = this.fire.bind(this); this.observeDOMEvents(args.events); }, // Get the `ActionView`s `field`. getField: function () { return this._field; }, // We associate the passed in `events` with the view so that the view will call out to the controller // when it's interacted with. observeDOMEvents: function (events) { // We don't operate on `ActionView`s without fields. if (! this.getField()) { return; } if (!Object.isArray(events)) { events = [events]; } events.uniq().compact().each( function (event) { this.getField().observe(event, this._boundFireFunction); }.bind(this) ); }, // We call the desired `action` in the `controller` and include the `subject` as the first variable. fire: function () { var args = $A(arguments); args.unshift(this.getSubject()); this._controller[this._action].apply(this._controller, args); } });
3.Controller
Controller:位于视图和模型中间,负责接受用户的输入,将输入进行解析并反馈给Subject。
(1)AjaxController接口:完成以上基本功能。
svc.AjaxController = Class.create(svc.Controller, { // Our initializer takes an `actionMethod` and an `actionPath` in order to define how // we communicate with the server and where we talk to. initialize: function ($super, args) { $super(args); this._actionPath = args.actionPath; this._actionMethod = args.actionMethod || 'post'; }, // Make a request to the `path` on the server, passing whatever `args` are included as parameters. // We're adding a `callback` here to be called `onComplete`, but we should really rethink this. makeRequest: function (args, callback) { var boundOnSuccess = this.onSuccess.bind(this); var wrappedSuccess = callback && typeof(callback) === 'function' ? boundOnSuccess.wrap(callback).bind(this) : boundOnSuccess; var req = new Ajax.Request(this.path(), { method: this.method(), parameters: this.parameters().update(args), onCreate: this.onCreate.bind(this), onSuccess: wrappedSuccess, onFailure: this.onFailure.bind(this), onComplete: this.onComplete.bind(this) }); }, // Defines the `method` of the request (usually `POST` or `GET`). method: function () { if (! this._actionMethod) { throw "AjaxController.js: method must be defined"; } return this._actionMethod; }, // The method called when the AJAX request is completed. onComplete: Prototype.emptyFunction, // The method called when the AJAX request is created. onCreate: Prototype.emptyFunction, // The method called when the AJAX request fails. onFailure: Prototype.emptyFunction, // The method called when the AJAX request succeeds. onSuccess: Prototype.emptyFunction, // An object representing the parameters, this should be extended or overwritten. parameters: function () { return $H(); }, // Retrieves the `actionPath` of the request. path: function () { if (! this._actionPath) { throw "AjaxController.js: path must be defined"; } return this._actionPath; } });
(2)AjaxSingleRequestController接口:AjaxController接口的补充,实现互斥访问功能
// AjaxSingleRequestController // ------------- // This creates a simple mutex lock on AJAX queries so that only one happens at a time. It inherits from // the normal `AjaxController` svc.AjaxSingleRequestController = Class.create(svc.AjaxController, { // Our initializer is the same as `AjaxController`. initialize: function ($super, args) { $super(args); this._inProgress = false; }, // Make a request if nothing is in progress. makeRequest: function ($super, args, callback) { if (this._inProgress) { return; } this._inProgress = true; $super(args, callback); }, // When the request finishes, unlock the progress lock onComplete: function () { this._inProgress = false; } });
二、给项目带来的好处
- 一个模型提供不同的多个视图表现形式,也能够为一个模型创建新的视图而无须重写模型。一旦模型的数据发生变化,模型将通知有关的视图,每个视图相应地刷新自己。
- 模型可复用。因为模型是独立于视图的,所以可以把一个模型独立地移植到新的平台工作。
- 提高开发效率。在开发界面显示部分时,你仅仅需要考虑的是如何布局一个好的用户界面;开发模型时,你仅仅要考虑的是业务逻辑和数据维护,这样能使开发者专注于某一方面的开发,提高开发效率。