在 JS 中创建、初始化事件

在 Node.js 中,我们可以使用 events 模块方便地创建、监听、发送事件。 对于浏览器而言,同样有一套基于 DOM 的事件相关 API 可供使用。

未标准化之前

创建事件

var event = document.createEvent(type);

返回 DOM Event 对象,type 可以是 "UIEvents""MouseEvent""KeyboardEvent" 等。

初始化事件

根据创建时传入的 type,调用对应的初始化函数:

  • event.initEvent(type, bubbles, cancelable)
  • event.initUIEvent(type, canBubble, cancelable, view, detail)
  • event.initMouseEvent(type, canBubble, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget)
  • event.initKeyboardEvent(typeArg, canBubbleArg, cancelableArg, viewArg, charArg, keyArg, locationArg, modifiersListArg, repeat)
  • 等等

以键盘事件为例:

var event = document.createEvent('KeyboardEvent');
event.initKeyboardEvent();

那些不统一的初始化参数

DOM 2 Events 规范经过多次修改,initKeyboardEvent() 中的参数也几经变迁,如今已经被标记为 deprecated。

Chrome / Safari / IE 中依然可以调用 initKeyboardEvent(),不过参数不尽相同,即使是 Webkit 内核,不同版本也有不同参数(这里的 polyfill 中有一些研究)。

Firefox(Gecko 内核)已经移除 initKeyboardEvent() 方法,可以使用 initKeyEvent() 代替。

initKeyEvent 使用示例(仅 Firefox 有效):

// create a key event
var event = document.createEvent('KeyboardEvent');
// define the event (Press `Shift` + `X`)
event.initKeyEvent(
  'keypress',  // typeArg,
  true,        // canBubbleArg,
  true,        // cancelableArg,
  null         // viewArg,  Specifies UIEvent.view. This value may be null.
  false,       // ctrlKeyArg,
  false,       // altKeyArg,
  true,        // shiftKeyArg,
  false,       // metaKeyArg,
  88,          // keyCodeArg,
  88           // charCodeArg
);
// dispatch the event
document.dispatchEvent(event);

IE 中 initKeyboardEvent() 的详细参数,详见MSDN - initKeyboardEvent method。 需要注意的是其中的 modifiersListArg,类型为 string,举例:'Control Shift Alt' (中间以空格作为间隔)表示同时按下了 ControlShiftAlt 这三个键。

initKeyboardEvent 使用示例(仅 IE9+ 有效):

// create a key event
var event = document.createEvent('KeyboardEvent');
// define the event (Press `Shift` + `X`)
event.initKeyEvent(
  'keypress',  // eventType,
  true,        // canBubble,
  true,        // cancelable,
  null         // viewArg,
  'X',         // keyArg,
  0,           // locationArg,
  'Shift',     // modifiersListArg,
  false,       // repeat,
  '',          // locale
);
// dispatch the event
document.dispatchEvent(event);

标准化之后

创建、初始化事件

目前的 W3C 规范推荐使用事件构造函数创建事件。

var event = new Event(typeArg, eventInit);
var keyboardEvent = new KeyboardEvent(typeArg, KeyboardEventInit);

依然以创建键盘事件为例,向构造函数传入事件类型和一个初始化字典:

var keyboardEvent = new KeyboardEvent('keydown', {
  altKey: true,
  bubbles: true,
  cancelable: true,
  code: 'KeyK',
  composed: true,
  ctrlKey: true,
  key: 'k',
  metaKey: true,
  repeat: true,
  shiftKey: true,
  view: window
});

向前兼容

目前,除 Firefox 外,在 Chrome / Safari 中存在bug,无论是 initKeyboardEvent() 还是使用构造函数,都无法初始化事件的 charCodekeyCodewhich属性,它们始终为 0。详情可以查看相关 Issue:

由于最新的规范里 charCodekeyCodewhich 均被废弃,所以 Webkit 和 Chromium 宣称不会进行修复。而 keycode 尚未被所有浏览器支持,众多的项目也多是判断 charCodekeyCode

可以使用 Object.defineProperties() 设置事件的 charCodekeyCodewhich(这也是 Chrome 团队 Paul Irish 推荐的做法)。

var keyboardEvent = new KeyboardEvent('keydown', {
  altKey: true,
  bubbles: true,
  cancelable: true,
  code: 'KeyK',
  composed: true,
  ctrlKey: true,
  key: 'k',
  metaKey: true,
  repeat: true,
  shiftKey: true,
  view: window
});

Object.defineProperties(keyboardEvent, {
  charCode: {
    get: function () {
      // 只有 keypress 事件和可打印字符才返回有效的 charCode
      if (this.type === 'keypress' && this.key && this.key.length === 1) {
        return this.key.charCodeAt(0);
      }
      return 0;
    }
  },
  keyCode: {
    get: function () {
      if (this.key && this.key.length === 1) {
        if (this.type === 'keypress') {
          return this.key.charCodeAt(0);
        }
        // 数字和大写字母的 keyCode 恰好对应 ASCII 值,英文标点的键值视浏览器而定
        return this.key.toUpperCase().charCodeAt(0);
      } else {
        console.error('暂不支持获取keyCode值!');
        return 0;
      }
    }
  },
  which: {
    get: function () {
      if (this.key && this.key.length === 1) {
        if (this.type === 'keypress') {
          return this.key.charCodeAt(0);
        }
        // 数字和大写字母的 which 恰好对应 ASCII 值,英文标点的键值视浏览器而定
        return this.key.toUpperCase().charCodeAt(0);
      } else {
        console.error('暂不支持获取which值!');
        return 0;
      }
    }
  }
});

console.log('key', keyboardEvent.key);
console.log('charCode', keyboardEvent.charCode);
console.log('keyCode', keyboardEvent.keyCode);
console.log('which', keyboardEvent.which);

其他

发送以及监听事件

事件的发送和监听对前端开发者来说再熟悉不过。

发送事件可以使用 dispatchEvent()(IE9 之前的浏览器可以使用 fireEvent()):

document.dispatchEvent(event);

事件监听使用 addEventListener()(IE9 之前的浏览器可以使用 attachEvent()):

document.addEventListener(type, listener[, useCapture])

取消事件监听 removeEventListener()(IE9 之前的浏览器可以使用 detachEvent()):

document.removeEventListener(type, listener[, useCapture]);

番外:如何区分用户动作创建的事件和通过 API 创建的事件

事件对象上存在一个 isTrusted 属性,返回一个布尔值,可以表明事件是由用户操作后产生(比如点击、拖拽)或是由脚本通过 API 创建。

参考链接