...
素材网 品牌美学馆标志
主站 营销美学 品牌设计 免费素材
681

JavaScript组件之旅:编码实现和算法 1

说上期我们讨论了队列管理组件的设计,并且给它取了个响亮而独特的名字:Smart Queue. 这次,我们要将之前的设计成果付诸实践,用代码来实现它。

首先,我们要考虑一下它的源文件布局,也就是决定代码如何拆分到独立的文件中去。为什么要这么做呢?还记得上期结尾处我提到这个组件会使用“外部代码”吗?为了区分代码的用途,决定将代码至少分成两部分:外部代码文件和 Smart Queue 文件。

区分用途只是其一,其二,分散到独立文件有利于代码的维护。试想,以后的某一天你决定要在现有的队列管理基本功能之上,添加一些新的扩展功能,或是把它包装成某个实现特定任务的组件,而又希望保持现有功能(内部实现)和调用方式(对外接口)不变,那么将新的代码写到单独的文件是最好的选择。

嗯,下期会重点谈谈文件布局的话题,现在要开始切入正题了。第一步,当然是要为组件创建自己的命名空间,组件所有的代码都将限制在这个顶层命名空间内:

var SmartQueue = window.SmartQueue || {};
SmartQueue.version = '0.1';

初始化的时候,如果碰到命名空间冲突就把它拉过来用。通常这个冲突是由重复引用组件代码导致的,因此“拉过来用”会将对象以同样的实现重写一次;最坏的情况下,如果碰巧页面上另一个对象也叫 SmartQueue, 那不好意思了,我会覆盖你的实现——如果没有进一步的命名冲突,基本上两个组件可以相安无事地运行。同时顺便给它一个版本号。

接着,按三个优先级为 SmartQueue 创建三个队列:

var Q = SmartQueue.Queue = [[], [], []];

每个都是空数组,因为还没有任务加进去嘛。又顺便给它建个“快捷方式”,后面要访问数组直接写 Q[n] 就可以啦。

接下来,我们的主角 Task 隆重登场——怎么 new 一个 Task, 定义在这里:

var T = SmartQueue.Task = function(fn, level, name, dependencies) {
if(typeof fn !== FUNCTION) {
throw new Error('Invalid argument type: fn.');
}
this.fn = fn;
this.level = _validateLevel(level) ? level : LEVEL_NORMAL;

// detect type of name
this.name = typeof name === STRING && name ? name : 't' + _id++;

// dependencies could be retrieved as an 'Object', so use instanceof instead.
this.dependencies = dependencies instanceof Array ? dependencies : [];
};

里面的具体细节就不说了,有必要的注释,一般我们的代码也能做到自我描述,后面代码也是这样。这里告诉客户(使用者):你想新建一个 SmartQueue.Task 实例,就要至少传一个参数给这个构造函数(后 3 个都可以省略进行缺省处理),否则抛出异常伺候。

但是这还不够,有时候,客户希望从已有 Task 克隆一个新实例,或是从一个“残废体”(具有部分 Task 属性的对象)修复出“健康体”(真正的 Task 对象实例),通过上面的构造方式就有点不爽了——客户得这样写:

var task1 = new SmartQueue.Task(obj.fn, 1, '', obj.dependencies);

只想传 fn 和 dependencies 两个属性,不想做额外的事情。好吧,我们来重构一下构造函数:

var _setupTask = function(fn, level, name, dependencies) {
if(typeof fn !== FUNCTION) {
throw new Error('Invalid argument type: fn.');
}
this.fn = fn;
this.level = _validateLevel(level) ? level : LEVEL_NORMAL;

// detect type of name
this.name = typeof name === STRING && name ? name : 't' + _id++;

// dependencies could be retrieved as an 'Object', so use instanceof instead.
this.dependencies = dependencies instanceof Array ? dependencies : [];
};

var T = SmartQueue.Task = function(task) {
if(arguments.length > 1) {
_setupTask.apply(this, arguments);
} else {
_setupTask.call(this, task.fn, task.level, task.name, task.dependencies);
}

// init context/scope and data for the task.
this.context = task.context || window;
this.data = task.data || {};
};

如此一来,原来的构造方式可以继续工作,而上面的懒人可以这样传入一个“残废体”:

var task1 = new SmartQueue.Task({fn: obj.fn, dependencies: obj.dependencies});

当构造函数收到多个参数时,按之前的方案等同处理;否则,视唯一的参数为 Task 对象或“残废体”。这里通过 JavaScript 中的 apply/call 方法将新实例传给重构出来的 _setupTask 方法,作为该方法的上下文 (context, 也有称为 scope), apply/call 是 JavaScript 在方法之间传递上下文的法宝,要用心体会哦。同时,允许用户定义 task.fn 在执行时的上下文,并将自定义的数据传递给执行中的 fn.

经典的 JavaScript 对象三段式是什么?

  1. 定义对象的构造函数
  2. 在原型上定义属性和方法
  3. new 对象,拿来用

所以,下面要为 SmartQueue.Task 对象的原型定义属性和方法。上期分析过 Task (任务)有几个属性和方法,部分属性我们已经在 _setupTask 中定义了,下面是原型提供的属性和方法:

T.prototype = {
enabled: true,
register: function() {
var queue = Q[this.level];
if(_findTask(queue, this.name) !== -1) {
throw new Error('Specified name exists: ' + this.name);
}
queue.push(this);
},
changeTo: function(level) {
if(!_validateLevel(level)) {
throw new Error('Invalid argument: level');
}
level = parseInt(level, 10);
if(this.level === level) {
return;
}
Q[this.level].remove(this);
this.level = level;
this.register();
},
execute: function() {
if(this.enabled) {
// pass context and data
this.fn.call(this.context, this.data);
}
},
toString: function() {
var str = this.name;
if(this.dependencies.length) {
str += ' depends on: [' + this.dependencies.join(', ') + ']';
}
return str;
}
};

如你所见,逻辑非常简单,也许你已经在一分钟内扫过了代码,嘴角不经意间露出一丝心领神会。不过,这里要说的是简单而且通常最不被重视的 toString 方法。在一些高级语言中,为自定义对象实现 toString 方法被作为最佳实践准则而推荐,为什么呢?因为 toString 可以很方便地在调试器中提供有用的信息,可以方便地将对象基本信息写入日志;在统一的编程模式中,实现 toString 可以让你少写一些代码。

>

品牌风云最新热文

房地产的VI做起来,就像是在搞艺术 唱片封面设计 | 看得见的音乐艺术 “美味酒廊”VI设计欣赏 美学茶话:读懂齐白石的画,我放弃了画画

访问手机站
© Copyright 素材公社tooopen.com 本站部份内容收集于网络,如有侵犯您权益请告知删除| 湘ICP备11010972号-1