异步事件的工作方式
事件! 事件到底是怎么工作的? JavaScript 出现了多久, 对 JavaScript 异步事件模型就迷惘了多久. 迷惘导致 bug,bug 导致加班, 加班导致没时间撩妹子, 这不是 JS 攻城狮想要的生活.
== 为了妹子, 一定要理解好 JavaScript 事件 ==
JavaScript 事件的运行
先来看一个烂大街的面试题
- for (var i = 0; i <3; i++) {
- setTimeout(function () {
- console.log(i);
- }, 200);
- }
- // 3 3 3
为什么输出的全都是 3??
只有一个名为 i 的变量, 其作用域由声明语句 var i 定义(var 定义的 i 作用城不是循环内部, 而是扩散至其所在的整个作用域).
循环结束后, i++ 还在执行, 直到 i<3 返回 false 为止.
JavaScript 事件处理器在线程空闲之前不会运行.
再来看一段代码
- var start = new Date();
- setTimeout(function () {
- console.log("回调触发间隔 1:", new Date() - start, "ms");
- }, 500);
- setTimeout(function () {
- console.log("回调触发间隔 2:", new Date() - start, "ms");
- }, 800);
- setTimeout(function () {
- console.log("回调触发间隔 3:", new Date() - start, "ms");
- }, 1100);
- while (new Date() - start < 1000) {
- }
回调触发间隔 1: 1002 ms
回调触发间隔 2: 1003 ms
回调触发间隔 3: 1101 ms
最终输出的毫秒数在不同环境下会有所不同, 但是最终数字肯定至少是 1000, 因为在 while 循环阻塞了线程(JavaScript 是单线程运行), 在循环结束运行之前, setTimeout 的处理器不会被触发.
为什么会这样??
调用 setTimeout 的时候, 会有一个延时事件排入队列, 然后 setTimeout 调用之后的代码运行, 然后之后之后的代码运行, 然后之后之后之后...
直到再也没有要运行的代码, 这个时候队列事件才会被记起.
如果队列事件中至少有一个事件适合被触发(如前面代码中的 500 和 800 毫秒的延时事件), 则 JS 线程会挑选一个事件, 并调用事件的处理器(回调函数).
执行完毕后, 回到事件队列中, 继续下一个...
也就是说: setTimeout 只能保证在指定的时间后将任务 (需要执行的函数) 插入任务队列中等候, 但是不保证这个任务在什么时候执行.
大家可以猜想下, 用户单击一个已附加有单击事件处理器的 DOM 元素时, 程序是如何工作的???
用户单击一个已附加有单击事件处理器的 DOM 元素时, 会有一个单击事件排入队列.
该单击事件处理器要等到当前所有正在运行的代码均已结束后 (可能还要等其他此前已排队的事件也依次结束) 才会执行.
恩, 用专业点的术语来说, 就是事件循环, JS 不断的从队列中循环取出处理器运行.
所以, setTimeout(fn,0)只是指定某个任务在主线程空闲时, 尽可能早得执行. 它在 "任务队列" 的尾部添加一个事件, 因此要等到同步任务和 "任务队列" 现有的事件都处理完, 才会得到执行.
异步函数的类型
JavaScript 提供的异步函数分为两类: I/O 函数, 计时函数
最为常见的异步 I/O 模型是 Ajax, 它是网络 IO 的一种, 在 Node.JS 中最为常见的是文件 IO.
最为常见的异步计时函数为 setTimeout 与 setInterval, 除了前面的示例, 这两个函数还存在一些无法弥补的精度问题.
看下如下两段代码:
- var fireCount = 0;
- var start = new Date();
- var timer = setInterval(function () {
- if (new Date() - start> 1000) {
- clearInterval(timer);
- console.log(fireCount);
- return;
- }
- fireCount++;
- },0);
- // node 环境输出: 860
- // Chrome 环境输出: 252
- var fireCount = 0;
- var start = new Date();
- var flag = true;
- while (flag) {
- if (new Date() - start> 1000) {
- console.log(fireCount);
- flag = false;
- }
- fireCount++;
- }
- // node 环境输出: 4355256
- // Chrome 环境输出: 4515852
为什么???
以下信息引用自网络
事实上 html5 标准规定 setTimeout 的最短时间间隔是 4 毫秒; setInterval 的最短间隔时间是 10 毫秒.
在此之前, 老版本的浏览器都将 setTimeout 最短间隔设为 10 毫秒. 另外, 对于那些 DOM 的变动 (尤其是涉及页面重新渲染的部分), 通常不会立即执行, 而是每 16.6 毫秒执行一次(大多数电脑显示器的刷新频率是 60Hz, 大概相当于每秒钟重绘 60 次, 不超过显示器的重绘频率, 因为即使超过那个频率用户体验也不会有提升). 这时使用 requestAnimationFrame() 的效果要好于 setTimeout().
Node.JS 提供了更细粒度的立即异步执行函数, process.nextTick,setImmediate
浏览器提供了一个新的函数 requestAnimationFrame 函数, 它允许以 60 + 帧 / 秒的速度运行 JavaScript 动画; 另一方面, 它也可以避免后台选项卡运行这些动画, 节约 CPU 周期. 详情
console.log 是异步吗? 在 Node.JS 中是严格的同步函数, 而在浏览器端, 则依赖具体浏览器的实现, 根据测试, 基本是同步!!
什么是异步函数: 函数会导致将来再运行另一个函数, 而另一个函数取自于事件队列(我们一般称为回调). 异步函数一般满足下面的模式.
- var functionHasReturned=false;
- asyncFunction(){
- console.log(functionHasReturned); // true
- }
- functionHasReturned=true;
异步的错误处理
JavaScript 中也有 try/catch/finally, 也存在 throw, 如果在一次异步操作中抛出错误, 会发生什么??
下面看两个《async JavaScript》书中的例子:
代码 1:
- function getObj(str){
- return JSON.parse(str);
- }
- var obj = getObj("{");
在 node 下运行, 输出的错误堆栈信息:
- undefined:1
- {
- SyntaxError: Unexpected end of JSON input
- at JSON.parse (<anonymous>)
- at getObj (/home/xingmu/ws/practice/myapp/test/test.JS:2:14)
- at Object.<anonymous> (/home/xingmu/ws/practice/myapp/test/test.JS:4:11)
- at Module._compile (module.JS:652:30)
- at Object.Module._extensions..JS (module.JS:663:10)
- at Module.load (module.JS:565:32)
- at tryModuleLoad (module.JS:505:12)
- at Function.Module._load (module.JS:497:3)
- at Function.Module.runMain (module.JS:693:10)
- at startup (bootstrap_node.JS:188:16)
代码 2:
- setTimeout(function a(){
- setTimeout(function b(){
- setTimeout(function c(){
- throw new Error("我犯错误了, 快来抓我!");
- },0);
- },0);
- },0);
输出:
- /home/xingmu/ws/practice/myapp/test/test.JS:4
- throw new Error("我犯错误了, 快来抓我!");
- ^
Error: 我犯错误了, 快来抓我!
- at Timeout.c [as _onTimeout] (/home/xingmu/ws/practice/myapp/test/test.JS:4:10)
- at ontimeout (timers.JS:482:11)
- at tryOnTimeout (timers.JS:317:5)
- at Timer.listOnTimeout (timers.JS:277:5)
为什么代码 2 输出的错误堆栈信息只有 c ?
因为在运行时, c 是从队列中取出来的, 而这个时候 a 和 b 还在队列中, 并不知道 c 运行出错了.
下面再看一段代码:
- try{
- setTimeout(function(){
- throw new Error("我犯错误了, 快来抓我!");
- },0);
- }catch(e){
- console.log(e);
- console.log("抓到你了!");
- }finally{
- console.log("我是终结者!");
- }
输出信息:
我是终结者!
- /home/xingmu/ws/practice/myapp/test/test.JS:3
- throw new Error("我犯错误了, 快来抓我!");
- ^
Error: 我犯错误了, 快来抓我!
- at Timeout._onTimeout (/home/xingmu/ws/practice/myapp/test/test.JS:3:9)
- at ontimeout (timers.JS:482:11)
- at tryOnTimeout (timers.JS:317:5)
- at Timer.listOnTimeout (timers.JS:277:5)
从这里可以看出, try/catch 块只会捕获 setTimeout 函数自身内部发生的错误, 而 setTimeout 的回调是异步运行的, 即使抛出错误, 也无法捕获.
所以说对异步执行的函数, 使用 try/catch 块并不能达到我们想要的效果, 那么对于异步回调的错误该怎么处理呢??
下面来看下, 在 Node.JS 的 API 中比较常见的错误处理模式:
- var fs = require("fs");
- fs.readFile("abc.text", function (err, data) {
- if (err) {
- console.log(err);
- return;
- }
- console.log(data.toString("utf8"));
- });
- // { Error: ENOENT: no such file or directory, open 'abc.text' errno: -2, code: 'ENOENT', syscall: 'open', path: 'abc.text' }
在 Node.JS 中, 类似这样的 API 非常多, 在回调函数中, 第一个参数总是接收一个错误, 这样就可以让回调函数自己决定怎么处理这个错误.
而在浏览器中, 我们最熟悉的回调错误处理模式是像 jQuery 中的 Ajax 一样, 针对成功和失败, 各定义一个单独的回调:
- $.Ajax({
- type:'POST',
- url:'/data',
- data: $('form').serialize(),
- success:function(response,status,xhr){
- //dosomething...
- },
- error:function (textStatus) {// 请求失败后调用的函数
- //dosomething...
- }
- });
不管是那个一个运行环境, 对于异步的错误处理有一点是一致的: 只能在回调的内部处理源于回调的错误.
未捕获异常的处理
是的, 总会有意想不到的错误发生, 这时候该怎么处理??
浏览器环境中, 我们经常可以在浏览器控制台看到很多未捕获的错误信息, 在开发环境这些信息可以帮助我们调试, 如果想修改这种行为, 可以给 Windows.error 添加一个处理器, 用来全局处理未捕获异常.
- Windows.onerror = function(error){
- // do something
- // 比如向服务器报告出现的未捕获异常
- // 比如给用户统一的消息处理
- // return true; 返回 true, 可以阻止浏览器的默认行为, 彻底忽略所有的错误
- }
看一段示例代码:
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <title>
- Title
- </title>
- </head>
- <body>
- <script>
- try {
- setTimeout(function() {
- throw new Error("我犯错误了, 快来抓我!");
- },
- 0);
- } catch(e) {
- console.log(e);
- console.log("抓到你了!");
- } finally {
- console.log("我是终结者!");
- }
- Windows.onerror = function(error) {
- alert("页面出错了");
- // do something other
- return true;
- };
- </script>
- </body>
- </HTML>
node 环境中, 有 domain 和 process.onuncaughtexception 两种方式来处理未捕获异常, 但是后端的处理比较复杂, JavaScript 作为一个单线程程序, 对于异常的处理更要慎重.
恩, 意思就是我也没有最好的方案...
当然很多工具也可以帮我们简化处理, 比如 pm2, 会自动重启挂掉的线程
来源: https://juejin.im/post/5bc49dc26fb9a05ce37b222d