详解defer和async的原理及应用

转自CSDN: https://blog.csdn.net/liuhe688/article/details/51247484.

deferasyncscript标签的两个属性,用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。

在介绍他们之前,我们有必要先了解一下页面的加载和渲染过程:

  1. 浏览器通过HTTP协议请求服务器,获取HMTL文档并开始从上到下解析,构建DOM
  2. 在构建DOM过程中,如果遇到外联的样式声明和脚本声明,则暂停文档解析,创建新的网络连接,并开始下载样式文件和脚本文件
  3. 样式文件下载完成后,构建CSSDOM;脚本文件下载完成后,解释并执行,然后继续解析文档构建DOM
  4. 完成文档解析后,将DOM和CSSDOM进行关联和映射,最后将视图渲染到浏览器窗口

在这个过程中,脚本文件的下载和执行是与文档解析同步进行,也就是说,它会阻塞文档的解析,如果控制得不好,在用户体验上就会造成一定程度的影响。

所以我们需要清楚的了解和使用deferasync来控制外部脚本的执行。

在开发中我们可以在script中声明两个不太常见的属性:deferasync,下面分别解释了他们的用法:

  • defer:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
  • async:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。

为了演示脚本的执行情况,进而介绍这两个属性的作用,我们先来搭建一个简单的服务器:

如图所示,我们创建一个app目录,用于放置一些简单的Web资源,同时创建了一个简易的Node服务器server.js,其代码如下:

var http = require('http');
var fs = require('fs');

var typeMapping = {
    'html': 'text/html',
    'js'  : 'text/javascript',
    'css' : 'text/css',
    'ico' : 'image/x-icon'
};

var getResourceExtension = function(req) {
    var url = req.url;
    var lastIndexOfDot = url.lastIndexOf('.');

    if (lastIndexOfDot === -1) return 'text/plain';
    return url.substring(lastIndexOfDot + 1);
};

var respondResourceToClient = function(req, res) {
    //read the reource and respond via 'pipe'
    fs.createReadStream(req.url.replace(/^\//, '')).pipe(res);
};

var server = http.createServer(function(req, res) {

    console.log('requesting url: ', req.url);

    var extension = getResourceExtension(req);

    res.writeHead(200, {'Content-Type': typeMapping[extension]});

    var delay = function(time) {
        setTimeout(function() {
            respondResourceToClient(req, res);
        }, time || 0);
    };

    if (extension === 'html' || extension === 'css') {
        delay(0);
    } else if (extension === 'js') {
        delay(1000);
    } else {
        res.end('');
    }
});

server.listen(3000);

console.log('listening at port 3000...');

从上面的代码我们可以看出,当服务器接收到请求之后,会判断请求资源是否为JS,如果是则延迟1s后返回对应的资源。

启动这个服务很简单,只需执行node server.js即可,然后就可以在浏览器中输入http://localhost:3000/app/index.html访问主页了,现在我们来看看index.html中的内容:

<!DOCTYPE html>
<html>
    <head>
        <title>defer & async</title>
        <link rel="stylesheet" type="text/css" href="css/main.css">
        <script type="text/javascript" src="js/1.js"></script>
    </head>
    <body>
        <div class="text">Hello World</div>
        <script type="text/javascript" src="js/2.js"></script>
    </body>
</html>

在这个HTML文档中,我们先在head中引用了一个外部的脚本文件js/1.js,然后在我们要显示的Hello World后面又引用了一个js/2.js,它们的内容都很简单,就是弹出对应的标示信息:

// js/1.js
alert(1);

// js/2.js
alert(2);

下面我们就来访问主页,看看会发生些什么:

从图中可以看到,渲染的过程的确是自上而下,同步进行的,也就是说遇到外部的脚本,就得暂停文档的解析,下载并且解释执行,这种方式是阻塞的,会造成网页空白的现象。

现在稍微修改一下代码,将head中的script标签加上defer属性,然后也稍微改动一下两个JS文件:

<!DOCTYPE html>
<html>
    <head>
        <title>defer & async</title>
        <link rel="stylesheet" type="text/css" href="css/main.css">
        <!-- adding a 'defer' attribute, by default, the value will be 'true' -->
        <script type="text/javascript" src="js/1.js" defer></script>
    </head>
    <body>
        <div class="text">Hello World</div>
        <script type="text/javascript" src="js/2.js"></script>
    </body>
</html>
// js/1.js
console.log(1);

// js/2.js
console.log(2);

再次访问index.html,我们会在控制台中看到下面的执行顺序:

显而易见,1.js被延后致至文档解析完成后执行了,它的执行顺序比body中的<script>还要靠后。与默认的同步解析不同,defer下载外部脚本的不是阻塞的,浏览器会另外开启一个线程,进行网络连接下载,这个过程中,文档解析及构建DOM仍可以继续进行,不会出现因下载脚本而出现的页面空白。

关于defer我们需要注意下面几点:

  1. defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
  2. 如果有多个声明了defer的脚本,则会按顺序下载和执行
  3. defer脚本会在DOMContentLoadedload事件之前执行

我们稍微改动一下代码,验证一下上面的几条:

<!DOCTYPE html>
<html>
    <head>
        <title>defer & async</title>
        <link rel="stylesheet" type="text/css" href="css/main.css">
        <!-- adding a 'defer' attribute, by default, the value will be 'true' -->
        <script type="text/javascript" src="js/1.js" defer></script>
        <script type="text/javascript" src="js/2.js" defer></script>
        <script type="text/javascript" defer>
            console.log(3);
        </script>
    </head>
    <body>
        <div class="text">Hello World</div>

        <script type="text/javascript">
            document.addEventListener("DOMContentLoaded", function() {
                console.log('dom content loaded, ready state:', this.readyState);
            }, false);

            window.addEventListener('load', function() {
                console.log('window loaded, dom ready state:', document.readyState);
            }, false);
        </script>
    </body>
</html>

上面代码中,head中所有的script标签都加上了defer,其中第三个是内联脚本,然后我们也添加了DOMContentLoadedload事件,下面来看一下打印的结果:

可以看到,外联的脚本是按照声明顺序执行的,内联脚本并没有遵守这个规则,另外,DOMContentLoadedload事件依次被捕获,DOM的状态分别变为interactivecomplete

接下来我们介绍一下async属性,为了能够很好的演示执行顺序,我们还需要一些修改:

<!DOCTYPE html>
<html>
    <head>
        <title>defer & async</title>
        <link rel="stylesheet" type="text/css" href="css/main.css">
        <!-- adding a 'async' attribute, by default, the value is 'true' as well -->
        <script type="text/javascript" src="js/1.js" async></script>
        <script type="text/javascript" src="js/2.js" async></script>
        <script type="text/javascript" src="js/3.js" async></script>
    </head>
    <body>
        <div class="text">Hello World</div>
    </body>
</html>

JS文件内如下:

// js/1.js
console.log(1);

// js/2.js
console.log(2);

// js/3.js
console.log(3);

再次访问index.html,会发现控制台打印如下:

我们发现,3个脚本的执行是没有顺序的,我们也无法预测每个脚本的下载和执行的时间和顺序。asyncdefer一样,不会阻塞当前文档的解析,它会异步地下载脚本,但和defer不同的是,async会在脚本下载完成后立即执行,如果项目中脚本之间存在依赖关系,不推荐使用async

关于async,也需要注意以下几点:

  1. 只适用于外联脚本,这一点和defer一致
  2. 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
  3. async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序

下面这两张图可以更清楚地阐述deferasync的执行以及和DOMContentLoadedload事件的关系:

图片来源:Asynchronous and deferred JavaScript execution explained.

图片来源:Running Your Code at the Right Time.

以上就是deferasync的介绍,了解和掌握这两个属性的作用,不仅对JS代码执行的控制更加熟练,也会对页面渲染多一分了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容