JavaScript之异步

来源:互联网 发布:志鸿优化答案查询 编辑:程序博客网 时间:2024/06/03 20:00

JavaScript之异步 - 基本概念

1.分块的程序

var data = $.ajax({                        url: '/Http/GetAction',                        data: { username: "Rod Chen" },                        type: 'GET',                        success: function (data) {                            return data;                        },                        error: function (err) {                        }        })console.log(data.responseText);setTimeout(function () {    console.log(data.responseText);}, 1000);
我们先看一下console.log(data.responseText);代码执行的结果,通常情况下输出的结果为undefined,可能看到这行代码会感觉这行代码放在这里很愚蠢,当然这只是个例子。

假设我们现在执行的代码是$.ajax,那么相对而言,sucess函数(假设ajax正常返回),console.log(data.responseText)都是将来执行的代码块。按照我们代码的逻辑来看,我们希望的是先执行success,然后执行输出语句,因为我们想要输出的肯定是数据而不是一个毫无意义的undefined。这就是问题所在?

标准 Ajax 请求不是同步完成的,这意味着 ajax(..) 函数还没有返回任何值可以赋给变量 data。如果 ajax(..) 能够阻塞到响应返回,那么 data = .. 赋值就会正确工作。可能你也会想到直接将输出语句放在success函数中不就行了吗?是的,这样是可以完成,因为success是$.ajax的回调函数,回调函数会在ajax得到结果之后执行,但是这样会存在一个延时,此时会先执行下面的代码,如果我输出的不仅仅是data,而是下面的这种情况。

var name = "rod chen";var data = $.ajax({                url: '/Http/GetAction',                data: { username: "Rod Chen" },                type: 'GET',                success: function (data) {                    console.log(name);    //rod chen1;                },                error: function (err) {                }})name = "rod chen1";
虽然success输出了name,但是name已经改变了,已经不是在我调用ajax时的name。此时:回调函数引起了混乱。

当然,你会说发送同步 Ajax 请求。尽管技术上说是这样,但是,在任何情况下都不应该使用这种方式,因为它会锁定浏览器 UI(按钮、菜单、滚动条等),并阻塞所有的用户交互。这是一个可怕的想法,一定要避免。

为了避免回调函数引起的混乱并不足以成为使用阻塞式同步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
我们还是先区分现在执行的代码和将来执行的代码
//现在:function now() {return 21;}function later() { .. }var answer = now();setTimeout( later, 1000 );//将来:answer = answer * 2;console.log( "Meaning of life:", answer )
如果我们在setTimeout后面紧接着修改了answer的值,同样还是会引起混乱。当然这个例子可能说服性不大,现在只是为了引出这样的一个事实。

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

2. 事件循环

尽管你显然能够编写异步JavaScript 代码(就像前面我们看到的定时代码),但直到ES6 Promise,JavaScript 才真正内建有直接的异步概念。
Note: JavaScript 引擎本身所做的只不过是在需要(who)的时候,在给定的任意时刻执行程序中的单个代码块。

Question: who是谁,谁需要?

JavaScript 引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是Web 浏览器。提供了一种机制来处理程序中多个块的执行,且执行每块时调用JavaScript 引擎,这种机制被称为事件循环。换句话说,JavaScript 引擎本身并没有时间的概念,只是一个按需执行JavaScript 任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。

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

Answer: 所以对于上面的问题,真正的需要是事件循环,浏览器会在特定的时间将函数插入到时间循环中用于调用。其实也可以看到异步行为都是依靠事件循环来实现的。

那么,什么事件循环?
先看一段伪代码,仅仅用于理解

// eventLoop是一个用作队列的数组//(先进,先出)var eventLoop = [ ];var event;//“永远”执行while (true) {    // 一次tick    if (eventLoop.length > 0) {        // 拿到队列中的下一个事件        event = eventLoop.shift();        // 现在,执行下一个事件        try {            event();        }        catch (err) {            reportError(err);        }    }}
有一个用while 循环实现的持续运行的循环,循环的每一轮称为一个tick,用户交互、 IO 和定时器会向事件队列中加入事件。对每个tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件就是你的回调函数。

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

Summary: 程序通常分成了很多小块,在事件循环队列中一个接一个地执行。严
格地说,和你的程序不直接相关的其他事件也可能会插入到队列中。 这也是在前面为什么非要提到块概念的意义。

3. 并行线程

术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。
异步是关于现在和将来的时间间隙,异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。
并行是关于能够同时发生的事情,并行是针对多处理器而言的。

并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。从微观的角度来说的,具体是指进程的并行性(多处理机的情况下,多个进程同时运行)和并发性(单处理机的情况下,多个进程在同一时间间隔运行的)。

事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。并行线程的交替执行和异步事件的交替调度,其粒度是完全不同的

完整运行
由于JavaScript的单线程特性, 函数中的代码具有原子性。(原子性: 一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就像数据库里面的事务一样,他们是一个团队,同生共死)。

如果一个javascript开始执行,那么当前块的代码都会一起执行,不会出现说我当前的代码没有执行完,就执行其他的代码块,这称为“完整运行特性”。

4. 并发

其实web网站中页面的加载存在多个ajax的请求,然后我们拿到请求结果会展示,会计算。这样的话,就会出现一种情况,在某个响应返回并处理的时候可能会看到两个或更多ajax请求被触发。

两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对。

ajax, 请求1ajax, 请求2 响应1ajax, 请求3 响应2响应3ajax, 请求4ajax, 请求5ajax, 请求6 响应4ajax, 请求7响应6响应5响应7

如上面的执行情况:可能某个ajax请求事件和某个Ajax 响应事件恰好同时可以处理。通过事件循环的概念,JavaScript 一次只能处理一个事件,所以要么是ajax请求2 先发生,要么是响应1 先发生,但是不会严格地同时发生。

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

下面列出了事件循环队列中所有这些交替的事件:
ajax, 请求1 <--- 进程1启动ajax, 请求2响应1 <--- 进程2启动ajax, 请求3响应2响应3ajax, 请求4ajax, 请求5ajax, 请求6响应4ajax, 请求7 <--- 进程1结束响应6响应5响应7 <--- 进程2结束
“进程”1 和“进程”2 并发运行(任务级并行),但是它们的各个事件是在事件循环队列中依次运行的。

单线程事件循环是并发的一种形式。

(1) 非交互

看下面的例子

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 );
Foo, bar请示是两个并发执行的“进程”,是指一段时间内两个方法交替执行,但是具体的顺序无法得知。但是这里不管执行的顺序谁先谁后,对代码执行的结果都没有影响,因为他们是独立运行的。

由于上述两个方法啊执行的任务彼此不相关,不需要交互。如果进程间没有相互影响的话,不确定性是完全可以接受的。这样的话,两个方法执行之间不存在竟态条件。

(2) 交互

并发的“进程”需要相互交流,如果出现这样的交互,就需要对它们的交互进行协调以避免竞态的出现。
举例
var res = [];function response(data) {    res.push( data );}// ajax(..)是某个库中提供的某个Ajax函数ajax( "http://some.url.1", response );ajax( "http://some.url.2", response );
我们假定期望的行为是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(..)是某个库中提供的某个Ajax函数ajax( "http://some.url.1", response );ajax( "http://some.url.2", response );

还有一种场景,如果不做协调,总是会出错。
var a, b;function foo(x) {    a = x * 2;    baz();}function bar(y) {    b = y * 2;    baz();}function baz() {    console.log(a + b);}// ajax(..)是某个库中的某个Ajax函数ajax( "http://some.url.1", foo );ajax( "http://some.url.2", bar );
无论 foo() 和 bar() 哪一个先被触发,总会使 baz() 过早运行( a 或者 b 仍处于未定义状态);但对 baz() 的第二次调用就没有问题,因为这时候 a 和 b 都已经可用了。这里给出了一种简单方法:
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 );

Note: 包裹 baz() 调用的条件判断 if (a && b) 传统上称为门( gate),我们虽然不能确定 a 和 b到达的顺序,但是会等到它们两个都准备好再进一步打开门(调用 baz())。

5. 任务

在 ES6 中,有一个新的概念建立在事件循环队列之上,叫作任务队列( job queue)。这个概念给大家带来的最大影响可能是 Promise 的异步特性。

任务队列:挂在事件循环队列的每个 tick 之后的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)。这就像是在说:“哦,这里还有一件事将来要做,但要确保在其他任何事情发生之前就完成它。”

设想一个调度任务(调用任务队列)的 API,称其为 schedule(..)。考虑:
console.log( "A" );setTimeout( function(){    console.log( "B" );}, 0 );// 理论上的"任务API"schedule( function(){    console.log( "C" );    schedule( function(){        console.log( "D" );    } );} );
可能你认为这里会打印出 A B C D,但实际打印的结果是 A C D B。因为任务处理是在当前事件循环 tick 结尾处,且定时器触发是为了调度下一个事件循环 tick。

Note: Promise 的异步特性是基于任务的,所以一定要清楚它和事件循环特性的关系。

6. 语句顺序

代码中语句的顺序和 JavaScript 引擎执行语句的顺序并不一定要一致。这些内容应该无法在自己的JavaScript 程序中观察到。
var a, b;a = 10;b = 30;a = a + 1;b = b + 1;console.log( a + b ); // 42
JavaScript 引擎在编译这段代码之后,可能会发现,其实这样执行会更快。
var a, b;
a = 10;a++;b = 30;b++;console.log( a + b ); // 42
尽管 JavaScript 语义让我们不会见到编译器语句重排序可能导致的噩梦,这是一种幸运,但是代码编写的方式(从上到下的模式)和编译后执行的方式之间的联系非常脆弱,理解这一点也非常重要。编译器语句重排序几乎就是并发和交互的微型隐喻。作为一个一般性的概念,清楚这一点能够使我们更好地理解异步 JavaScript 代码流问题。
0 0