JavaScript 学习笔记之线程异步模型

  核心的javascript程序语言并没有包含任何的线程机制,客户端javascript程序也没有任何关于线程的定义,事件驱动模式下的javascript语言并不能实现同时执行,即不能同时执行两个及以上的事件处理程序,所有的处理过程都是同步进行的。javascript的这种线程模式在大型复杂的web应用中显得捉襟见肘,实际工作中,我们会不遗余力的寻找各种异步模型来弥补这一点,直到HTML5中web worker的出现才让javascript的多线程模型出现曙光,尽管这种技术并不是真正意义上实现多线程,下面就自己在工作中的摸索分享如下:

javascript的线程模型

  客户端js程序是类似单线程执行的,采用同步通讯,即浏览器在响应用户的某个事件处理过程中,步通讯会中断浏览器对其他事件或者回调的响应直至返回处理结果,而在中断过程中浏览器会变的没有响应,看起来像死掉了一样,所以,js脚本编程中,应避免单个事件处理程序过于复杂耗时的计算。如果实在需要,这种处理应在页面文档完全加载完成后执行,且要回馈用户当前浏览器是正在活动而非挂掉;实在不行,也可以将这些大块的复杂耗时处理过程拆分成小的任务在后台延时执行,以获取最大的用户体验。

同步处理过程图形化展示如下:

  js程序的单线程执行模型让程序编写更加灵活简单,不用考虑两个事件处理过程共同操作某个变量产生的问题,也不必担心在文档加载过程中其他线程出现的更改,更不需要顾虑多线程带来的死锁、互斥访问等问题。但在实际工作中,这种单线程处理过程并不能很好的满足项目的需要,HTML5中web worker在一定程度上完善了这种缺陷,下面会详细讲到。对于其同步通讯过程,我们大多会进行异步通讯设计如引用setTimeOut、setInterval等异步过程函数,或者采用ajax异步回调处理等。

异步处理过程图形化展示如下:

下面是在js异步处理上的探索:

1. 延时函数

  js中常用的延时函数有两个,分别是setTimeOutsetInterval,一个是将脚本块延时到将来的某个时间点执行,另一个是将脚本块每隔一段时间执行一次,这两个函数本身就是异步处理的,看下面的例子:

 1 setTimeout(function() {
 2     console.log("first");
 3 }, 0);
 4 setInterval(function() {
 5     if (this.__Initial__ == undefined) {
 6         console.log("second");
 7         this.__Initial__="Initialed"
 8     }
 9 }, 0);
10 //主程序的
11 console.log("main");

上面定义了一个延时处理和一个间隔处理,同时主程序也添加了一个输出处理,而阴影部分把延时和间隔都设置为了0,即理论上立即执行,从同步通讯的角度,程序顺序执行,分别输出first、second和main,实际执行结果如下:

  结果好像有点意外,程序最先输出了main,可见这两个函数是异步处理的,从js同步通讯的角度就不难理解了,首先有一个主控的程序运行环境即输出“main”的执行上下文,其次是两个待处理的事件处理过程(分别为输出“first”和输出“second”),主控程序维持一个事件处理列表,执行顺序为输出“main”、“first”、“second”,上述结果就是证明。

既然这两个函数时异步的,且从上面的结果也能交替输出,理论上就可以实现类似多线程处理了,改写上面的例子(以setInterval为例)如下:

 1 //可以看做线程1
 2 setInterval(function() {
 3     console.log("first");
 4 }, 100);
 5 //可以看做线程2
 6 setInterval(function() {
 7     while (1) {
 8         console.log("second");
 9     }
10 }, 100);
11 //主程序的(可以看做是主线程)
12 for (var i = 0; i < 10; i++) {
13     console.log("main");
14 }

这次我们模拟线程执行的情形,之前说到过它们的执行顺序,从同步的角度,应该是先输出10次“main”,然后从setInterval异步执行的角度,“first”和“second”交替输出。然而阴影部分定义了一个无限循环,以模仿复杂耗时的处理过程,执行上面代码,结果如下:

  结果再次让人意外了有木有,“main”是顺利的执行了10次,然后输出了一次“first”,接下来是无尽的输出“second”,“first”的输出一直处于等待状态,页面随之卡死无反应,并没有像理想中交替输出“first”和“second”,这是js中单线程处理模型造成的。可见:

1. 在简单处理逻辑的情况下,可以通过setInterval模拟多线程处理,但是一旦处理逻辑复杂耗时的情况下,setInterval并不适用,系统又回归到单线程执行模型上了。

2. setTimeOut和setInterval在复杂耗时的情况下会中断浏览器的所有响应,故并不是真正意义上的异步。

3. js中不能够通过无间隔循环监听某一变量来实现事件处理程序的自动触发。

相对于简单的处理过程,通过这两个函数实现交替执行或者触发式响应可行,如果涉及到复杂耗时的计算,我们必须要想办法拆分成子任务或者寻求其他的异步处理机制,如ajax等。

2. Ajax(Asynchronous Javascript and XML)的异步实现

  Ajax的好处不言而喻,做过web开发的人都知道,它的出现给web开发解决了多少问题,页面可以动态的进行局部刷新而不是整个页面全部刷新,页面加载不再是缓慢耗时而可以先加载框架页面然后延时丰富文档内容,许多前端任务可以“并行”进行而不用过分依赖javascript提供的线性路径,页面在处理复杂耗时的任务时仍然能够保持及时响应及互不阻塞的人机交互,这一切的一切都要归功于Ajax的异步处理机制。下面是一个简单的Ajax实现:

 1 function BeginRequest() {
 2     //创建XMLHttpRequest对象
 3     var request = new XMLHttpRequest();
 4     //配置一个异步请求
 5     request.open("POST", "../textPage/httpScript.ashx");
 6     //定义请求状态变化的处理过程
 7     request.onreadystatechange = function() {
 8         if (request.readyState === 4 && request.status === 200) {
 9             alert(request.responseText);
10         }
11     }
12     //设置请求头
13     request.setRequestHeader("content-type", "text/plain");
14     //开启一个请求
15     request.send(null);
16 }

看以看出,普通的Ajax请求可以分为以下四个步骤:

第一步:创建XMLHttpRequest(这里以XHR2为例)对象,这可以由被大多数浏览器兼容并实现的 “ new XMLHttpRequest() ” 语句来实现,但对于IE7之前的IE浏览器来说,需要用下面这种方式创建:

 1 //判断是否支持XMLHttpRequest
 2     if (window.XMLHttpRequest === undefined) {
 3         //如果不支持,则定义一个
 4         window.XMLHttpRequest = function() {
 5             try {
 6                 //用最新的版本创建
 7                 return new ActiveXObject("Msxml2.XMLHTTP.6.0");
 8             }
 9             catch (ex) {
10                 try {
11                     //用旧的版本创建
12                     return new ActiveXObject("Msxml2.XMLHTTP.3.0");
13                 } catch (e) {
14                     //不支持
15                     throw new Error("XMLHttpRequest is not supported");
16                 }
17             }
18         }
19     }

第二步:通过open设置一个请求,这个请求包含两个或者三个参数:

  1) 请求的方法(method):常用的有GET、POST,前者多用于请求指定资源,后者多用于表单提交。当然还有其他的如:DELETE、HEAD、OPTIONS、PUT,这些方法根据各浏览器支持程度多有不同。

  2) 请求的URL:向服务器请求的路径,可以是相对于当前文档的相对路径,也可以是绝对路径(必须要指定协议、主机和端口),跨域请求将会报错。

  3) 请求的类型:同步(false)还是异步(true)请求,此参数为boolean类型,默认为异步请求,可以不显示传递此参数。

第三步:设置请求头,通过setRequestHeader()函数进行,通常在POST请求情况下使用,可以设置Content-type、字符集等,此步可选。

第四步:通过send方法发送请求,如果是GET请求,则传递null值,如果是POST请求,则传递消息内容,内容必须要符合setRequestHeader设置的content-type。

  上面步骤必须按顺序一一进行,不然会抛出异常。请求发出后,你必须要同时处理两种情况:请求成功返回以及任何在请求执行时出现的潜在错误,这里通过XHR对象的onreadystatechange事件实现,此函数对http的请求状态(status)和响应结果的准备状态(readystate)进行监听,详细如下:

1 //定义请求状态变化的处理过程
2     request.onreadystatechange = function() {
3         if (request.readyState === 4 && request.status === 200) {
4             callback(request.responseText);
5         }
6     }

其中,http请求状态status是一个数值,如200表示请求成功,404表示请求目标未找到。响应状态readState也是一个数值,分别表示请求响应过程的各个阶段状态,对照表如下:

代表的常量

所处的阶段

UNSENT

0

Open()还没有执行

OPENED

1

Open()执行后

HEADERS_RECEIVED

2

接收到响应头

LOADING

3

接收到响应的主体内容

DONE

4

响应完成

所以,在使用时也可以用各值的常量代替,如果XMLHttpRequest.DONE。请求过程中,上面的状态会经历0~4的变化,每次变化将会触发onreadystatechange函数,响应的结果包含三个部分:

  1) 标记请求是成功或者失败的状态值;

  2) 响应头的集合;

  3) 请求响应的主体内容。

上面我们通过onreadystatechange判断请求状态(status和readyState)来确定请求成功还是失败,成功时,通过请求对象的responseText或者responseXML属性获取响应结果,然后通过回调函数callback对结果进行处理,失败时根据请求的状态给用户以反馈。

  我们看到,通过对状态值的判断来进行决策未免过于繁琐和混乱,于是XHR2在其中定义了一系列响应过程事件,这些事件覆盖到请求响应的各个阶段,实现这些事件的全部或者部分而不用再对状态进行判断就可以获取我们所想要的信息,大大简化和清晰了代码的流程,这些事件集合见下表:

事件名称

事件触发阶段描述

Loadstart

Send()执行后触发。

Process

请求响应结果返回时触发,如果请求响应过程太快,则不会触发此函数。

Load

请求完成,即响应结束时触发,请求完成并不代表成功,函数中仍需要对状态进行判断来确定请求成功与否。

Timeout

错误处理函数,请求超时时触发。

Abort

错误处理函数,请求终止时触发。

Error

错误处理函数,其他网络错误出现时触发。

Loadend

所有处理过程完成后触发。

注:上述函数集合,在Firefox、Chrome和Safari中支持良好,IE支持较差。这些函数中的Load、Timeout、Abort、Error,对于某个请求,它们之中的一个将被执行,因为结果要么是成功,要么是失败,不会出现两种情况同时存在的。这些函数的的调用方式可通过XMLHttpRequest的对象request进行,如request.onload equest.onprocess等,也可以通过addEventListener添加事件,代码如下:

 1 function BeginRequest() {
 2     //创建XMLHttpRequest对象
 3     var request = new XMLHttpRequest();
 4     //配置一个异步请求
 5     request.open("GET", "../textPage/httpScript.ashx"); 
 6     
 7     //请求开始时的处理事件
 8     request.onloadstart = function() {
 9         console.log("loadstart: the send method has been just called.");
10         request.timeout = 10000;
11     };
12     //过程处理事件
13     request.onprogress = function() {
14         console.log("progress: the response is being downloaded.");
15     };
16     //响应结束时的处理事件
17     request.onload = function() {
18         console.log("load: the request is complete.");
19         if (request.readyState === 4 && request.status === 200) {
20             console.log("success: the request is success.");
21         }
22     };
23     //响应超时
24     request.ontimeout = function() {
25         console.log("timeout: the request is timeout.");
26     };
27     //请求退出
28     request.onabort = function() {
29         console.log("abort: the request is abort.");
30     }
31     //其他错误处理
32     request.onerror = function() {
33         console.log("network error: unkown network-error occured.");
34     };
35     //请求所有过程结束后的处理(相当于一个finally处理)
36     request.onloadend = function() {
37         console.log("loadend: all of the progress are completed.");
38     };
39     
40     //设置请求头
41     request.setRequestHeader("content-type", "text/plain");
42     //开启一个请求
43     request.send(null);
44 }
45 
46 window.onload = function() {
47     BeginRequest();
48 }

上面过程将这些函数悉数定义(chrome浏览器),执行结果如下:

可以看到,正常响应情况下这些函数的执行顺序,值得注意的是,load事件只是标记请求及响应过程已完成,但并不代表这个过程是成功的,故我们在load事件里添加了对状态的判断。上面通过在loadstart事件中用request.timeout=10000语句设置了该请求的超时时间为10s,如果缩短该属性值到10ms会有怎样的执行结果呢:

结果在意料之中,timeout事件执行了,load事件则没有执行。在实际情况中,如果请求响应过程耗时不被实际需求所允许,可以直接通过调用abort函数取消该次请求,如请求太过频繁,新的请求才是用户想要的,对于旧的请求就可以abort掉了,下面看看这个函数的效果,对于上面的例子,我们改写progress函数如下:

1 //过程处理事件
2     request.onprogress = function() {
3         console.log("progress: the response is being downloaded.");
4         request.abort();
5     };

执行上面的例子,结果如下:

可见,abort事件被触发了,故XHR2可以通过调用abort()函数来触发一个onabort()事件。同时,progress事件的event对象含有三个有趣的属性:loaded表示响应过程当前已经传输的字节数(bytes);total表示响应过程全部需要传输的字节数;lengthComputable表示响应过程是否知道整个传输内容的长度,知道则为true,否则为false,这样就可以通过loaded和total属性实现响应过程的进度跟踪,使用户得到更好的体验。

  不难看出,XHR2的这些函数集大大提高了我们对ajax请求状态进行检测和控制,如此,可以把复杂耗时的操作扔给ajax进行异步实现,释放应用程序去实时响应其他消息事件,可以大大提高用户的体验。从线程实现的角度,ajax确实提供了一种多线程的解决方案,但它依赖于http请求,更多时候我们只是将复杂耗时的操作扔给了后台执行,对于客户端来说,我们并没有在多线程的道路上取得建设性的进展,也许HTML5中的web worker会在多线程的实现上给我们以惊喜。

3. Html5中的Web Worker

  作为web前端开发人员,HTML5已经给了太多的惊喜,web worker就是其中之一,它的出现对javascript的固有的单线程模型构成了巨大的挑战,让多线程在javascript中的定义出现了转机,它能够确实的定义多个平行的执行线程(即worker),这些workers生存于自身执行的环境空间,除了消息传递(如postmessage),它们不能够同创建它们的DOM或者Window进行通讯,这就意味着,在javascript脚本中,我们利用worker可以编写复杂耗时的处理过程以及使用同步处理API而不用担心整个浏览器卡死无响应。

  实际使用中,根据需要,可以定义10个、100个甚至上千个worker来并行处理多个复杂耗时的逻辑,这些处理脚本都放在单独的文件中(.js),故worker的使用分为两个部分,一部分在页面DOM级主线程上(即worker对象),另一部分在工作线程worker中(即worker处理的执行上下文),它们之间通过消息传递进行通讯和数据交换。下面我们通过一个例子来看看工作线程(worker)的使用:

我们定义一个叫webworker的工作线程脚本块,存储在文件webworker.js中,实现如下:

 1 //引入工作线程处理过程中依赖的js
 2 //importScripts("../aboutsort.js");
 3 
 4 //接收来自DOM级工作线程对象发送的消息
 5 onmessage = function(e) {
 6     console.log("receive the message from the main thread. the argument value is :" + e.data);
 7     //工作线程开始进行复杂耗时的逻辑处理
 8     BeginRequest();
 9 }
10 
11 function BeginRequest() {
12     //创建XMLHttpRequest对象
13     var request = new XMLHttpRequest();
14     //配置一个异步请求
15     request.open("GET", "../../textPage/httpScript.ashx");
16     //定义请求状态变化的处理过程
17     request.onreadystatechange = function() {
18         if (request.readyState === 4 && request.status === 200) {
19             //正常处理过程被延时5秒钟
20             setTimeout(function() {
21                 //向DOM级主线程发送消息,一般为返回处理结果
22                 postMessage("ok.");
23 
24             }, 5000);
25         }
26         else if (request.status === 404) {
27             postMessage("fail.");
28         }
29     }
30     //设置请求头
31     request.setRequestHeader("content-type", "text/plain");
32     //开启一个请求
33     request.send(null);
34 }

DOM级主线程中调用的实现:

 1 (function() {
 2     //传递工作线程待处理的脚本块路径来创建工作线程对象
 3     var worker = new Worker("../js/worker/webworker.js");
 4     //向工作线程内部发送消息
 5     worker.postMessage("parameter");
 6     //接收来自工作线程的消息
 7     worker.onmessage = function(e) {
 8         console.log("the response from the worker:" + e.data);
 9         //关闭工作线程
10         worker.terminate();
11     }
12     //定义工作线程出错时的处理过程
13     worker.onerror = function() {
14         console.log("error occured.");
15     }
16 } ());

由上可以看出,工作线程的执行脚本是存储在单独的js文件中,通过调用方(DOM级主线程)引用并创建worker对象来调用,它们通过onmessage和postMessage进行消息传递(即请求和响应):

worker对象

1. worker对象可以传递给构造函数new Worker()一个线程脚本块的路径URL来创建,这个URL可以是相对路径,也可以是绝对路径,绝对路径时必须要符合javascript的同源策略。

2. 一个DOM级主线程可以创建多个worker对象,它们可分别处理不同的脚本块,这些worker对象共同依赖这个创建它们的DOM。

3. 拥有一个worker对象后,可以通过对象的postMessage方法向脚本块的执行上下文发送消息,消息内容可通过参数传递,这个参数不局限于string,也可以是对象,HTML5进行深度复制(structure clone)来传递。

4. 可以通过worker对象的message方法接收来自worker处理线程的响应,该方法有一个参数(暂定为e),可以通过访问e.data来获取响应内容,还可以定义对象的error方法对执行过程进行一场捕获。

5. 消息传递建立后,可以通过worker对象的terminate函数进行关闭,在关闭之前可以有多次消息传递。

6. worker对象的这些方法,可以通过atachEvent或者addEventListener等通用事件函数实现。

worker执行上下文(WorkerGlobalScope,也即工作线程执行的脚本块)

1. 它是worker工作线程的全局环境,与创建这个工作线程的DOM和window没有任何关系,它是独立的执行环境,就相当于javascript中的Global变量。

2. 在这个执行环境中,也有一个message函数,后者是接收来自worker对象在DOM级主线程中发送的消息,它有一个参数(暂定为e),可以通过e.data获取传递的消息内容。

3. 另外,也还有一个postMessage函数,这个函数是向DOM级主线程中回发消息使用的,它的执行方向同worker对象的postMessage方法相反,并拥有相同类型的参数(暂定为e)。

4. 这个执行上下文中有一个close方法,它的作用同worker对象的terminate方法相似,实现工作线程的自我关闭,不过worker对象没有检测工作线程是否已经关闭的方法,向一个已经关闭的工作线程发送消息将被忽略。

5. 因为这个执行上下文的独立性,在工作线程脚本块中,不能够访问window对象以及创建它的DOM对象,但是它可以使用许多其他重要的构造函数,如XHR等,甚至可以再次创建工作线程(worker对象的嵌套暂支持不够广泛)。

6. 另外,如果工作线程脚本块中需要依赖其他脚本文件内容,需要跟上面例子中一样,通过importScript函数引入使用。

  到目前为止,从某个DOM级线程创建的各个worker对象是相互独立的,没有任何交集,欣喜的是,HTML5的API中定义了一个叫共享工作线程(share worker)的东西,它的出现让这些独立的worker有了交集,它提供一种计算服务给所有的工作线程,类似网络socket,不过这个技术暂不被广泛支持,期待它广泛支持的那一天。

此外,值得一提的是,在worker的处理脚本中添加以下代码:

1 while (true) {
2     console.log("1");
3 }

运行后发现,DOM级线程并没有被阻塞,即浏览器并没有卡死,可见,工作线程中的处理是完全独立于主程序运行环境之外的,这也符合了多线程的特征,虽然在DOM级的多线程仍然无法实现,但通过web worker ,脚本块之前的多线程已经初见成效,这也是让人欣慰的地方。

小结:随着互联网的进度,用户对web的人机交互的需求越来越高,前端的异步技术在这个趋势中发挥着重要的作用,web的开发者们也在不遗余力的寻找新的、方便的、快捷的异步机制来追求更好的用户体验,上面三种方式均是对javascript异步多线程的尝试,其中延时函数只能伪造多线程执行,有很大的局限性,XHR过分依赖http,好像HTML5中的web worker在多线程的道路上取得了建设性进展,虽然它并没有实现DOM级主程序中的多线程,但这是个很好的开端,希望接下来会越来越好~~~能力有限,欢迎大家指正~

原文地址:https://www.cnblogs.com/freshfish/p/3403436.html