一.同步和异步的概念。

同步:即按代码的顺序执行任务。

在下列代码中,按照同步概念,则是先打印1后打印2。

 console.log(1);
console.log(2);

异步:即执行一个任务的同时执行另一个任务。如果按照此概念执行上面代码,则是同时打印出1和2。

二.客户端JavaScript中代码的执行顺序

首先,不管是核心JavaScript还是客户端JavaScript都不包含任何线程机制,只有一个单线程执行模型。单线程即指脚本和事件处理程序在同一时间只能执行一个,不能同时执行,没有并发性。(HTML5定义了一种为后台线程的“Web Worker”,本人不甚了解,不做赘述)。

单线程的好处在于编程更加简单,编写代码可以确保两个事件处理程序不会同时运行,操作DOM文档也不会担心有其他线程同时修改文档。但这也意味着JavaScript脚本和事件处理程序不能运行太久,否则会降低网页的可读性,甚至导致浏览器奔溃的假象。那么,一个JS程序是怎么具体执行的呢?

JS的执行任务分为同步任务和异步任务:

同步任务:指除了异步任务之外的其他程序。

异步任务:指各种事件(比如资源载入事件中的loaded、DOMContentLoaded中的回调函数,普通事件中的click,focus,mouseover中的回调函数,window对象的定时器setInterval、setTimeout中的回调函数等等)。

过程:一个js程序执行时,首先将同步任务放入一个执行栈中,先解析同步任务和异步任务并且按顺序执行所有同步任务。当异步任务被触发时(如用户点击鼠标或者按下键盘),异步进程处理则会检测到并将其对应的异步任务转移到任务队列中。同步任务全部执行完毕后,则查看任务队列中是否有未完成的回调函数,如果有则按顺序执行。在此后期间会不断查看任务队列并不断执行,形成事件循环。请看如下过程图

三、HTML文件中script标签的执行顺序和其属性defer、async产生的影响

1.在默认情况下,HTML解析器遇到script标签时,是先执行脚本,进入脚本并按上面所述的顺序执行完代码。然后再继续解析渲染HTML页面文档,这是对于内联脚本来说。但同样的,对于一个由src属性指定外部文件的脚本来说,也是先下载并执行该脚本。也就是说,在完成改脚本的下载和执行前,其后面的文档部分都不会显现出来(实际上DOM树已经被载入,但是没被解析为DOM树)。

以下一个1996最先进的JS代码可以证明该概念(当时没有那么多的异步事件API实现异步调用,所以用如下的同步程序来实现动态添加HTML元素)

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>Table of Factorials</h1>
<script>
function factorial(n) { //用来实现阶乘的函数
if(n <= 1) return n;
else return n * factorial(n-1);
} document.write("<table>"); //开始创建表格
document.write("<tr><th>n</th><th>n!</th></tr>"); //创建表头
for(var i = 0;i <= 10;i++) {
document.write("<tr><td>" + i + "</td><td>" + factorial(i) + "</td></tr>"); //输出十行表格
}
document.write("</table>"); //表格结束
document.write("Generated at " + new Date()); //输出时间戳
</script>
    <h2>Table of Factorials</h2>
</body>
</html>

以下为结果图:

可以得知,脚本的执行在默认的情况下是同步和阻塞的,这是由其单线程模型决定的。但是,对于使用src引入外部文件的script标签来说,其属性defer和async可以改变这种情况,实现异步调用

2.对于外联脚本(即由src属性引入外部js文件的脚本),其有两个属性可以改变同步状态——deferasync只要有这两个属性之一即为异步脚本

defer(延迟):有了该属性的外联脚本会延迟解析执行,即等待文档的载入和解析完成并可以操作时(不包括img,即可以理解为DOMContentLoaded事件触发时)才解析执行。看以下代码:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css" media="screen">
div {
width: 100px;
height:100px;
}
</style>
</head>
<body>
<script type="text/javascript" src="console.js" defer></script>
<script type="text/javascript">
console.log(+new Date());
</script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded",function(){
console.log(+new Date());
});
</script>
</body>
</html>

console.js的代码为:

 console.log(+new Date());

运行结果截图:

可见,带有defer属性的console.js代码是第一个内联js执行后最后一个内联js执行前才执行的,印证了以上说法

async(异步):HTML解析器在遇到带有该属性的脚本时,不会中止页面文档的解析,而是一边下载该脚本一边继续后面文档的解析,一旦脚本下载解析完成则尽快停止文档解析并回去解析执行该脚本,从而避免了下载脚本时阻塞文档解析,可以凭此提高文档解析加载速度。看以下代码:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css" media="screen">
div {
width: 100px;
height:100px;
}
</style>
</head>
<body>
<script type="text/javascript" src="console.js" async></script>
<script type="text/javascript">
console.log(+new Date());
</script>
<div> </div>
</body>
</html>

结果截图:

可以看出,原本按照默认方式应当先打印的console.js文件反而在内联script标签之后执行,可以印证上面所述。

那么,如果两个属性都同时拥有呢?这样的标签会按照什么方式执行?答案是浏览器会遵从async属性并忽略defer属性。

注意点:

1.拥有这两个属性的script标签的js文件即为异步脚本,异步脚本不能使用document.write()(因为如果用该函数会覆盖掉其对应标签解析之前的文档内容);如下面代码:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="console.js" defer></script>
<script type="text/javascript" src="console2.js" async></script> </head>
<body> </body>
</html>

其中console.js和console.log2代码都为:

 document.write(1);

结果截图:

2.defer和async都是布尔属性,没有值,只要出现即能激活该属性

3.defer和async都只适用于外联脚本,内联脚本使用这两个属性是无效的。

4.defer能访问完整的文档树,无论其脚本位置在何处;而async必定能看到其脚本所在位置之前的文档树,但是可能或不可能访问其后面的文档内容。

四、客户端JavaScript执行顺序的总结

JS程序的执行有两个阶段

第一阶段:解析载入HTML文档的内容,并执行<script>元素里的代码(包括内联脚本和外部脚本),通常按其出现顺序执行。除非出现defer、async属性使其成为异步脚本(详情见上面defer、async属性的说明)

第二阶段:这个阶段是异步的,而且是由事件驱动的(即有用户事件才会发生)。在这个阶段,一旦用户产生事件,浏览器就会调用之前脚本中的事件处理程序函数,来响应异步发生的事件(如鼠标单击,键盘输入。此时对应的事件处理回调函数被放在了任务队列中,详情见第二部分)

我们对这两个阶段再进行详细的划分,形成一条理想的时间线:

1.Web浏览器创建一个Document对象,并开始解析渲染HTML文档,生成Element对象和Text节点放入文档中。此时,document.readystate的值为“loading”。

2.当解析HTML文档过程中遇到没有async和defer属性的脚本时,解析器停止解析文档并开始按顺序对脚本进行解析执行,此时脚本内可以便利和操作脚本之前的文档树。解析完遇到的脚本后则继续文档的解析,以此类推

3.如果遇到带有async属性的脚本,浏览器会一边下载该脚本一边继续后面文档内容的解析,当脚本下载解析完毕后立即返回解析执行该脚本。

4.当文档完成解析时,此时document.readystate的值为interactive

5.然后按照其出现顺序继续解析执行带有defer属性的脚本

6.所有的文档和脚本加载执行渲染完成后(不包括外部加载的图片多媒体文件等)浏览器触发了Document对象的DOMContentLoaded事件,标志着程序执行从同步脚本执行阶段进入到了异步事件处理事件程序执行阶段。注意此时可能还有异步任务还没执行完成。

7.此时,文档已经完全解析完成,但是有一些内容还在加载,如图片。当这些内容完全加载并且异步脚本全部载入和执行后,document.readystate的值为“complete”,并且触发window.onload事件。

8.此刻起,调用异步事件,以异步响应用户输入事件。

注意:这是一条理想的时间线。DOMContentLoaded事件和document.readystate属性大部分浏览器都支持。defer属性也被大部分浏览器支持。而async在IE9及其之前的版本是不支持的。

浅谈个人对客户端JavaScript同步、异步、执行顺序等概念的理解的更多相关文章

  1. JavaScript程序的执行顺序

    JavaScript程序的执行顺序:同步==>异步==>回调 同步是阻塞模式,异步是非阻塞模式.     同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个 ...

  2. js 异步执行顺序

    参考文章: js 异步执行顺序   1.js的执行顺序,先同步后异步 2.异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列 3.调用Promise 中的res ...

  3. [转载]Javascript 同步异步加载详解

    http://handyxuefeng.blog.163.com/blog/static/4545217220131125022640/ 本文总结一下浏览器在 javascript 的加载方式. 关键 ...

  4. js同步、异步、回调的执行顺序以及闭包的理解

    首先,记住同步第一.异步第二.回调最末的口诀 公式表达:同步=>异步=>回调 看一道经典的面试题: for (var i = 0; i < 5; i++) { setTimeout( ...

  5. 浅谈 .NET 中的对象引用、非托管指针和托管指针 理解C#中的闭包

    浅谈 .NET 中的对象引用.非托管指针和托管指针   目录 前言 一.对象引用 二.值传递和引用传递 三.初识托管指针和非托管指针 四.非托管指针 1.非托管指针不能指向对象引用 2.类成员指针 五 ...

  6. JavaScript Alert 函数执行顺序问题

    * { color: #3e3e3e } body { font-family: "Helvetica Neue", Helvetica, "Hiragino Sans ...

  7. html 和 javascript 的相关执行顺序

    1.dom 树和 js 的加载顺序 http://blog.csdn.net/jdsxzhao/article/details/44646463 2. jquery中各个事件执行顺序如下: https ...

  8. 通过实验窥探javascript的解析执行顺序

    简介 javascript是一种解释型语言,它的执行是自上而下的.但是各浏览器对于[自上而下]的理解是有细微差别的,而代码的上下游也就是程序流对于程序正确运行又是至关重要的.所以我们有必要深入理解js ...

  9. 浅谈WebService开发二(同步与异步调用)转

    上文 <http://www.dotnetgeek.cn/xuexiwebservice1.html>已经跟大家说了,如果创建一个webservice和简单的调用,本文将注重webserv ...

随机推荐

  1. unicode编码原理及问题

    历史在1963年,计算机的使用尚不广泛,那时使用的是7-bit的ASCII码,范围为0-127作为字符的编码,只支持少部分的字符,但是随着计算机的普及,不同的国家地区开始自己制造自己的编码规范,这导致 ...

  2. 【Offer】[43] 【1~n整数中1出现的次数】

    题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 输入一个整数n,求1~n这n个整数的十进制表示中1出现的次数.例如,输入12, 1~12这些整数中包含1的数字有1.10.11和12,1 ...

  3. 使用FlameGraph火焰图分析JAVA应用性能

    开源项目推荐 Pepper Metrics是我与同事开发的一个开源工具(https://github.com/zrbcool/pepper-metrics),其通过收集jedis/mybatis/ht ...

  4. BigDecimal转String

    代码: public static void main(String[] args) { // 浮点数的打印 System.out.println(new BigDecimal("10000 ...

  5. MultipartFile 获取上传TXT文件字数

    @ResponseBody @RequestMapping(value = "/addImgForDynamic")//(发布动态) public Map addImgForDyn ...

  6. 基于 HTML5 的 PID-进料系统可视化界面

    前言 随着工业物联网和互联网技术的普及和发展,人工填料的方式已经逐渐被机械设备取代.工业厂商减小误操作.提升设备安全以及追求高效率等制造特点对设备的要求愈加高标准.严要求.同时机械生产以后还需遵从整个 ...

  7. cobbler高可用方案

    一.环境准备 主网IP 私网IP 主机名 角色 VIP 10.203.178.125 192.168.10.2 cnsz22VLK12919 主 10.203.178.137,192.168.10.1 ...

  8. 数据库(DDL,DML,DQL、DCL)

    1.数据查询语言DQL 数据查询语言DQL基本结构是由SELECT子句,FROM子句,WHERE   子句组成的查询块:   SELECT <字段名表>   FROM <表或视图名& ...

  9. rocketmq学习(一) rocketmq介绍与安装

    1.消息队列介绍 消息队列本质上来说是一个符合先进先出原则的单向队列:一方发送消息并存入消息队列尾部(生产者投递消息),一方从消息队列的头部取出消息(消费者消费消息).但对于一个成熟可靠的消息队列来说 ...

  10. Linux 笔记 - 第十八章 Linux 集群之(三)Keepalived+LVS 高可用负载均衡集群

    一.前言 前两节分别介绍了 Linux 的高可用集群和负载均衡集群,也可以将这两者相结合,即 Keepalived+LVS 组成的高可用负载均衡集群,Keepalived 加入到 LVS 中的原因有以 ...