JavaScript异步编程笔记

栏目: JavaScript · 发布时间: 6年前

内容简介:事件!事件到底是怎么工作的?JavaScript出现了多久,对JavaScript异步事件模型就迷惘了多久。迷惘导致bug,bug导致加班,加班导致没时间撩妹子,这不是js攻城狮想要的生活。==为了妹子,一定要理解好JavaScript事件==先来看一个烂大街的面试题

事件!事件到底是怎么工作的?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,会自动重启挂掉的线程


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Master Switch

The Master Switch

Tim Wu / Knopf / 2010-11-2 / USD 27.95

In this age of an open Internet, it is easy to forget that every American information industry, beginning with the telephone, has eventually been taken captive by some ruthless monopoly or cartel. Wit......一起来看看 《The Master Switch》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换