禅意coder 禅意coder
首页
  • 跟我一起学NodeJs
  • JavaScript 异步编程
  • kafka 相关知识
  • 分类
  • 标签
  • 归档
关于
GitHub
首页
  • 跟我一起学NodeJs
  • JavaScript 异步编程
  • kafka 相关知识
  • 分类
  • 标签
  • 归档
关于
GitHub
  • 一起学nodejs

  • javascript 异步编程

    • javascript 异步编程-1.深入理解javascript事件
      • 事件的调度
        • 最常见的一个例子
        • 关于setTimeout的理解
      • 异步函数的类型
        • 异步的IO函数
        • 异步的计时函数
      • 异步函数的编写
        • 间或异步函数
        • 缓存型异步函数
        • 异步递归和回调存储
        • 返回值与回调的混搭
      • 异步错误处理
        • 未捕获的异常处理
        • 抛出还是不抛出
        • 嵌套式回调的解嵌套
    • javascript 异步编程-3.Promise对象和Deferred对象
    • javascript 异步编程-4.async.js工作流控制
    • javascript 异步编程-5.异步脚本加载
  • kafka知识

  • linux的奥义

  • java语言

  • 系列文章
  • javascript 异步编程
liushaoqing
2017-12-01

javascript 异步编程-1.深入理解javascript事件

# 深入理解javascript事件

# 事件的调度

# 最常见的一个例子

for (var i = 1; i <= 3; i++) {
 setTimeout(function(){
     console.log(i);
 }, 0);
};
// 4 4 4
1
2
3
4
5
6

循环结束后JavaScript的事件处理器才开始执行

# 关于setTimeout的理解

var start = new Date;
setTimeout (function(){
    var end = new Date();
    console.log('Time elapsed:', end - start,'ms');
}, 500);
while (new Date - start; 1000) {};
// Time elapsed:
// 1010 ms
// Time elapsed:
// 1015ms
1
2
3
4
5
6
7
8
9
10
  1. 按照多线程的思维定势,计500毫秒后计时函数就会运行。不过这要求中断欲持续整整一秒钟的循环。每次的结果都会稍有不同,但是总是大于1000ms.因为setTimeout回调在while循环结束运行之前不可能被触发.

  2. 调用setTimeout的时候,会有一个延时事件排入队列。然后setTimeout调用之后的那行代码运行,接着是再下一行代码,直到再也没有任何代码。这时JavaScript虚拟机才会问:“队列里都有谁啊?”

  3. 事件的易调度性是JavaScript语言最大的特色之一。像set-Timeout这样的异步函数只是简单地做延迟执行,而不是孵化新的线程。JavaScript代码永远不会被中断,这是因为代码在运行期间只需要排队事件即可,而这些事件在代码运行结束之前不会被触发。

# 异步函数的类型

JavaScript环境提供的异步函数通常可以分为两大类:I/O函数和计时函数。 如果想在应用中定义复杂的异步行为,就要使用这两类异步函数作为基本的构造块。

# 异步的IO函数

  1. 创造Node.js,并不是为了人们能在服务器上运行JavaScript,仅仅是因为Ryan Dahl想要一个建立在某高级语言之上的事件驱动型服务器框架。JavaScript碰巧就是适合干这个的语言。为什么?因为JavaScript语言可以完美地实现非阻塞式I/O。

  2. 在浏览器端,Ajax方法有一个可设置为false的async选项(但永远、永远别这么做),这会挂起整个浏览器窗格直到收到应答为止。在Node.js中,同步的API方法在名称上会有明确的标示,譬如fs.readFileSync。编写短小的脚本时,这些同步方法会很方便。但是,如果所编写的应用需要处理并行的多个请求或多项操作,则应该避免使用它们。

  3. 有些I/O函数既有同步效应,也有异步效应。举例来说,在现代浏览器中操纵DOM对象时,从脚本角度看,更改是即时生效的,但从视效角度看,在返回事件队列之前不会渲染这些DOM对象更改。jsfiddle示例 。

console.log是异步的吗 ?

var obj = {};
console.log(obj);
obj.foo = 'bar';
1
2
3

怎么会这样 ?

  1. WebKit的console.log并没有立即拍摄对象快照,相反,它只存储了一个指向对象的引用,然后在代码返回事件队列时才去拍摄快照。
  2. Node的console.log是另一回事,它是严格同步的,因此同样的代码输出的却为{}。

# 异步的计时函数

# 浏览器端

var fireCount = 0;
var start = new Date;
var timer = setInterval(function() {
    if  (new Date-start > 1000) {
           clearInterval(timer);
           console.log(fireCount);
            return;
        }
        fireCount++;
    },
0);
// 246
// 251
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 大约为200次/秒。这是Chrome、Safari和Firefox等浏览器的平均值。

  • 在Node环境下,此事件的触发频率大约能达到600次/秒。(若使用setTimeout来调度事件,重复这些实验也会得到类似的结果。)作为对比,

  • 如果将setInterval替换成简单的while循环,则在Chrome中此事件的触发频率将达到400万次/秒,而在Node中会达到500万次/秒!

  • 这是怎么回事?最后我们发现,setTimeout和setInterval就是想设计成慢吞吞的!事实上,HTML规范(这是所有主要浏览器都遵守的规范)推行的延时/时隔的最小值就是4毫秒.

  • 在Node中,process.nextTick 允许将事件调度成尽可能快地触发。对于笔者的系统,process.nextTick事件的触发频率可以超过10万次/秒。

  • 一些现代浏览器(含IE9+)带有一个requestAnimation-Frame函数。此函数有两个目标:一方面,它允许以60+帧/秒的速度运行JavaScript动画;另一方面,它又避免后台选项卡运行这些动画,从而节约CPU周期。在最新版的Chrome浏览器中,甚至能实现亚毫秒级的精度。

  • setTimeout和setInterval就是些不精确的计时工具。在Node中,如果只是想产生一个短时延迟,请使用pro-cess.nextTick。在浏览器端,请尝试使用垫片技术(shim):在支持requestAnimationFrame的浏览器中,推荐使用reques-tAnimationFrame;在不支持requestAnimationFrame的浏览器中,则退而使用setTimeout。

# 异步函数的编写

JavaScript中的每个异步函数都构建在其他某个或某些异步函数之上。凡是异步函数,从上到下(一直到原生代码)都是异步的!

遗憾的是,要想确认某个函数异步与否,唯一的方法就是审查其源代码。有些同步函数却拥有看起来像是异步的API,这或者是因为它们将来可能会变成异步的,又或者是因为回调这种形式能方便地返回多个参数。

# 间或异步函数

有些函数某些时候是异步的,但其他时候却不然。举个例子,jQuery的同名函数(通常记作$)可用于延迟函数直至DOM已经结束加载。但是,若DOM早已结束了加载,则不存在任何延迟,$的回调将会立即触发。

$(function() {  
    utils.log('Ready');
});
//utils.js
window.utils = {  
    log: function() { 
       if (window.console) 
       console.log.apply(console, arguments);
    }
};
<script src ="application.js"></script>
<script src ="util.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12

这段代码运行得很好,但前提是浏览器并未从缓存中加载页面(这会导致DOM早在脚本运行之前就已加载就绪)。如果出现这种情况,传递给$的回调就会在设置utils.log之前运行,从而导致一个错误。

# 缓存型异步函数

# 异步递归和回调存储

请避免异步递归。仅当所采用的库提供了异步功能但没有提供任何形式的回调机制时,异步递归才有必要。如果真的遇到这种情况,要做的第一件事应该是为该库写一个补丁。或者,干脆找一个更好的库。

# 返回值与回调的混搭

# 异步错误处理

如果从异步回调中抛出错误,会发生什么事?

setTimeout(function A() { 
     setTimeout(function B() { 
        setTimeout(function C() {
           throw new Error('Something terrible has happened!');
        },0); 
    }, 0);
}, 0);
//Error: Something terrible has happened!
//    at C (script 1:94)
1
2
3
4
5
6
7
8
9

上述应用的结果是一条极其简短的堆栈轨迹。,A和B发生了什么事?为什么它们没有出现在堆栈轨迹中?这是因为运行C的时候,A和B并不在内存堆栈里。这3个函数都是从事件队列直接运行的。基于同样的理由,利用try/catch语句块并不能捕获从异步回调中抛出的错误。

总的来说,取用异步回调的函数即使包装上try/catch语句块,也只是无用之举。(特例是,该异步函数确实是在同步地做某些事且容易出错。例如,Node的fs.watch(file,callback)就是这样一个函数,它在目标文件不存在时会抛出一个错误。)正因为此,Node.js中的回调几乎总是接受一个错误作为其首个参数,这样就允许回调自己来决定如何处理这个错误。举个例子,下面这个Node应用尝试异步地读取一个文件,还负责记录下任何错误(如“文件不存在”)。

var fs = require('fs');
fs.readFile('fhgwgdz.txt', function(err, data) {
  if (err) {
  return console.error(err);
  };
  console.log(data.toString('utf8'));
}
1
2
3
4
5
6
7

客户端的例子

$.get('/data',
 { success: successHandler, 
 failure: failureHandler}
);`
1
2
3
4

不管API形态像什么,始终要记住的是,只能在回调内部处理源于回调的异步错误

# 未捕获的异常处理

# 浏览器环境

现代浏览器会在开发人员控制台显示那些未捕获的异常,接着返回事件队列。要想修改这种行为,可以给window.onerror附加一个处理器。如果windows.onerror处理器返回true,则能阻止浏览器的默认错误处理行为。

window.onerror = function(err) {  return true;  //彻底忽略所有错误};
1

在成品应用中,会考虑某种JavaScript错误处理服务,譬如:Errorception 。Errorception提供了一个现成的windows.on-error处理器,它向应用服务器报告所有未捕获的异常,接着应用服务器发送消息通知我们。

# NodeJs 环境

在Node环境中,window.onerror的类似物就是process对象的uncaughtException事件。正常情况下,Node应用会因未捕获的异常而立即退出。但只要至少还有一个uncaughtExcep-tion事件处理器,Node应用就会直接返回事件队列。

process.on('uncaughtException', function(err) {  console.error(err);  });//避免了进程停止
1

但是,自Node 0.8.4起,uncaughtException事件就被废弃了。据其文档所言,

对异常处理而言,uncaughtException是一种非常粗暴的机制,它在将来可能会被放弃……请勿使用uncaughtException,而应使用Domain对象。

var myDomain = require('domain').create();
myDomain.run(function () {
    setTimeout(function () {
        throw new Error('Listen to me!')
    }, 50);
});
myDomain.on('error', function (err) {
    console.log('Error ignored!');
});
1
2
3
4
5
6
7
8
9

源于延时事件的throw只是简单地触发了Domain对象的错误处理器。

# 抛出还是不抛出

如果抛出那些自己知道肯定会被捕获的异常呢?这种做法同样凶险万分。2011年,Isaac Schlueter(npm的开发者,在任的Node开发负责人)就主张try/catch是一种“反模式”的方式。 Schlueter提倡完全将throw用作断言似的构造结构,作为一种挂起应用的方式——当应用在做完全没预料到的事时,即挂起应用。Node社区主要遵循这一建议,尽管这种情况可能会随着Domain对象的出现而改变。

# 嵌套式回调的解嵌套

function checkPassword(username, passwordGuess, callback) {
    var queryStr = 'SELECT * FROM user WHERE username = ?';
    db.query(queryStr, username, function (err, result) {
        if (err) throw err;
        hash(passwordGuess, function (passwordGuessHash) {
            callback(passwordGuessHash === result['password_hash']);
        });
    });
}
1
2
3
4
5
6
7
8
9

这段代码有什么问题呢?目前为止,没有任何问题。它能用,而且简洁明了。但是,如果试图向其添加新特性,它就会变得毛里毛躁、险象环生,比如去处理那个数据库错误,而不是抛出错误(请参阅1.4.3节)、记录尝试访问数据库的次数、阻塞访问数据库,等等。

function checkPassword(username, passwordGuess, callback) {
    var passwordHash;
    var queryStr = 'SELECT * FROM user WHERE username = ?';
    db.query(qyeryStr, username, queryCallback);

    function queryCallback(err, result) {
        if (err) throw err;
        passwordHash = result['password_hash'];
        hash(passwordGuess, hashCallback);
    }

    function hashCallback(passwordGuessHash) {
        callback(passwordHash === passwordGuessHash);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这种写法更啰嗦一些,但读起来更清晰,也更容易扩展。由于这里赋予了异步结果(即passwordHash)更宽广的作用域,所以获得了更大的灵活性。 按照惯例,请避免两层以上的函数嵌套。关键是找到一种在激活异步调用之函数的外部存储异步结果的方式,这样回调本身就没有必要再嵌套了。

编辑
上次更新: 2021/01/23, 09:10:58
nodejs_connect
javascript 异步编程-3.Promise对象和Deferred对象

← nodejs_connect javascript 异步编程-3.Promise对象和Deferred对象→

最近更新
01
轮子哥编程的感悟(转载)
01-23
02
tmux 配置和使用
01-23
03
命令行日常使用配置
01-15
更多文章>
Theme by Vdoing | Copyright © 2020-2021 刘少卿 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×