learn-javascript

引入 js

引入使用 js 的方式有多种形式

重点内容便是掌握 js 的加载时序与执行时序

document.currentScript 可以获得当前正在运行的脚本(Chrome 29+, FF4+)

如果是内联嵌入

则按文档流顺序加载,并总是立即执行(依次),此时会阻塞文档解析,此时 async 属性无效。

详见示例script-inline

如果是加载外部 js,则分以下几种情况:

  1. 默认情况下,没有 deferasync 属性

     <script src="script.js"></script>
    

    script 标签默认行为(不带defer或async属性)会阻止文档解析,相关脚本会立即加载并执行。脚本执行顺序和 script 标签出现的顺序一致。测试示例script

    “立即”指的是在渲染该 script 标签时,就立即加载并执行脚本,此时终端该标签之下的文档解析,直到脚本加载并执行完成,再继续后面的文档解析。

  2. 带有 async 属性

    async 属性是html5新特性(IE10+)。

     <script async src="script.js"></script>
    

    async 表示异步加载js(不阻塞文档解析),并且异步执行。脚本执行顺序是乱序的。测试示例script-async

    加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

    NOTE: 通过脚本异步插入的 script 标签达到的效果和带async属性的 script 标签是一样的

  3. 带有 defer 属性

     <script defer src="script.js"></script>
    

    defer 表示会推迟脚本的执行,并且不阻塞文档解析,其执行如同脚本放在 </body> 之前。如果脚本放在<head>中,会更早的下载,且不用担心被其他脚本推迟下载事件。

    加载解析后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,并且在 DOMContentLoaded 事件触发之前完成(会延迟此事件的触发)。测试示例script-defer

    同时,带有defer的脚本彼此之间,能保证其执行顺序。

    注意:DOMContentLoaded 事件必须等待其所属 script 之前的样式表加载解析完成才会触发。

    window 对象上的 onload 事件在所有文件包括样式表,图片等其他资源全部下载完毕后才触发。

  4. 同时带有 async, defer 属性

     <script async defer src="myscript.js"></script>
    

    效果同 async。测试示例script-async-and-defer

概括来讲,就是这两个属性都会使 script 标签异步加载,然而执行的时机是不一样的。如下图

script 加载执行时序

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

此图告诉我们以下几个要点:

理解了,就方便记忆、使用了。

性能分析

script-async script-defer

小扩展

尝试做性能分析:为什么一再强调将css放在头部,将js文件放在尾部?

假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间。

关于 document.write

将一个文本字符串写入一个由 document.open() 打开的文档流

因为 document.write 需要向文档流中写入内容,所以,若在一个已关闭(例如,已完成加载)的文档上调用 document.write,就会自动调用 document.open这将清空该文档的内容

<script>
document.write(`<script src="js/f.js"><\/script>`);
</script>

详细过程:

example

注意:在有deferredasynchronous 属性的 script 中,document.write 会被忽略,控制台会显示 “A call to document.write() from an asynchronously-loaded external script was ignored” 的报错信息。

注意:在 Edge 中,在 <iframe> 内部调用 document.write 多于一次时会引发错误 SCRIPT70: Permission denied。

注意:从 Chrome 55 开始,Chrome(可能)不会运行通过 document.write() 注入的<script>,以防止使用 2G 连接的用户找不到 HTTP 缓存。前往此链接查看这种情况发生需要满足的条件。

如果js是异步引入的(加async或者动态加入的),里面的document.write因安全原因是无法工作的。”Failed to execute ‘write’ on ‘Document’: It isn’t possible to write into a document from an asynchronously-loaded external script unless it is explicitly opened.”

执行顺序和普通的 script 标签没有区别。即使你插入的标签带有asyncdefer,其行为也是没有区别的。

其他

实现动态加载 js

// @dwdjs/utils
const noop = () => {};
const error = url => {
  console.log(`load script error ${url}`);
};
const doc = document;
const domHead = doc.querySelector('head');
const domBody = doc.querySelector('body') || domHead;
// const s = doc.getElementsByTagName('script')[0];

export function loadJs(scriptUrl, obj = {}) {
  const script = document.createElement('script');
  if (typeof obj === 'boolean') {
    // 默认是同步加载,同步模式又称阻塞模式
    // 同步加载流程是瀑布模型,异步加载流程是并发模型。
    obj = {
      async: true, // 异步加载
      defer: true, // 延迟加载
    };
  }
  script.async = obj.async;
  script.defer = obj.defer;
  script.src = scriptUrl;

  script.onload = () => {
    (obj.onload || noop)();
  };
  script.onerror = () => {
    (obj.onerror || error)(scriptUrl);
  };
  // script.crossOrigin = 'anonymous';
  // s.parentNode.insertBefore(s1, s);
  if (obj.first) {
    domHead.appendChild(script);
  } else {
    domBody.appendChild(script);
  }
}

export function loadCss(cssUrl) {
  const style = document.createElement('style');
  style.rel = 'stylesheet';
  style.src = cssUrl;
  domHead.appendChild(style);
}

经典问题

提示

深入理解 script 加载与执行机制,对性能提升,加载器实现非常重要

参考:

性能分析

使用 <link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin> 可优化资源加载 crossorigin属性必须,不然资源会加载两次

https://github.com/marcelduran/webpagetest-api

sudo npm i -g webpagetest webpagetest test https://m.mishifeng.com -k A.29c6fe1119c29af09a9171fdea280e1a

https://css-tricks.com/use-webpagetest-api/#single-point-of-failure

var WebPageTest = require('WebPageTest')
var wpt = new WebPageTest('https://www.webpagetest.org/', 'your-api-key')
wpt.runTest('https://css-tricks.com', {
  connectivity: 'Cable',
  location: 'Dulles:Chrome',
  firstViewOnly: false,
  runs: 1,
  video: true
}, function processTestRequest(err, result) {
  console.log(err || result)
})


wpt.getTestStatus('160814_W7_960', function processTestStatus(err, result) {
  console.log(err || result)
})

wpt.runTest('https://css-tricks.com', {
  connectivity: 'Cable',
  location: 'Dulles:Chrome',
  firstViewOnly: false,
  runs: 1,
  pollResults: 5,
  video: true
}, function processTestResult(err, result) {
  // First view — use `repeatView` for repeat view
  console.log('Load time:', result.data.average.firstView.loadTime)
  console.log('First byte:', result.data.average.firstView.TTFB)
  console.log('Start render:', result.data.average.firstView.render)
  console.log('Speed Index:', result.data.average.firstView.SpeedIndex)
  console.log('DOM elements:', result.data.average.firstView.domElements)

  console.log('(Doc complete) Requests:', result.data.average.firstView.requestsDoc)
  console.log('(Doc complete) Bytes in:', result.data.average.firstView.bytesInDoc)

  console.log('(Fully loaded) Time:', result.data.average.firstView.fullyLoaded)
  console.log('(Fully loaded) Requests:', result.data.average.firstView.requestsFull)
  console.log('(Fully loaded) Bytes in:', result.data.average.firstView.bytesIn)

  console.log('Waterfall view:', result.data.runs[1].firstView.images.waterfall)
})