js异步和性能

来源:互联网 发布:数组foreach c# 编辑:程序博客网 时间:2024/06/06 03:16

事实上,程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。

可以把 JavaScript 程序写在单个 .js 文件中,但是这个程序几乎一定是由多个块构成的。这 些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数。

从现在到将来的“等待”,最简单的方法(但绝对不是唯一的,甚至也不是最好的!)是 使用一个通常称为回调函数的函数:

// ajax(..)是某个库中提供的某个Ajax函数 ajax( "http://some.url.1", function myCallbackFunction(data){      console.log( data ); // 耶!这里得到了一些数据!  } );

function now() {     return 21; }  function later() {     answer = answer * 2;     console.log( "Meaning of life:", answer ); }  var answer = now();  setTimeout( later, 1000 ); // Meaning of life: 42

这个程序有两个块:现在执行的部分,以及将来执行的部分。这两块的内容很明显,但这 里我们还是要明确指出来。
现在:
function now() {     return 21; } 
 
function later() { .. } 
 
var answer = now(); 
 
setTimeout( later, 1000 );
将来:
answer = answer * 2; console.log( "Meaning of life:", answer );
现在这一块在程序运行之后就会立即执行。但是,setTimeout(..) 还设置了一个事件(定 时)在将来执行,所以函数 later() 的内容会在之后的某个时间(从现在起 1000 毫秒之 后)执行。
任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点 击、Ajax 响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序 中引入了异步机制。


异步控制台

到底什么时候控制台 I/O 会延迟,甚至是否能够被观察到,这都是游移不定的。如果在调 试的过程中遇到对象在 console.log(..) 语句之后被修改,可你却看到了意料之外的结果, 要意识到这可能是这种 I/O 的异步化造成的。
如果遇到这种少见的情况,最好的选择是在JavaScript 调试器中使用断点, 而不要依赖控制台输出。次优的方案是把对象序列化到一个字符串中,以强 制执行一次“快照”,比如通过 JSON.stringify(..)。

事件循环

所以,举例来说,如果你的 JavaScript 程序发出一个 Ajax 请求,从服务器获取一些数据, 那你就在一个函数(通常称为回调函数)中设置好响应代码,然后 JavaScript 引擎会通知 宿主环境:“嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个 函数。”
然后浏览器就会设置侦听来自网络的响应,拿到要给你的数据之后,就会把回调函数插入 到事件循环,以此实现对这个回调的调度执行。

并行线程
术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。记住,异步是 关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。


完整运行
由于 JavaScript 的单线程特性,foo()(以及 bar())中的代码具有原子性。也就是说,一旦 foo() 开始运行,它的所有代码都会在 bar() 中的任意代码运行之前完成,或者相反。 这称为完整运行(run-to-completion)特性。

在 JavaScript 的特性中,这种函数顺序的不确定性就是通常所说的竞态条件(race condition), foo() 和 bar() 相互竞争,看谁先运行。具体来说,因为无法可靠预测 a 和 b 的最终结果,所以才是竞态条件。p147

并发p148

onscroll, 请求1       <--- 进程1启动  onscroll, 请求2  响应1                 <--- 进程2启动 onscroll, 请求3  响应2  响应3  onscroll, 请求4  onscroll, 请求5  onscroll, 请求6  响应4  onscroll, 请求7       <--- 进程1结束 响应6  响应5  响应7                 <--- 进程2结束
“进程”1 和“进程”2 并发运行(任务级并行),但是它们的各个事件是在事件循环队列 中依次运行的。
另外,注意到响应 6 和响应 5 的返回是乱序的了吗?
单线程事件循环是并发的一种形式(当然还有其他形式,后面会介绍)


非交互 

var res = {};   function foo(results) {      res.foo = results;  }   function bar(results) {      res.bar = results;  }   ajax( "http://some.url.1", foo );  ajax( "http://some.url.2", bar )

交互 

var res = [];   function response(data) {      res.push( data );  }   // ajax(..)是某个库中提供的某个Ajax函数 ajax( "http://some.url.1", response ); 
 ajax( "http://some.url.2", response ); 

所以,可以协调交互顺序来处理这样的竞态条件:
var res = [];  
 
function response(data) {      if (data.url == "http://some.url.1") {          res[0] = data;      }      else if (data.url == "http://some.url.2") {          res[1] = data;      }  }  
 
// ajax(..)是某个库中提供的某个Ajax函数 ajax( "http://some.url.1", response );  ajax( "http://some.url.2", response ); 

var a, b;   function foo(x) {      a = x * 2;      if (a && b) {          baz();      }  }   function bar(y) {      b = y * 2;      if (a && b) {          baz();      }  }   function baz() {      console.log( a + b );  }   // ajax(..)是某个库中的某个Ajax函数 ajax( "http://some.url.1", foo );  ajax( "http://some.url.2", bar );
var a;   function foo(x) {      if (!a) {          a = x * 2;          baz();      }  }   function bar(x) {      if (!a) {          a = x / 2;          baz();      }  }   function baz() {      console.log( a );  }   // ajax(..)是某个库中的某个Ajax函数 ajax( "http://some.url.1", foo );  ajax( "http://some.url.2", bar ); 

协作 p155

就是将一个大的数据不断拆分成一部分然后处理,再通过settimeout来做个神秘的魔法。



任务p156

console.log( "A" );   setTimeout( function(){ 
console.log( "B" );  },
0 );   // 理论上的"任务API"  
schedule( function(){      
console.log( "C" );       schedule( function(){          
console.log( "D" ); } ); 
} );


语句顺序p159
代码中语句的顺序和 JavaScript 引擎执行语句的顺序并不一定要一致。这个陈述可能看起 来似乎会很奇怪,所以我们要简单解释一下。

尽管 JavaScript 语义让我们不会见到编译器语句重排序可能导致的噩梦,这是一种幸运, 但是代码编写的方式(从上到下的模式)和编译后执行的方式之间的联系非常脆弱,理解 这一点也非常重要。
编译器语句重排序几乎就是并发和交互的微型隐喻。作为一个一般性的概念,清楚这一点 能够使你更好地理解异步 JavaScript 代码流问题。


小结
实际上,JavaScript 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响 应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访 问,所以对状态的修改都是在之前累积的修改之上进行的。
一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个 tick。用户交互、IO 和定时器会向事件队列中加入事件。
任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一 个或多个后续事件。
并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。
通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交 互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身 分割为更小的块,以便其他“进程”插入进来。