JavaScript异步编程笔记

1,771 阅读8分钟

异步事件的工作方式

事件!事件到底是怎么工作的?JavaScript出现了多久,对JavaScript异步事件模型就迷惘了多久。迷惘导致bug,bug导致加班,加班导致没时间撩妹子,这不是js攻城狮想要的生活。

==为了妹子,一定要理解好JavaScript事件==

JavaScript事件的运行

先来看一个烂大街的面试题

for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 200);
}
// 3 3 3

为什么输出的全都是3??

  1. 只有一个名为i的变量,其作用域由声明语句var i定义(var定义的i作用城不是循环内部,而是扩散至其所在的整个作用域)。
  2. 循环结束后,i++还在执行,直到i<3返回false为止。
  3. 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元素时,程序是如何工作的???

  1. 用户单击一个已附加有单击事件处理器的DOM元素时,会有一个单击事件排入队列。
  2. 该单击事件处理器要等到当前所有正在运行的代码均已结束后(可能还要等其他此前已排队的事件也依次结束)才会执行。

恩,用专业点的术语来说,就是事件循环,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,会自动重启挂掉的线程