您的位置:首页 > 其它

异步:现在与未来

2017-12-29 23:45 232 查看
写在前面的话

本文是你不知道的JavaScript(中卷 )读书笔记,文章中的很多例子都来源于原书,当然本文也结合了自己的一些理解及总结归纳,如果你能看到这篇文章,请批判的阅读本文。

1 分块的程序

程序无论简单或者复杂,无论基于何种编程语言,都是由代码块组合而成的,每个代码块都扮演着自己角色,相互分工,相互合作,从而让一个程序运行起来。

在JavaScript(后面简称JS)中,可以把JS程序写在.js文件中,这个JS程序代码块或许几十行,或者几万行,但我们都可以把这些块分为两块,现在执行的块,将来执行的块。

在.js文件中,JS引擎会从上到下对代码进行解析并判断是否采取进一步操作,比如触发某个事件,并执行这个事件。那么问题来了,如果一个代码块A(任务A)在代码块B(任务B)前,而代码块A会占用很长的时间,这时就无法执行代码块B了,人们可能会认为这时出现阻塞情况。A阻塞B。但JS程序中并不会出现这种情况,现在无法完成或者需要特定场景下才能完成的任务放在将来执行,也就是说现在无法完成的任务异步执行。

看下面的代码

//ajax是某个库中提供的AJAX函数
var data = ajax(url);
console.log(data);     //undefined


data通常是
undefined
因为AJAX是异步执行的,在执行
console.log(data)
时,data并没有通过AJAX获得到。

那么如何获得
data
的值并打印到控制台呢,
data
是通过AJAX异步操作返回的值,当异步操作完成后(在将来某个点),我们在打印
data
值,而在完成(现在到将来)这段时间的等待,最简单的方法是使用回调函数。

ajax(url,function myCallbackFunction(data) {
console.log(data);
});


看下面的代码

function now() {
return 21;
}

function later() {
answer = answer * 2;
console.log('Meaning of life:', answer);
}

let answer = now();
setTimeout(later, 1000);


这个程序有两个块,现在执行的部分,将来执行的部分

现在

function now() {
return 21;
}

function later() {
..
}

let answer = now();
setTimeout(later, 1000);


将来

answer = answer * 2;
console.log('Meaning of life:', answer);


现在这一块在程序运行完之后立即运行,但是setTimeout设置了一个事件(定时)在将来执行,所以函数later()的内容会在之后的某个时间(从现在起1000ms之后)执行。

下面斜体一段引自原书

任何时候,只要把一段代码包装成一个函数,并指定他在响应某个事件(定时器,鼠标点击,Ajax响应等)时执行,你就是在代码块中创建了一个将来执行的块也由此在这个程序中引入了异步机制。

异步控制台

console.*方法族如何工作并没有标准规范,它们不是javascript正式的一部分,而是由宿主环境添加到JS中的,不同的浏览器和JS环境可以按照自己的意愿来实现。有时候这会引起混淆。

值得注意的是,在某些条件下, 某些浏览器的
console.log(..
)并不会把传入的内容立即输出。在许多程序(不只是
JavaScript
)中,
I/O
是非常低

速的阻塞部分。所以,(从页面 /UI 的角度来说)浏览器在后台异步处理控制台
I/O
能够提高性能,这时用户甚至可能根本意识不到其发生。

下面这种情景不是很常见,但也可能发生

var a = {
index: 1
};
// 然后
console.log( a ); // ??
// 再然后
a.index++;


我们通常认为恰好在执行到 console.log(..) 语句的时候会看到 a 对象的快照,打印出类似于 { index: 1 } 这样的内容,然后在下一条语句 a.index++ 执行时将其修改,这句的执行会严格在 a 的输出之后。

多数情况下,前述代码在开发者工具的控制台中输出的对象表示与期望是一致的。但是,这段代码运行的时候,浏览器可能会认为需要把控制台 I/O 延迟到后台,在这种情况下,等到浏览器控制台输出对象内容时, a.index++ 可能已经执行,因此会显示 { index: 2 }。到底什么时候控制台 I/O 会延迟,甚至是否能够被观察到,这都是游移不定的。如果在调试的过程中遇到对象在 console.log(..) 语句之后被修改,可你却看到了意料之外的结果,要意识到这可能是这种 I/O 的异步化造成的。

如果遇到这种少见的情况,最好的选择是在 JavaScript 调试器中使用断点,而不要依赖控制台输出。次优的方案是把对象序列化到一个字符串中,以强制执行一次“快照”,比如通过 JSON.stringify(..)。

2.事件循环

JS引擎运行在宿主环境中(浏览器或者Node.js等),这些宿主环境都有一个共同“点”(thread,线程),它们都提供一种机制来处理程序中多个块的执行,且执行每块时调用JS引擎,这种机制称为“事件循环”。

换句话说, JavaScript 引擎本身并没有时间的概念,只是一个按需执行 JavaScript 任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。

举例

JavaScript 程序发出一个 Ajax 请求,从服务器获取一些数据,那你就在一个函数(通常称为回调函数)中设置好响应代码,然后 JavaScript 引会通知宿主环境:“嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个函数。“

事件循环伪代码

// eventLoop是一个用作队列的数组
//(先进,先出)
var eventLoop = [ ];
var event;
//“永远”执行
while (true) {
// 一次tick
if (eventLoop.length > 0) {
// 拿到队列中的下一个事件
event = eventLoop.shift();
// 现在,执行下一个事件
try {
event();
}
catch (err) {
reportError(err);

}
}
}


有一个用 while 循环实现的持续运行的循环,循环的每一轮称为一个 tick。对每个 tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件就是你的回调函数。

setTimeout(..) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。

如果这时候事件循环中已经有 20 个项目了,你的回调就会等待,所以setTimeout(..)定时器的精度可能不高,只能确保你的回调函数不会在指定的时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的状态而定。

1.3 并行线程

“异步和并行”常常被混为一谈,但实际上意义完全不同,异步指的是现在和将来的这段时间间隙(为什么说AJAX是一种异步的操作?我们填写一张表单,然后用AJAX提交到服务器验证,从提交到服务器验证并返回结果是需要时间的,这是在”将来“完成的,在这段时间间隙你完全可以做其他的事情,比如提交另外一张表单,而不是等待上一张表单验证的结果。)并行是关于能够同步发生的事情。

并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至在不同的计算机上,但多个线程能够共享单个进程的内存。

与之相对的是,事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。并行线程
4000
的交替执行和异步事件的交替调度,其粒度是完全不同的。(这段话半知半解,看学完node是否更加深刻理解)

看下面一段代码

var a = 1;
var b = 2;
function foo() {
a++;
b = b * a;
a = b + 3;
}
function bar() {
b--;
a = 8 + b;
b = a * 2;
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );


由于JS单线程特性,这段代码只有两种可能的结果,取决于
foo
bar
哪个先运行,而这由取决于ajax操作拿到服务器返回结果的时间长短。

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

1.4 并发

1.5 任务

1.6 语句顺序
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  读书笔记