Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 常用的一些事件总结 #66

Open
6 of 8 tasks
tcatche opened this issue Sep 18, 2017 · 0 comments
Open
6 of 8 tasks

JavaScript 常用的一些事件总结 #66

tcatche opened this issue Sep 18, 2017 · 0 comments

Comments

@tcatche
Copy link
Owner

tcatche commented Sep 18, 2017

本文博客地址:JavaScript 常用的一些事件总结.

本文主要总结了在开发中经常使用的的一些浏览器事件的使用。

系列目录

本文涉及内容加多,截止现在,“拖拽事件”、“触摸事件”、“手势事件” 三节尚未完成。

键盘事件

键盘事件最常用的是 keyupkeydown 两个事件,这两个事件本身很简单,不再多说,本节说一点键盘事件其他细节吧。

这两个事件常配合表单使用,比如用户输入内容后校验,虽然也可以用,但是处理起来较为麻烦,需要判断组合键,非文本输入按键比如 Shift、F1 之类的等,更合适的方法是使用表单的 input 事件,将在后面表单事件说明

组合键

和鼠标事件类似,按键事件也具有这几个属性:altKeyctrlKey/metaKeymetaKeyctrlKeyshiftKey`,具体可以参考上一节,不再详细说明。

e.code 和 e.key

这两个属性表示了按键的内容,是两个比较接近的内容,但是也有一点点不同。

code 表示按的是哪个键,而 key 则表示按键的键值,这么说比较模糊,先看个例子:

document.onkeydown = function (e) {
  console.log('The code is: ', e.code);
  console.log('The key is: ', e.key);
  console.log('---------')
}

// 按下键盘左边shift
The code is:  ShiftLeft
The key is:  Shift
---------

// 按下键盘右边shift
---------
The code is:  ShiftRight
The key is:  Shift

由上述例子可以看出,code 是表示的具体的哪个按键被按下,所以会区分左 Shift 和右 Shift,而 key 则反映了按键代表的含义,再看另一个例子:

// 按下键盘 A
The code is:  KeyA
The key is:  a
---------

// 按下键盘组合键 shift + a
The code is:  ShiftLeft
The key is:  Shift
---------
The code is:  KeyA
The key is:  A

忽略组合键多出来的一组按键,可以看出,此时,多了 Shift, code 值不变,是小写 a,而 key 值变成了大写的 A

字母键的 key 值为 "Key" + <letter>,数字的 key 值为 "Digit" + <number> ,小键盘和手机按键的 key 值通常为 "Numpad" + <number>,其他控制键则为其名字,更具体查看Specs - Key Codes for Standard Keyboards,这里不在一一说明。

e.keyCode

使用 keyCode 判断按键是很常用的代码,比如判断用户输入了回车键,则直接执行操作。但是须知,这个属性已经被废弃,虽然在很多浏览器还有支持,但是不建议再使用,而是使用 key 或者 code 替代:

// 不建议使用 keyCode
document.onkeydown = function (e) {
  if (!e) e = window.event;
  if ((e.keyCode || e.which) == 13) {
    alert("enter");
  }
}

// 建议使用 key/code 替代
document.onkeydown = function (e) {
  if (!e) e = window.event;
  if ((e.key) == "Enter") {
    alert("enter");
  }
}

默认行为

一些功能键有一些默认的行为,比如:Delete 会删除表单域的一个字符,PageDown 会向下滚动,Ctrl + S 可以打开浏览器保存网页的对话框,这些行为都是可以取消的:

document.onkeydown = function (e) {
  if (e.code === 'KeyS' && e.ctrlKey){
    e.preventDefault();
    console.log('cancel save page');
  }
  if (e.code === 'PageDown'){
    e.preventDefault();
    console.log('cancel page down');
  }
}

比如上面例子取消了保存网页和翻页的默认操作,但是有些操作是无法取消的,比如操作系统的 Alt + F4

页面生命周期事件

和资源生命周期有关的事件主要有:loadDOMContentLoadedabortbeforeunloadunload

load

load:目标对象为 Window, DocumentElement,当一个资源和其依赖的所有资源加载完成的时候,会触发 load 事件。这个资源可以是图片、CSS 文件、JS 文件、视频、documentwindow 等等。

// 页面内所有的资源加载完成后执行
window.addEventListener("load", function(event) {
  console.log("All resources finished loading!");
});

// 图片加载完成则执行
document.getElementById('image').addEventListener("load", function(event) {
  console.log("Image resource finished loading!");
});

DOMContentLoaded

DOMContentLoaded:目标对象为 Document,当初始 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架完成加载。所以如果想要你的网页加载完成立即执行某些脚本,使用这个事件要优于 load 事件的,因为 load 事件要等待所有内容加载完成,这样无疑是很慢的。

window.onload = function () {
  console.log('onload 后输出');
}
document.addEventListener("DOMContentLoaded", function(event) {
  console.log('DOMContentLoaded 先输出');
});

// "DOMContentLoaded 先输出"
// "onload 后输出"

注意:本函数只能绑定在 Document 元素上,而 load 可以绑定到任何元素上,监听该元素的资源加载完成事件。更多关于初始化资源加载情况的监听探讨可以查看本人的另一篇文章(关于文档加载状态相关的事件探讨

abort

abort:目标对象为 WindowElement,当资源的加载被中止时,此事件被触发。

beforeunload

beforeunload:目标对象为 Window, 当窗口,文档及其资源即将被卸载时,beforeunload 事件触发。如果此事件的监听器处理函数为 Event 对象的 returnValue 属性赋值非空字符串,或者函数返回非空字符串,浏览器会弹出一个对话框,来询问用户是否确定要离开当前页面。否则,该事件不做响应。

window.addEventListener("beforeunload", function (e) {
  var confirmationMessage = "Are you sure to close?";

  (e || window.event).returnValue = confirmationMessage; //Gecko + IE
  return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});

注意:在 chrome 51 后,不再支持用户自定义网页提示的消息文字,chrome 认为这个经常被用来欺骗用户(详情查看Remove custom messages in onbeforeunload dialogs)。而且 Safari 9.1+,Firefox 4+ 都不支持这个功能。

注意:在实践中调用 window.alert(), window.confirm(), window.prompt() 会被忽略,同时该对话框只能起到提示、阻止关闭作用,如果用户选择关闭,并无法在js中获得回调值,所以,如果页面有未保存动作,可以在这个时候进行一些保存动作。

unload

unload:目标对象为 Window, DocumentElement,当DOM实现从环境中删除资源(如文档)或任何依赖资源(如图像,样式表,脚本)时,会触发此事件。

如果事件被绑定在 document 或者 window 对象上的时候,当此事件触发,此时文档会处于以下状态:

  • 所有资源仍存在 (图片, iframe 等.)
  • 对于终端用户所有资源均不可见
  • 界面交互无效 (window.open, window.alert, window.confirm 等.)
  • 错误不会停止卸载文档的过程

请注意 unload 事件也遵循文档树:父 iframe 会在子 iframe 卸载前卸载:

<!-- Parent.html-->
<!DOCTYPE html>
<html>
  <head>
    <title>Parent Frame</title>
    <script>
      window.addEventListener('beforeunload', function(event) {
        console.log('I am the 1st one.');
      });
      window.addEventListener('unload', function(event) {
        console.log('I am the 3rd one.');
      });
    </script>
  </head>
  <body>
    <iframe src="child-frame.html"></iframe>
  </body>
</html>

<!-- child-frame.html-->
<!DOCTYPE html>
<html>
  <head>
    <title>Child Frame</title>
    <script>
      window.addEventListener('beforeunload', function(event) {
        console.log('I am the 2nd one.');
      });
      window.addEventListener('unload', function(event) {
        console.log('I am the 4th and last one…');
      });
    </script>
  </head>
  <body>
  </body>
</html>

更具体的关于删除文档的细节查看:Unloading Documents — Prompt to unload a document

拖拽事件

拖拽事件也是鼠标事件的变形,这里单独列出来解释。

// todo

触摸事件

// todo

手势事件

// todo

错误事件

当错误发生时,触发错误事件。错误事件有两种:

  • 当一项资源(如 <img><script> )加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的 onerror() 处理函数。这些 error 事件不会向上冒泡到 window,不过能被单一的 window.addEventListener 捕获。
  • 当 JavaScript 运行时错误(包括语法错误)发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()

第一种应用场合很少,这里主要介绍一下利用第二种的事件来监控和报告发生在网页的错误。

window.onerror

window.onerror = function(message, source, lineNumber, columnNumber, errorObj) { ... }

函数参数:

  • message:字符串,错误信息。
  • source:字符串,发生错误的脚本URL
  • lineNumber:数字, 发生错误的行号
  • columnNumber:数字,发生错误的列号
  • errorObj:对象,Error 对象

若该函数返回true,则阻止执行默认事件处理函数。

前四个参数告诉您哪个脚本,行和列出现错误,实际上,最后一个参数 Error 对象也许是最有价值的。

Error 对象和 error.stack

new Error([message[, fileName[, lineNumber]]])

Error 对象的构造函数包含3个属性:messagefileNamelineNumber。实际上这三个属性已经包含在了 window.onerror 提供给你的参数中。

Error 对象最有价值的属性是一个非标准属性:Error.prototype.stack。此堆栈属性会告诉你发生错误时程序的每个帧的源位置。错误堆栈跟踪是调试的关键部分,尽管这个属性是非标准的,但是每个现代浏览器都提供此属性。

这是一个堆栈追踪的示例:

Error: foobar
    at new bar (<anonymous>:241:11)
    at foo (<anonymous>:245:5)
    at callFunction (<anonymous>:229:33)
    at Object.InjectedScript._evaluateOn (<anonymous>:875:140)
    at Object.InjectedScript._evaluateAndWrap (<anonymous>:808:34)

使用 try...catch 的 Polyfill

并不是所有浏览器都支持 errorObj 这个参数,如果为了获得 errorObj 对象,可以使用 try...catch 手动捕获。

function invoke(obj, method, args) {
  try {
    return obj[method].apply(this, args);
  } catch (e) {
    captureError(e); // report the error
    throw e; // re-throw the error
  }
}

invoke(Math, 'highest', [1, 2]); // throws error, no method Math.highest

当然,调用的每个函数都使用 invoke 手动包装无疑十分愚蠢。js 是单线程的,所以我们可以将 try...catch 代码包裹程序的入口,这样就不需要到处手动包装。

这意味着你需要包装以下函数声明:

  • 在你的应用程序开始(例如 jquery 的 $(document).ready,document 的 DOMContentLoaded 事件)
  • 在事件处理程序中,例如 addEventListener$.fn.click
  • 基于定时器的回调,例如 setTimeoutrequestAnimationFrame
function wrapErrors(fn) {
  // don't wrap function more than once
  if (!fn.__wrapped__) {
    fn.__wrapped__ = function () {
      try {
        return fn.apply(this, arguments);
      } catch (e) {
        captureError(e); // report the error
        throw e; // re-throw the error
      }
    };
  }

  return fn.__wrapped__;
}

$(wrapErrors(function () { // application start
  doSynchronousStuff1(); // doesn't need to be wrapped now.

  setTimeout(wrapErrors(function () {
    doSynchronousStuff2(); // doesn't need to be wrapped now.
  });

  $('.foo').click(wrapErrors(function () {
    doSynchronousStuff3(); // doesn't need to be wrapped now.
  });
}));

Script error 安全限制

当加载自不同域的脚本中发生错误时,为避免信息泄露,语法错误的细节将不会报告,而代之简单的Script error.。这种情况通常发生在使用了 CDN,或者来自其他机构提供的服务。

这个问题有两种方法解决:

  • 让非本域网络资源设置 Access-Control-Allow-Origin 标头,这将允许跨域资源共享(CORS)。
  • source 标签上使用新的 crossorigin 属性:<script crossorigin="anonymous" src="script.js"></script>

crossorigin 标签有两个可能的值:

  • anonymous:匿名,意味着无需用户凭据来访问该文件。
  • use-credentials:如果从引用的外部 JavaScript 文件使用了 Access-Control-Allow-Credentials 头,则使用凭据。

其他事件

cut、 copy、 paste

这些事件在剪切、复制、粘贴的时候触发,可以被取消。

比如下面这个例子,我们想要用户认真填写自己的Id,禁止用户粘贴:

<input type="text" id="id" />

<script>
document.getElementById('id').onpaste = function(e) {
  e.preventDefault();
  // 删除剪切板中的内容
  deleteCurrentDocumentSelection();
  alert("Not allow to copy and paste");
}
</script>

还有一个常见的例子是当用户复制的时候追加额外的文本,比如追加文章的版权说明:

document.addEventListener('copy', function(e) {
  // 禁止默认行为
  e.preventDefault();
  // 获取用户选中的内容,此时不包含标签。
  const selectedText = window.getSelection().toString();
  const apptendText = '\n\n 著作权归作者所有,未经允许,禁止转载'
  // 注意,此处第一个参数是 "text/plain"
  event.clipboardData.setData("text/plain", selectedText + apptendText);
  // 删除剪切板中的内容
  deleteCurrentDocumentSelection();
});

此时选中,复制任何文本,都会在后面追加著作权的信息。

这里的方案仅仅修改文本,如果想要将标签也复制进去比较麻烦:

// 选中选择的文本所在的 html
function getSelected() {
  const selectionRange = window.getSelection().getRangeAt(0);
  const selectionContents = selectionRange.cloneContents();
  const fragmentContainer = document.createElement('div');
  const fragmentContainer.appendChild(selectionContents);
  return fragmentContainer;
}

document.addEventListener('copy', function(e) {
  // 禁止默认行为
  e.preventDefault();
  // 获取用户选中的内容,此时不包含标签。
  const selectedContainer = getSelectedHtml();
  const appendHtml = '<br/><br/> 著作权归作者所有,未经允许,禁止转载';
  selectedContainer.append(appendHtml);
  // 注意,此处第一个参数是 "text/html"
  event.clipboardData.setData("text/html", selectedContainer.innerHtml);
  // 删除剪切板中的内容
  deleteCurrentDocumentSelection();
});

剪切板是系统级别的,浏览器可能会出于安全的策略限制了对系统剪切板的访问权限。

剪切板事件并不常用,本文只做了简单的介绍,想要了解更多细节,查看

resize 和 scroll

这两个事件很简单,分别是监听文档视图尺寸和页面滚动的事件,本身没有什么要说的,这里主要说的是这两个事件的触发频率比较高,建议使用 requestAnimationFramesetTimeoutcustomEvent 来调节事件的触发频率:

使用 requestAnimationFramecustomEvent 优化高频率触发的事件:

var throttle = function(type, name, obj) {
  obj = obj || window;
  var running = false;
  var func = function() {
    if (running) { return; }
    running = true;
    requestAnimationFrame(function() {
      obj.dispatchEvent(new CustomEvent(name));
      running = false;
    });
  };
  obj.addEventListener(type, func);
};

/* init - you can init any event */
throttle("resize", "optimizedResize");
/* init - you can init any event */
throttle("scroll", "optimizedScroll");

// handle event
window.addEventListener("optimizedResize", function() {
  // resize to do sth;
});

// handle event
window.addEventListener("optimizedScroll", function() {
  // scroll to do sth;
});

参考

@tcatche tcatche changed the title 回流(Reflow)和重绘(Repaint) JavaScript 常用的一些事件总结 Sep 18, 2017
@tcatche tcatche closed this as completed Sep 19, 2017
@tcatche tcatche reopened this Sep 19, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant