异步事件的工作方式
事件!事件到底是怎么工作的?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的一种,在nodejs中最为常见的是文件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()。
nodejs提供了更细粒度的立即异步执行函数,process.nextTick,setImmediate
浏览器提供了一个新的函数requestAnimationFrame函数,它允许以60+帧/秒的速度运行JavaScript动画;另一方面,它也可以避免后台选项卡运行这些动画,节约CPU周期。详情
console.log是异步吗? 在nodejs中是严格的同步函数,而在浏览器端,则依赖具体浏览器的实现,根据测试,基本是同步!!
什么是异步函数:函数会导致将来再运行另一个函数,而另一个函数取自于事件队列(我们一般称为回调)。异步函数一般满足下面的模式。
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块并不能达到我们想要的效果, 那么对于异步回调的错误该怎么处理呢??
下面来看下,在nodejs的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' }
复制代码
在nodejs中,类似这样的API非常多,在回调函数中,第一个参数总是接收一个错误,这样就可以让回调函数自己决定怎么处理这个错误。
而在浏览器中,我们最熟悉的回调错误处理模式是像jquery中的ajax一样,针对成功和失败,各定义一个单独的回调:
$.ajax({
type:'POST',
url:'/data',
data: $('form').serialize(),
success:function(response,status,xhr){
//dosomething...
},
error:function (textStatus) {//请求失败后调用的函数
//dosomething...
}
});
复制代码
不管是那个一个运行环境,对于异步的错误处理有一点是一致的: 只能在回调的内部处理源于回调的错误。
未捕获异常的处理
是的,总会有意想不到的错误发生,这时候该怎么处理??
- 浏览器环境中,我们经常可以在浏览器控制台看到很多未捕获的错误信息,在开发环境这些信息可以帮助我们调试,如果想修改这种行为,可以给window.error添加一个处理器,用来全局处理未捕获异常。
window.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("我是终结者!");
}
window.onerror = function (error) {
alert("页面出错了");
// do something other
return true;
};
</script>
</body>
</html>
复制代码
- node环境中,有domain和process.onuncaughtexception两种方式来处理未捕获异常,但是后端的处理比较复杂,javascript作为一个单线程程序,对于异常的处理更要慎重。
恩,意思就是我也没有最好的方案。。。
当然很多工具也可以帮我们简化处理,比如pm2,会自动重启挂掉的线程