二〇一三年我翻译了一本我见过 O’Reilly 出版的最水的书,名叫《DOM Enlightenment》。 我将它译作《DOM 启蒙》,中间拖延症几经反复,终于在二〇一四年初交稿并付梓。
除最后一章讲述的如何编写 jQuery 风格的 DOM 库,以及倒数第二章讲述的 DOM 事件外,本书内容 都是比较粗浅的 DOM API 介绍,文字说明和代码演示都大同小异,在翻译的过程中我一度认为作者是 写了个程序批量生成这些章节的。为了不使翻译过程太过机械化,个别地方我还顺手翻译了示例代码里的 注释,努力做一个尽责的翻译。
不过本文无关这一翻译过程,本文想说的是一个轮子,一个受《DOM 启蒙》最后一章启发,API 完全抄袭 jQuery 的 DOM 操作库。
缘起
受工作内容所限,在我的日常工作中,通常是没办法直接引用成熟的前端库的,不然整个页面的首次加载
尺寸肯定会超出技术规格的要求。在之前,我们都是直接裸写 DOM 操作、事件绑定等代码。如果我们是
在开发无线 Web 应用,那这么做其实是对的,iOS 和 Android 对基本的 DOM API 标准支持都很好,
document.querySelector
、.firstElementChild
、.addEventListener
等全都可以用。
但在我们还需要支持 IE 的历史版本,尽情使用原生 DOM API 对我们来说还是太奢侈了。比如我们经常 得写这样的代码来获取当前元素的相邻元素节点:
/*
* 还得事先加上如下修补代码:
*
* window.Node = window.Node || { ELEMENT_NODE: 1 }
*/
function nextElementSibling(el) {
var sibling
while (sibling = el.nextSibling) {
if (sibling.nodeType == Node.ELEMENT_NODE) {
return sibling
}
}
}
这种时候就很怀念 jQuery 或者其他 DOM 操作库了:
$(el).next()
炮制
假设你有个函数,名字叫做 jSelect
什么的,它能接收多种参数:
new jSelect('.foo > p')
:CSS 选择器new jSelect(document.body)
:DOM 元素new jSelect(document.scripts)
:DOM 元素集合new jSelect({})
:原始数据类型(Object、Array 等)
并能按传入参数类型的不同,分别作如下处理:
- 根据当前上下文查找 CSS 选择器,将返回的 DOM 元素集合按第三点处理
- 将 DOM 元素作为函数实例的第
0
个属性 - 将 DOM 元素集合展开,放到第
0
、1
、2
…… 等属性 - 如果是 Array,则像传入 DOM 元素集合一般展开;如果是 Object,则作为第
0
个属性
function jSelect(selector, context) {
context = context || document
// case 1
if (typeof selector == 'string') {
return jSelect(context.querySelectorAll(selector))
}
// case 2
else if (selector.nodeType == Node.ELEMENT_NODE) {
this[0] = selector
this.length = 1
}
// case 3 and case 4 - Array
else if ('length' in selector) {
for (var i = 0, len = selector.length; i < len; i++) {
this[i] = selector[i]
}
this.length = selector.length
}
// case 4 - Object
else {
this[0] = selector
this.length = 1
}
this.context = context
}
方法
同时,假设 jSelect.prototype
上有如下方法:
// Traversing
jSelect.prototype.next()
jSelect.prototype.prev()
jSelect.prototype.children()
jSelect.prototype.first()
jSelect.prototype.last()
// Query
jSelect.prototype.find()
// Manipulation
jSelect.prototype.remove()
jSelect.prototype.html()
// ... and so on.
那现在你就可以愉快地查询、遍历、操作 DOM 了:
new jSelect('.foo > p:first-child').next().remove()
instanceof
而且这个函数还能省略掉 new
,不管加不加 new
都会实例化:
function jSelect(selector, context) {
if (!(this instanceof jSelect)) {
return new jSelect(selector, context)
}
// implementation code mentioned above
}
fn
不仅如此,你还能直接通过 jSelect.fn
注册方法,因为:
jSelect.fn = jSelect.prototype
于是你可以:
jSelect.fn.highlight = function() {
return this.each(function(el) {
$(el).css({ backgroundColor: 'yellow' })
})
}
Array-like Object
像 jSelect 构造函数返回的实例这种对象({ '0': ..., '1': ..., '2': ..., length: 3 }
)
我们管它叫 Array-like Object,在《DOM 启蒙》里我将它译作“类数组对象”。
有一个优化这种对象在 Chrome 终端里的输出格式的小窍门,就是再给这个对象实现一个
.splice
方法。当终端发现当前对象既有 .length
又有 .splice
,就会认为它是个数组,
输出格式就会变成数组:
所以我们还需要实现 jSelect.prototype.splice
,偷懒直接用数组的吧:
jSelect.fn.splice = Array.prototype.splice
到这里,你就已经实现了一个高仿 jQuery 的 DOM 操作库。其实,jQuery 的作者在发布 jQuery 之前,原本就想把它叫做 jSelect 的,可惜后者域名被人注册了,只好改名 jQuery。
界线
延续这份 jSelect,假如我们又实现了 jSelect.fn.animate
、jSelect.fn.on
、
jSelect.fn.off
、jSelect.fn.ajax
,岂不是彻底山寨了 jQuery?
浏览器兼容性又该怎么办呢?有些修复起来颇麻烦、代价颇昂贵(代码量飙升)的坑,要不要处理?
这个时候你就需要厘清自己的思路,你是需要一个完完整整带有自己个人风格的 jQuery,还是仅仅为了 解决实际业务中某类特定问题,又很喜欢 jQuery 的 API 风格,所以需要一个 jQuery API 子集?
作为一个有职业操守的前端工程师,造轮子的时候一定要时刻提醒自己这些问题……
jQuery 的不足
在迷你模块盛行的今天(君不见 NPM 里到处都是代码量一百行不到的模块),大而全的前端库愈发不受
欢迎了。有的人不喜欢 jQuery.fn.ajax
,所以有了 superagent;有的人指出 jQuery
里的 Promise 实现不符合规范,无法和 Q、bluebird 等 Promise 实现愉快地互通。
不过,我等受 jQuery 启发良多,现在吃饱了骂厨子,指摘 jQuery 的不是,其实是挺没必要的。 毕竟这是一款历史悠久的前端库了,有些包袱是不得不背的。
推荐阅读同事墨智老师的书《jQuery 技术内幕》。
yen
从前文中的 jSelect 拓展开,加入事件绑定与触发,就成了我们组最近开源的模块 yen。jQuery
的简写是个美元符 $
,人民币的符号 ¥,和日元的符号相同,所以我们就把这个小小山寨品
叫做 yen
。
然而用的时候仍然是:
var $ = require('yen')
哎呀真是充满了各种怪诞。