超赞的动手创建JavaScript框架的详细教程


Posted in Javascript onJune 30, 2015

 觉得Mootools不可思议?想知道Dojo是如何实现的?对JQuery的技巧感到好奇?在这篇教程里,我们将探寻框架背后的秘密,然后试着自己动手建立一个你所喜爱的框架的简易版本。

我们几乎每天都在使用各种各样的JavaScript框架。当你刚入门的时候,方便的DOM(文档对象模型)操作让你觉得JQuery这样的东西非常棒。这是因为:首先,对于新手来说DOM太难理解了;当然,对于一个API来说难以理解可不是什么好事。其次,浏览器间的兼容性问题非常令人困扰。

  •     我们将元素包装成对象是因为我们想要能够为对象添加方法。
  • 在这个教程里,我们将试着从头实现这些框架之一。是的,这会很有趣,不过在你太过兴奋前我要澄清几点:
  •     这不会是一个功能很完善的框架。的确,我们要写很多东西,但它还算不上JQuery。可是我们将要做的事情会让你体验到在真正编写框架的感觉。
  •     我们不打算保证全方位的兼容性。我们将要编写的框架能够在 Internet Explorer 8+、Firefox 5+、Opera 10+、Chrome和Safari上工作。
  •     我们的框架不会覆盖到所有可能的功能。比如说,我们的append和preappend方法只有在你传给它一个我们框架的实例时才能工作;我们不会用原生的DOM节点和节点列表。

    另外:尽管在教程中我们不会为我们的框架编写测试用例,但是我已经在第一次开发它的时候做好了。你可以从 Github上获取框架和测试用例的代码。

第一步: 创建框架模板

我们将从一些包装代码开始,它将容纳我们的整个框架。这是典型的立即函数(IIFE).
 

window.dome = (function () {
 function Dome (els) {
 }
 var dome = {
  get: function (selector) {
  }
 };
 return dome;
}());

你可以看到,我们的框架叫做dome,因为它是一个基本的DOM框架。没错,基本(lame有“瘸子”、“不完整”的意思,dom加lame等于dome)的。

我们已经有了一些东西。 首先,我们有了一个函数;它将成为构造框架的对象实例的构造函数;那些对象将会包含我们选择和创建的元素。

然后,我们有了一个dome对象,它就是我们的框架对象;你可以看到它最终作为函数的返回值返回给了函数调用者(译注:赋值给了window.dome)。这里还有一个空的get函数,我们将用它从页面里选取元素。那么,我们来填充代码吧。

第二步: 获取元素

dome的get函数只有一个参数,但是它可以是很多东西。如果它一个string(字符串),我们将假定它是一个CSS(层叠样式表)选择器;不过我们也可能得到一个DOM节点或者DOM节点列表。
 

get: function (selector) {
 var els;
 if (typeof selector === "string") {
  els = document.querySelectorAll(selector);
 } else if (selector.length) {
  els = selector;
 } else {
  els = [selector];
 }
 return new Dome(els);
}

我们用document.querySelectorAll来简单的选择元素:当然,这将限制我们的浏览器兼容性,不过对于这种情况还是可以接受的。如果selector不是string类型,我们将检查它的length属性。如果存在,我们就知道我们得到的是一个节点列表;否则,就是一个单独的元素,我们将它放到一个数组里。这是因为我们要在下面向Dome传递一个数组。你可以看到,我们返回了一个新的Dome对象。让我们回到Dome函数并且为它填充代码。

第三步: 创建Dome实例

这是Dome函数:
 

function Dome (els) {
 for(var i = 0; i < els.length; i++ ) {
  this[i] = els[i];
 }
 this.length = els.length;
}

    我强烈建议你去深入研究一些你喜欢的框架

这非常简单:我们只是遍历了els的所有元素,并且把它们存储在一个以数字为索引的新对象里。然后我们添加了一个length属性。

但是这有什么意义呢?为什么不直接返回元素?因为:我们将元素包装成对象是因为我们想要能够为对象添加方法;这些方法能够让我们遍历这些元素。实际上这正是JQuery的解决方案的浓缩版。

我们的Dome对象已经返回了,现在让我们来为它的原型(prototype)添加一些方法。我会直接把那些方法写在Dome函数下面。

第四步:添加几个实用工具

要添加的第一批功能是些简单的工具函数。由于Dome对象可能包含至少一个DOM元素,那么我们需要在几乎每一个方法里面都遍历所有元素;这样,这些工具才会给力。

我们从一个map函数开始:   
 

Dome.prototype.map = function (callback) {
 var results = [], i = 0;
 for ( ; i < this.length; i++) {
  results.push(callback.call(this, this[i], i));
 }
 return results;
};

当然,这个map函数有一个入参,一个回调函数。我们遍历Dome对象所有元素,收集回调函数的返回值到结果集中。注意我们是怎样调用回调函数的:
 

callback.call(this, this[i], i));

通过这种方式,函数将在Dome实例的上下文中被调用,并且函数接收到两个参数:当前元素和元素序号。

我们也想要一个foreach函数。事实上这很简单:
 

Dome.prototype.forEach(callback) {
 this.map(callback);
 return this;
};

由于map函数和foreach函数之间的不同仅仅是map需要返回些东西,我们可以仅仅将回调传给this.map然后忽略返回的数组;代替返回的是,我们将返回this,来使我们的库呈链式。foreach会被频繁的调用,所以,注意当一个函数的回调被返回,事实上,返回的是Dome实例。例如,下面的方法事实上就返回了Dome实例:
 

Dome.prototype.someMethod1 = function (callback) {
 this.forEach(callback);
 return this;
};
Dome.prototype.someMethod2 = function (callback) {
 return this.forEach(callback);
};

还有一个:mapOne。很容易就知道这个函数是做什么的,但是真正的问题是,为什么需要它?这就需要一些我们称之为"库哲学"的东西了。
一个简短的"哲学"阐释

  •     首先,对于一个初学者来说,DOM很让人纠结;它的API不完善。

如果构建一个库仅仅是写代码,那就不是什么难事。但是当我开发这个库时,我发现那些不完善的部分决定了一定数量的方法的实现方式。

很快,我们要去构建一个返回被选择元素的文本的text方法。如果Dome对象包含多个DOM节点(比如dome.get("li")),返回什么?如果你就像jQuery那样($("li").text())很简单的编写,你将得到一个字符串,这个字符串是所有元素的文本的直接拼接。有用吗?我认为没用,但是我不认为没有更好的办法。

对于这个项目,我将以数组方式返回多个元素的文本,除非数组里只有一个元素,那么我仅仅返回一个文本字符串,而不是一个包含了一个元素的数组。我想你会经常去获取单个元素的文本,所以我们优化了那种情况。但是,如果你想去获取多个元素的文本,我们的返回你也会用着很爽。
回到代码

那么,mapOne方法仅仅是简单的运行map函数,然后返回数组,或者一个数组里的元素。如果你仍然不确定这是如何有用,坚持一下,你就会看到!
 

Dome.prototype.mapOne = function (callback) {
 var m = this.map(callback);
 return m.length > 1 ? m : m[0];
};

 
第5步: 处理Text和HTML

接着,让我们来添加文本方法。就像jQuery,我们可以传递一个string值,设置节点元素的text值,或者通过无参方法得到返回的text值。
 

Dome.prototype.text = function (text) {
 if (typeof text !== "undefined") {
  return this.forEach(function (el) {
   el.innerText = text;
  });
 } else {
  return this.mapOne(function (el) {
   return el.innerText;
  });
 }
};

如你所料,当我们设置(setting)或者得到(getting)value值时,需要检查text的值。要注意的是如果justif(文本)方法不起作用,是因为text为空字符串是一个错误的值。

如果我们设置(setting)时,可是使用一个forEach 遍历元素,设置它们的innerText属性。如果我们得到(getting)时,返回元素的innerText属性。在使用mapOne方法是要注意:如果我们正在处理多个元素,将返回一个数组;其他的则还是一个字符串。

如果html方法使用innerHTML属性而不是innerText,它将会更优雅的处理涉及text文本的事情。
 

Dome.prototype.html = function (html) {
 if (typeof html !== "undefined") {
  this.forEach(function (el) {
   el.innerHTML = html;
  });
  return this;
 } else {
  return this.mapOne(function (el) {
   return el.innerHTML;
  });
 }
};

就像我说过的:几乎相同的。

第六步: 修改类

下一步,我们想对class进行操作,所以添加能addClass()和removeClass()。addClass()的参数是一个class名称或者名称的数组。为了实现动态参数,我们需要对参数的类型进行判断。如果参数是一个数组,那么遍历这个数组,将元素添加上这些class名称,如果参数是一个字符串,则直接加上这个class名称。函数需要确保不将原来的class名称弄乱。
 

Dome.prototype.addClass = function (classes) {
 var className = "";
 if (typeof classes !== "string") {
  for (var i = 0; i < classes.length; i++) {
   className += " " + classes[i];
  }
 } else {
  className = " " + classes;
 }
 return this.forEach(function (el) {
  el.className += className;
 });
};

很直观吧?嘿嘿

现在,写下removeClass(),同样简单。不过每次只允许删除一个class名称。
 

Dome.prototype.removeClass = function (clazz) {
 return this.forEach(function (el) {
  var cs = el.className.split(" "), i;
  while ( (i = cs.indexOf(clazz)) > -1) {
   cs = cs.slice(0, i).concat(cs.slice(++i));
  }
  el.className = cs.join(" ");
 });
};

对于每一个元素,我们都将el.className 分割成一个字符串数组。那么我们使用一个while循环连接,直到cs.indexOf(clazz)返回值大于-1。我们将得到的结果join成el.className。

第七步: 修复一个IE引起的BUG

我们处理的最糟浏览器是IE8.在这个小小的库中,只有一个IE引起的BUG需要去修复; 并且谢天谢地,修复它非常简单.IE8不支持Array的方法indexOf;我们需要在removeClass方法中使用到它, 下面让我们来完成它:
 

if (typeof Array.prototype.indexOf !== "function") {
 Array.prototype.indexOf = function (item) {
  for(var i = 0; i < this.length; i++) {
   if (this[i] === item) {
    return i;
   }
  }
  return -1;
 };
}

它看上去非常简单,并且它不是完整实现(不支持使用第二个参数),但是它能实现我们的目标.

第8步: 调整属性

现在,我们想要一个attr函数。这将很容易,因为它几乎和text方法或者html方法是一样的。像这些方法,我们都能够设置和得到属性:我们将设置一个属性的名称和值,同时只通过参数名来得到值。
 

Dome.prototype.attr = function (attr, val) {
 if (typeof val !== "undefined") {
  return this.forEach(function(el) {
   el.setAttribute(attr, val);
  });
 } else {
  return this.mapOne(function (el) {
   return el.getAttribute(attr);
  });
 }
};

如果形参有一个值,我们将遍历元素并通过元素的setAttribute方法设置属性值。另外,我们将使用mapOne返回通过getAttribute方法得到参数。

第9步: 创建元素

像任何一个优秀的框架一样,我们也应该能够创建元素。当然,在Demo实例中没有一个好的方法,所以让我们来把方法加入到demo工程中。
 

var dome = {
 // get method here
 create: function (tagName, attrs) {
 }
};

正如你所看到的:我们需要两个形参:元素名,和一个参数对象。大多数的属性通过我们的arrt方法被使用,但是tagName和attrs却有特殊待遇。我们为className属性使用addClass方法,为text属性使用text方法。当然,我们首先要创建元素,和Demo对象。下面就是所有的作用:
 

create: function (tagName, attrs) {
 var el = new Dome([document.createElement(tagName)]);
  if (attrs) {
   if (attrs.className) {
    el.addClass(attrs.className);
    delete attrs.className;
   }
  if (attrs.text) {
   el.text(attrs.text);
   delete attrs.text;
  }
  for (var key in attrs) {
   if (attrs.hasOwnProperty(key)) {
    el.attr(key, attrs[key]);
   }
  }
 }
 return el;
}

如上,我们创建了元素,将他发送到新的Dmoe对象中。接着,我们处理所有属性。注意:当使用完className和text属性后,我们不得不删除他们。这将保证当我们遍历其他的键时,它们还能被使用。当然,我们最终通过返回这个新的Demo对象。

我们创建了新的元素,我们想要将这些元素插入到DOM,对吧?

第10步:尾部添加(Appending)与头部添加(Prepending)元素

下一步,我们来实现尾部添加与头部添加方法。考虑到多种场景,实现这些方法可能有些棘手。下面是我们的想要达到的效果:
 

dome1.append(dome2);
dome1.prepend(dome2);

    IE8对我们来说就是一奇葩。

尾部添加或头部添加,包括以下几种场景:

  •     单个新元素添加至单个或多个已存在元素中
  •     多个新元素添加至单个或多个已存在元素中
  •     单个已存在元素添加至单个或多个已存在元素中
  •     多个已存在元素添加至单个或多个已存在元素中

注意:这里的”新元素“表示还未加入DOM中节点元素,”已存在元素“指已存在于DOM中的节点元素。
现在让我们一步步来实现之:
 

Dome.prototype.append = function (els) {
 this.forEach(function (parEl, i) {
  els.forEach(function (childEl) {
  });
 });
};

假设参数els是一个DOM对象。一个功能完备的DOM库应该能处理节点(node)或节点序列(nodelist),但现在我们不作要求。首先遍历需要被添加进的元素 (父元素),再在这个循环中遍历将被添加的元素 (子元素)。
    如果将一个子元素添加至多个父元素,需要克隆子元素(避免最后一次操作会移除上一次添加操作)。可是,没必要在初次添加的时候就克隆,只需要在其它循环中克隆就可以了。因此处理如下:
 

if (i > 0) {
 childEl = childEl.cloneNode(true);
}

变量i来自外层forEach循环:它表示父级元素的序列号。第一个父元素添加的是子元素本身,而其他父元素添加的都是目标子元素的克隆。因为作为参数传入的子元素是未被克隆的,所以,当将单个子元素添加至单个父元素时,所有的节点都是可响应的。
最后,真正的添加元素操作:

parEl.appendChild(childEl);

因此,组合起来,我们得到以下实现:
 

Dome.prototype.append = function (els) {
 return this.forEach(function (parEl, i) {
  els.forEach(function (childEl) {
   if (i > 0) {
    childEl = childEl.cloneNode(true);
   }
   parEl.appendChild(childEl);
  });
 });
};

prepend方法

我们按照相同的逻辑实现prepend方法,其实也相当简单。
 

Dome.prototype.prepend = function (els) {
 return this.forEach(function (parEl, i) {
  for (var j = els.length -1; j > -1; j--) {
   childEl = (i > 0) ? els[j].cloneNode(true) : els[j];
   parEl.insertBefore(childEl, parEl.firstChild);
  }
 });
};

不同点在于添加多个元素时,添加后的顺序会被反转。所以不能采用forEach循环,而是用倒序的for循环代替。同样的,在添加至非第一个父元素时需克隆目标子元素。

第十一步: 删除节点

对于我们最后一个节点的操作方法,从dom中删除这些节点,很简单,只需要:

Dome.prototype.remove = function () {
 return this.forEach(function (el) {
  return el.parentNode.removeChild(el);
 });
};

只需要通过节点的迭代和在他们的父节点调用删除子节点方法。比较好的是这个dom对象依然正常工作(感谢文档对象模型吧)。我们可以在它上面使用我们想使用的方法,包括插入,预插回DOM,很漂亮,不是吗?

第12步:事件处理

最后,却是最重要的一环,我们要写几个事件处理函数。

如你所知,IE8依然使用旧的IE事件,因此我们需要为此作检测。同时,我们也要做好使用DOM 0 级事件的准备。

查看下面的方法,我们稍后会讨论:
 

Dome.prototype.on = (function () {
 if (document.addEventListener) {
  return function (evt, fn) {
   return this.forEach(function (el) {
    el.addEventListener(evt, fn, false);
   });
  };
 } else if (document.attachEvent) {
  return function (evt, fn) {
   return this.forEach(function (el) {
    el.attachEvent("on" + evt, fn);
   });
  };
 } else {
  return function (evt, fn) {
   return this.forEach(function (el) {
    el["on" + evt] = fn;
   });
  };
 }
}());

在这里,我们用到了立即执行函数(IIFE),在函数内我们做了特性检测。如果document.addEventListener方法存在,我们就使用它;另外我们也检测document.attachEvent,如果没有就使用DOM 0级方法。请注意我们如何从立即执行函数中返回最终函数:其最后会被分配到Dome.prototype.on。在做特性检测时,与每次运行函数时检测相比,这样的方式分配适合的方法更加方便。

事件解绑方法off与on方法类似:.
 

Dome.prototype.off = (function () {
 if (document.removeEventListener) {
  return function (evt, fn) {
   return this.forEach(function (el) {
    el.removeEventListener(evt, fn, false);
   });
  };
 } else if (document.detachEvent) {
  return function (evt, fn) {
   return this.forEach(function (el) {
    el.detachEvent("on" + evt, fn);
   });
  };
 } else {
  return function (evt, fn) {
   return this.forEach(function (el) {
    el["on" + evt] = null;
   });
  };
 }
}());
Javascript 相关文章推荐
使用户点击后退按钮使效三行代码
Jul 07 Javascript
javascript引用对象的方法代码
Aug 13 Javascript
JavaScript this调用规则说明
Mar 08 Javascript
影响jQuery使用的14个方面
Sep 01 Javascript
js获取所有checkbox的值的简单实例
May 30 Javascript
详解自动生成博客目录案例
Dec 09 Javascript
BootStrap中jQuery插件Carousel实现轮播广告效果
Mar 27 jQuery
jquery将标签元素的高设为屏幕的百分比
Apr 19 jQuery
Vue按需加载的具体实现
Dec 02 Javascript
解决淘宝cnpm 安装后cnpm不是内部或外部命令的问题
May 17 Javascript
webpack 3.X学习之多页面打包的方法
Sep 04 Javascript
移动端如何用下拉刷新的方式实现上拉加载
Dec 10 Javascript
JavaScript中Null与Undefined的区别解析
Jun 30 #Javascript
jQuery结合AJAX之在页面滚动时从服务器加载数据
Jun 30 #Javascript
深入探究使JavaScript动画流畅的一些方法
Jun 30 #Javascript
使用jQuery在对象中缓存选择器的简单方法
Jun 30 #Javascript
在Node.js应用中读写Redis数据库的简单方法
Jun 30 #Javascript
javascript日期计算实例分析
Jun 29 #Javascript
javascript处理a标签超链接默认事件的方法
Jun 29 #Javascript
You might like
PHP中的string类型使用说明
2010/07/27 PHP
thinkphp数据查询和遍历数组实例
2014/11/28 PHP
php防止sql注入简单分析
2015/03/18 PHP
Laravel 集成 Geetest验证码的方法
2018/05/14 PHP
PHP+MariaDB数据库操作基本技巧备忘总结
2018/05/21 PHP
php 读取文件夹下所有图片、文件的实例
2018/10/17 PHP
jQuery(非HTML5)可编辑表格实现代码
2012/12/11 Javascript
js确认删除对话框效果的示例代码
2014/02/20 Javascript
javascript判断数组内是否重复的方法
2015/04/21 Javascript
jQuery如何使用自动触发事件trigger
2015/11/29 Javascript
jQuery animate和CSS3相结合实现缓动追逐效果附源码下载
2016/04/18 Javascript
微信小程序 radio单选框组件详解及实例代码
2017/01/10 Javascript
微信小程序实现根据字母选择城市功能
2017/08/16 Javascript
使用ECharts实现状态区间图
2018/10/25 Javascript
layui使用button按钮 点击出现弹层 弹层中加载表单的实例
2019/09/04 Javascript
详解vue中$nextTick和$forceUpdate的用法
2019/12/11 Javascript
NodeJS和浏览器中this关键字的不同之处
2021/03/03 NodeJs
python实现判断数组是否包含指定元素的方法
2015/07/15 Python
Python编码类型转换方法详解
2016/07/01 Python
浅谈Python浅拷贝、深拷贝及引用机制
2016/12/15 Python
在scrapy中使用phantomJS实现异步爬取的方法
2018/12/17 Python
Python 调用 zabbix api的方法示例
2019/01/06 Python
Flask框架学习笔记之模板操作实例详解
2019/08/15 Python
Python 元组拆包示例(Tuple Unpacking)
2019/12/24 Python
html5 跨文档消息传输示例探讨
2013/04/01 HTML / CSS
Sephora丝芙兰马来西亚官方网站:国际化妆品购物
2018/03/15 全球购物
英国假睫毛购买网站:FalseEyelashes.co.uk
2018/05/23 全球购物
小米官方旗舰店:Xiaomi
2020/08/07 全球购物
《忆江南》教学反思
2014/04/07 职场文书
电子商务专业毕业生求职信
2014/06/12 职场文书
上课随便讲话检讨书
2014/09/12 职场文书
毕业实习计划书
2015/01/16 职场文书
莫言诺贝尔获奖感言(全文)
2015/07/31 职场文书
解析laravel使用workerman用户交互、服务器交互
2021/04/28 PHP
Mysql服务添加 iptables防火墙策略的方案
2021/04/29 MySQL
A22国内电台短波广播频率表
2022/05/10 无线电