Yen

二〇一三年我翻译了一本我见过 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 什么的,它能接收多种参数:

  1. new jSelect('.foo > p'):CSS 选择器
  2. new jSelect(document.body):DOM 元素
  3. new jSelect(document.scripts):DOM 元素集合
  4. new jSelect({}):原始数据类型(Object、Array 等)

并能按传入参数类型的不同,分别作如下处理:

  1. 根据当前上下文查找 CSS 选择器,将返回的 DOM 元素集合按第三点处理
  2. 将 DOM 元素作为函数实例的第 0 个属性
  3. 将 DOM 元素集合展开,放到第 012…… 等属性
  4. 如果是 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.animatejSelect.fn.onjSelect.fn.offjSelect.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')

哎呀真是充满了各种怪诞。