目录
异步
分块的程序
事件循环
并行线程
并发
非交互
交互
协作
任务
语句顺序
异步
js如何表达和控制持续一段时间的程序行为(分散在一段时间内运行的程序行为)
持续一段时间,不是指类似于 for循环开始到结束的过程。而是指 程序的一部分现在运行,而另一部分则在未来运行。现在与将来之间有一段间隙,这段间隙在实际程序中,可以是等待用户输入、从数据库或文件系统中请求数据、通过网络发送数据并等待响应,或者是在以固定时间间隔执行重复任务(比如动画)。
- JavaScript中的异步,是指程序一部分现在运行,而一部分则在将来运行——现在和将来有时间间隔,在这段间隔中,程序没有活跃执行。
- 程序现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
分块的程序
无论JS程序是在多个JS文件或一个JS文件,JS中都是都是由一个个块组成。同一时间只有一个块可以在 现在执行。最常见的块就是 函数。
var data = ajax("http://some.url.1");
console.log(data); // data通常并不会出现Ajax的结果
因为标准Ajax请求不是同步完成的,当打印data时,ajax函数可能还没有返回任何值给变量data。
如何能够确定得到ajax函数的返回值?1、将ajax(...)
能够阻塞到响应返回,即发出ajax请求后什么事也不做,直到得到返回值。这与我们希望将一部分值在 未来运行是相违背的。
- 程序中将来执行的部分并不一定在现在运行的部分执行完之后就立刻执行。比如:Ajax发送请求,需要等待数据返回之后,才会执行“将来”的部分。
- 从现在到将来的“等待”,最简单的方法是使用一个通常称为回调函数的函数。(不是唯一的方案)
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
任何时候,只要把一段代码包装称一个函数,并指定它在响应某个时间(定时器、鼠标点击、Ajax响应等)时执行,你就在代码中创建了一个将来执行的块,也就在这个程序中引入了异步机制
② 异步控制台
ar a = {index: 1};
console.log(a.index); // 1
console.log(a); // 在浏览器控制台中查看为 {index: 2}
a.index++;
- 原因:
浏览器会认为需要把控制台I/O延迟到后台,这种情况下,等到浏览器控制台输出对象内容时,a.index++可能已经执行了,因此会显示 {index: 2}
某些浏览器的console.log()
并不会把传入的内容立即输出,可能出现异步情况。这种情况比较少见,最好的选择是使用断点调试。
事件循环
JS引擎并不是独立运行的,它运行在宿主环境中,大多数为Web浏览器,也有如Node.js等服务器端环境。 这些环境提供一种机制来处理程序中多个块的执行,且执行每块时调用JS引擎,这种机制被称为 事件循环。 可以按照下面的伪代码来理解事件循环:
var eventLoop = [];
var event;
//“永远”执行
while (true) {
// 一次tick
if (eventLoop.length > 0) {
event = eventLoop.shift();
try {
event();
}
catch (err) {
reportError(err);
}
}
}
setTimeout(...)
定时器精度不高原因
setTimeout(…) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。
如果这时候事件循环中已经有 20 个项目了会怎样呢?你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么setTimeout(…) 定时器的精度可能不高。大体说来,只能确保你的回调函数不会在指定的时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的状态而定。
也就是说,定时器只能保证回调函数在设定的时间之后执行,而不是到达设定的时间就会执行,是在设定的时间到了之后入队列,等到调用栈为空时候拿出执行
并行线程
tips:进程和线程??❤❤❤❤❤❤❤
进程和线程是不同的操作系统管理资源的方式,进程有自己独立的内存空间,线程没有独立地址空间,但是有自己堆栈和局部变量,线程是进程的一个实体,是CPU任务调度和分配的基本单位,线程不能独立执行,必须依存在进程之中,同一进程的多个线程共享同一内存空间,进程退出时改进程的所有线程会被清空
”异步“与”并行“常常被混为一谈,但实际上它们的意义完全不同。
-
异步:关于现在
和将来
的时间间隙。
事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存并行访问和修改。通过分立线程中彼此合作的事件循环,
-
并行:关于能够同时发生的事情。并行和顺序执行可以共存。
并行最常见的工具有进程
与线程
。进程与线程独立运行,并可能同时运行,多个线程能够共享单个进程的内存。
JS不支持跨线程共享数据( 单线程 ),并且具有 完整运行
特性,即如果有两个函数执行,两个函数不会交替运行,一定是先完整执行第一个函数,然后才是第二个函数。
虽然JS不需要考虑 线程层次的不确定性,但是依然存在 竞态条件
,考虑下面的代码:
我们无法预料哪个ajax会先请求到数据,但是不论是谁先请求到数据,在它执行回调函数的时候内部不会被打断,所以它只可能有两种结果,这取决与谁先请求到数据。
“竞态”就是在多线程的编程中,你在同一段代码里输入了相同的条件,但是会输出不确定的结果的情况
var a = 0;
function foo(){
a += 1;
}
function bar(){
a += 2;
}
// 两个方法会并行运行(实际上并不是同时发生的,先1后2,但异步时间的间隙无法确定先后),但最终的结果无法预测
ajax('http://xxxx.url.1', foo);
ajax('http://xxxx.url.2', bar);
并发
设想一个状态更新列表,随着用户向下滚动列表而住就按加载更多内容。这里需要两个”进程“,一个监听页面滚动触发onscroll
,并发起Ajax请求,另一个接受Ajax请求,并把内容展示到页面。当用户滚动页面时,可能在等待第一个响应的同时,就会有第二、第三个请求发出。
两个或多个”进程“同时执行就出现了并发,不管组成它们的单个运算是否 并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作”进程级的并行,与运算级的并行(不同处理器上的线程)相对。
- 如果并发的”进程“需要通过
作用域
或DOM
间接监护,就需要对交互进行协调,避免竞态的出现。
非交互
下面的程序,无论按哪种顺序执行都无所谓,foo
可能先执行,bar
可能先执行,但因为它们是独立运行的,所以不会相互影响。
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( "http://some.url.1", response );
ajax( "http://some.url.2", response );
上面的程序,由于执行顺序的不确定性,会导致res
中数据的存放顺序也是不确定的。假定我们期望res[0]
存放"http://some.url.1"
的结果,res[1]
存放"http://some.url.2"
的结果,在这种情况下,我们就需要协调交互顺序。下面是一种做法:
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( "http://some.url.1", response );
ajax( "http://some.url.2", response );
协作
考虑这里有一个执行时间特别长的程序,它的执行会阻塞其他程序的进行,因为JS
是单线程的,同时UI
不能进行刷新,因为JS
引擎线程与渲染线程互斥,此时我们就需要将它分割成多个步骤或多批任务,使得其他并发程序有机会将自己的运算插入到事件循环队列中交替运行,这就是并发协作。
以处理1000万
条数据为例,我们采取并发协作的方式来实现:
var res = [];
// response(..)从Ajax调用中取得结果数组
function response(data) {
// 一次处理1000个
var chunk = data.splice( 0, 1000 );
// 添加到已有的res组
res = res.concat(
// 创建一个新的数组把chunk中所有值加倍
chunk.map( function(val){
return val * 2;
} )
);
// 还有剩下的需要处理吗?
if (data.length > 0) {
// 异步调度下一次批处理
setTimeout( function(){
response( data );
}, 0 );
}
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
复制代码
我们每次处理1000
条数据,然后将剩余数据通过定时器方式再加入事件循环队列末尾,好让其他程序有执行的机会,不断反复,就达到了交替执行。
任务
在ES6中,有一个新的概念建立在事件循环队列之上,叫做任务队列(job queue)
,这个概念用于Promise的异步特性。
事件循环队列类似于一个游乐园游戏:玩过了一个游戏之后,你需要重新到队尾排队才能再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。
任务队列是挂在前文提到的事件循环队列的每个tick之后的一个队列,但是不是被添加到队列末尾,而可以直接插队,优先处理,也即:”尽可能早的将来“。
语句顺序
代码中语句的顺序和JS引擎执行语句的顺序并不一定要一致。JS引擎在编译代码之后,可能会对语句的顺序进行重新安排,以提高执行速度。可以保证的是,JS引擎在编译阶段执行的优化都是安全的优化。