整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript:高级-解析与执行过程

JavaScript:高级-解析与执行过程

JavaScript是一种描述型的脚本语言,是一种解析语言,由浏览器动态解析,不同类型的浏览器、不同版本的浏览器对于JavaScript的解析有着微小的差别,不同浏览器的JavaScript解析引擎效率也有差异。

JavaScript的执行过程分为两大部分:

  • 第一部分:解析过程,也称为预编译期。主要工作就是对于JavaScript的代码中声明的所有变量和函数进行预处理。需要注意的是,在此进行处理的仅是声明函数,而对于变量的处理仅是声明,并开辟出一块内存空间,不进行赋值操作;
  • 第二部分:执行过程。在执行过程中,浏览器的JavaScript引擎对于每个代码块进行顺序执行。如果有外部引用的JavaScript,且JavaScript相互关联,此时就要注意,不同JavaScript的引入顺序。如果声明代码块在调用代码块后调用,则将不会达到预期的效果。

总的来说,JavaScript的执行分为两部分:解析过程和执行过程。解析时按照代码块,一段一段进行解析,执行时按照代码块顺序逐行执行,解析一个代码块,执行一个代码块。

因为是解释型语言,所以JavaScript如果在解析过程中有错误,则不会提示,也可以理解为JavaScript不会出现编译错误,但如果出现了运行时错误,出现错误以下的所有JavaScript代码将不会继续执行。

全局预处理阶段

预处理:创建一个词法环境(LexicalEnvironment,简写为LE),扫描JavaScript中用声明的方式声明的函数,用var定义的变量并将它们加到预处理阶段的词法环境中去。

预处理阶段先读取代码块,不是一行一行的解析执行定义的方法和用var定义的变量,会放到一个(不同的环境,会有对应的词法环境)词法环境环境中。

 var a=1;    // 用var定义的变量,已赋值
 var b;        // 用var定义的变量,未赋值
 c=3;        // 未定义的变量,直接赋值
 
 // 用声明的方式声明的函数
 function d(){
   console.log('hello');
 }
 
 // 函数表达式
 var e=function() {
   console.log('world');
 }

词法环境:

 LE {  // 此时的LE相当于window
   a: undefined
   b: undefined
   d: 函数引用
   e: undefined
 }

预处理的函数必须是JavaScript中用声明的方式声明的函数,不是函数表达式。

示例:

 d();
 e();
 
 // 用声明的方式声明的函数
 function d(){
   console.log('hello');
 }
 
 // 函数表达式
 var e=function() {
   console.log('world');
 }

执行结果:

 hello
 
 TypeError: e is not a function

命名冲突

函数优先原则:在既有函数声明又有变量声明的时候,函数声明的权重高于变量声明,所以最终结果往往是指向函数的引用。

示例 1:

 console.log(f);
 
 var f=1;
 
 function f() {
   console.log('func');
 }

结果:

 [Function: f]

示例 2:

 console.log(f);
 
 function f() {
   console.log('func');
 }
 
 var f=1;

结果:

 [Function: f]

执行阶段

 console.log(a); //  undefined
 console.log(b); //  TypeError: b is not a function
 console.log(c); //  [Function: f]
 console.log(d); //  undefined
 
 var a=1;
 b=2;
 console.log(b); // 2
 function c(){
   console.log('c');
 }
 
 var d=function(){
   console.log('d');
 }
 
 console.log(d); //  [Function: f]
 

词法环境:

 LE {
   a: undefined
   c: [Function: f]
   d: undefined
 }

函数冲突原则

  1. 处理函数声明有冲突时,会覆盖;
  2. 处理变量声明有冲突时,会忽略。以传入参数的值为准。

预处理阶段传输参数值一一对应

 function func(a, b) {
   console.log(a);
   console.log(b);
   
   var b=100;
   function a{}
 }
 
 func(1, 2);

词法环境:

 LE {
   b: 2
   a: 指向函数的引用
   arguments: 2  // 调用函数时实际调用的参数个数
 }

运行结果:

 [Function: f]
 2

没有用var声明的变量,会变成最外部LE的成员,即全局变量:

用代码分割、延迟加载、使用 Web Workers、压缩文件和异步加载等技术提升您的 JavaScript 技能。

译自 How To Master JavaScript Performance Optimization,作者 Alexander T Williams。

JavaScript 是现代 Web 应用程序的基石,为从动态内容到交互式功能的一切提供支持。然而,随着应用程序变得越来越复杂,确保 JavaScript 能够高效运行变得至关重要。

随着用户对更快、更具响应性的应用程序的需求不断增长,开发人员必须优先考虑 JavaScript 优化以满足这些期望。从减少加载时间到提高交互性,优化您的 JavaScript 可以显著提高 Web 应用程序的整体性能。

为什么优化 JavaScript 的性能

正确理解网站的性能是优化 JavaScript 代码的第一步。

考虑到这一点,衡量您的网站或应用程序的性能至关重要,因为它可以帮助您识别影响下载时间、渲染速度和整体用户体验的瓶颈。

如果没有对性能进行适当的衡量,您可能会浪费时间应用优化,而这些优化并不能解决您的网站所面临的实际问题。

分析性能数据

有几种工具可以帮助您有效地衡量性能。内置的浏览器工具,例如 Chrome DevTools,提供关于网络活动、加载时间和 CPU 使用率的全面而有价值的见解。

收集完性能数据后,下一步是确定哪些优化是必要的。

这些工具可以让您看到页面中哪些部分加载时间最长,以及哪些脚本可能会减慢网站速度。除此之外,性能 API 还可以提供更复杂的数据,用于深入分析。

收集完性能数据后,下一步是确定哪些优化是必要的。并非每种技术都适合每个项目,因此根据您网站的具体需求进行优先排序非常重要。

例如,如果您的分析表明事件处理程序会导致延迟,您可以专注于改进事件管理。类似地,如果大型 JavaScript 文件会减慢加载时间,缩小和异步加载可能是正确的解决方案。

此外,它还可以帮助您遵守 GDPR,或与您的网站或应用程序相关的欧盟、美国或其他地方的任何数据保护法规。优化您的 JavaScript 有助于提高性能,同时确保您的数据处理实践符合标准。

正确管理的代码可以帮助最大限度地减少不必要数据的收集,从而简化尝试遵守和遵循重要监管要求的过程。

优化 JavaScript 性能的挑战

我们都经历过:如果您的代码没有得到妥善管理,JavaScript 有时会成为一个真正的头痛问题。

您可能遇到的一些常见问题包括质量较差的事件处理,这会导致深层调用堆栈和更慢的性能。无序的代码是另一个大问题,会导致资源分配效率低下,并使浏览器更难快速执行脚本。

代码拆分允许您将 JavaScript 代码分解成更小、更易于管理的块。

然后是过度依赖的问题,这会减慢应用程序的速度,通常会显著减慢速度,尤其是对于带宽有限的移动用户而言——而且不要忘记,低效的迭代会不必要地拖延处理时间。

理解和实现代码拆分

代码拆分允许您将 JavaScript 代码分解成更小、更易于管理的块——这在您的应用程序变得越来越复杂时至关重要,有助于减少加载时间并提高用户的初始渲染速度。

那么,如何进行代码拆分呢?一种常用的方法是使用动态导入,它允许您仅在需要时加载 JavaScript 模块,而不是一次性将整个应用程序加载到用户身上。这就像只为周末旅行打包必需品,而不是打包整个衣橱。

根据最近的调查统计,48.9% 的开发人员已采用动态导入按需加载模块,45.7% 的开发人员正在使用服务工作者 来增强离线用户体验。

同样,对于 JS 库也是如此,允许进行各种应用内操作,例如在 React 应用中查看文档,动态在实时分析仪表板中渲染图表,或加载交互式地图以用于基于位置的服务。然后是 webpack,一个工具,一旦你掌握了它,就会感觉有点像魔法;它可以自动将你的代码拆分成更小的块,按需加载它们。

如何实现代码拆分

  1. 动态导入: 使用import() 函数在需要时加载模块。例如:
import('./module.js').then(module=> {
  module.doSomething();
});
  1. Webpack 配置: 配置 webpack 使用SplitChunksPlugin 和optimization.splitChunksoptions 自动将代码拆分成更小的块。
  2. React.lazy: 在 React 应用中,使用React.lazy 进行组件级代码拆分:
const MyComponent=React.lazy(()=> import('./MyComponent'));

实现延迟加载以提高性能

延迟加载是一种很棒的技术,可以通过延迟加载非必要资源来提高 Web 应用的性能,直到它们真正需要时才加载。

简而言之,延迟加载允许这些元素仅在进入用户的视野时加载,而不是让用户等待每个图像、视频或媒体文件预先加载。

延迟加载最常见的用例包括图像、视频和其他媒体密集型内容等元素。使用延迟加载可以大幅减少初始加载时间,从而增强网站或应用的整体用户体验。

实现延迟加载的一种流行方法是通过 Intersection Observer API。这个特定的 API 允许你检测元素何时进入或退出视窗,因此你可以在内容即将对用户可见时才加载它。它效率高且设置起来相对容易。

如何实现延迟加载

  1. Intersection Observer API: 检测元素何时进入视窗并动态加载内容:
const observer=new IntersectionObserver((entries)=> {
  entries.forEach(entry=> {
    if (entry.isIntersecting) {
      loadImage(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img=> observer.observe(img));
  1. 对于 React 开发人员,React.lazy 函数是延迟加载组件的强大工具。使用React.lazy,你可以在组件级别拆分代码,以便仅在需要时加载应用的必要部分。

利用 Web Workers 卸载繁重的计算

Web Workers 是现代 Web 开发中的一项强大功能,旨在帮助处理繁重的计算,而不会减慢用户界面。

Web Workers 从主线程卸载密集型任务,通过在后台线程中运行脚本,提供流畅且响应迅速的用户体验。

Web Workers 通过启用并行执行来显著提高性能;因此,当主线程处理用户交互和渲染时,Web Workers 负责后台的资源密集型操作,例如数据处理和计算。这可以防止 UI 由于长时间运行的脚本而变得无响应。

使用 Web Workers 的一些更实际的示例包括卸载基本数据处理任务。例如,当处理需要排序、过滤或复杂计算的大型数据集时,Web Worker 可以管理这些操作,而不会冻结主 UI 线程。

如何利用 Web Workers

  1. 创建 Web Worker: 为工作者创建一个单独的 JavaScript 文件:
// worker.js
self.onmessage=(e)=> {
  const result=computeHeavyTask(e.data);
  postMessage(result);
};
  1. 使用 Web Worker: 从主线程实例化并与工作者通信:
const worker=new Worker('worker.js');

worker.onmessage=(e)=> {
  console.log('Result from worker:', e.data);
};

worker.postMessage(data);

JavaScript 优化的其他技术

优化 JavaScript 不仅仅是代码分割和延迟加载,还有其他一些技术可以显著提高应用程序的性能。

异步加载允许脚本与其他资源并行获取。

一种重要的方法是 压缩和压缩 JavaScript 文件,这涉及从代码中删除不必要的字符和空格,而不会改变其功能。像 UglifyJS 这样的工具可以帮助完成此过程,使用 gzip 或 Brotli 压缩可以进一步减小文件大小,从而加快加载时间。

另一方面,异步加载允许脚本 与其他资源并行获取,防止它们阻塞页面的渲染。HTML 中的 async 属性通常用于此目的。

使用 defer 属性延迟脚本,确保 代码在初始 HTML 解析后执行,这提高了用户与网站交互的速度。

利用 HTTP/2 和 JavaScript CDN 可以进一步提高网站或应用程序的性能。

HTTP/2 引入了多路复用等功能,允许多个请求同时通过单个连接发送,从而减少延迟。使用 内容交付网络 (CDN) 为您的 JavaScript 文件提供服务 可以保证它们从更靠近用户的位置提供服务,从而加快交付速度。

提升您的 JavaScript 水平

代码分割、延迟加载、使用 Web Workers、压缩文件和利用异步加载等技术并不完全是秘密,但开发人员并没有充分利用它们——远非如此。

每种方法都可以提高应用程序的速度和响应能力,将它们纳入开发工作流程将提供更流畅的用户体验,并使您的应用程序保持领先地位。

了执行Javascript,需要在HTML文件内以特定的方式书写JavaScript的代码,JavaScript的书写方法有多种,其执行的流程也各不相同:

1 <script>标签嵌入

此种嵌入方法无法操作<script>之后的DOM元素。因为<script>之后的DOM元素还未构造,因此在<script>标签内就无法取得位于其后的DOM元素。

2 读取外部JavaScript文件

此种嵌入方法可以指定defer、async属性。defer可以推迟执行,async可以异步执行。

3 onload嵌入

此种嵌入方法在页面读取完后再对其执行,所以可以对所有的DOM元素操作。

<body onload="alert('hello')">
window.onload=function(){alert('hello');};

当window.onload事件触发时,页面上所有的DOM、样式表、脚本、图片、flash都已经加载完成了。

//window.onload不能同时编写多个。
//以下代码无法正确执行,结果只输出第二个。
window.onload=function(){
  alert("test1");
};

window.onload=function(){
  alert("test2");
};

//$(document).ready()能同时编写多个
//结果两次都输出
$(document).ready(function(){ 
   alert("Hello World"); 
}); 
$(document).ready(function(){ 
   alert("Hello again"); 
}); 

window.onload和body中onload也有些许区别:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.9.0.js"></script>
    <script language="javascript">
        window.onload=haha;
        function haha(){console.log("window.onload");}

        if(document.addEventListener){
            function DOMContentLoaded(){
                console.log("DOMContentLoaded");
            }
            document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
        }</script>
</head>
<body onload="console.log('bodyonload');">
        <div id="div1">a</div>
</body>
</html>

在IE10和FireFox下,结果为 :

"DOMContentLoaded"
"bodyonload"

说明body中的onload会覆盖window.onload

在chrome下,结果为:

DOMContentLoaded
window.onload
bodyonload

然后,如果把javascript代码移到最下面,结果又会是什么样呢?

chrome和IE10、FireFox的结果竟然是一样的:

DOMContentLoaded
window.onload

IE 10、Fire Fox可以理解,window.on load和body中的 on load 谁在下面就是谁覆盖谁,只会执行后面的那个。

4 DOM ContentLoaded嵌入

onload方法可能需要等待时间,而本方法可以在完成HTML解析后发生的事件,减少等待时间。

在chrome、IE10和FireFox中,执行结果是:DOMContentLoaded然后才是onload的输出。所以说一般情况下,DOMContentLoaded事件要在window.onload之前执行,当DOM树构建完成的时候就会执行DOMContentLoaded事件。

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script type="text/javascript" src="jquery2.js"></script>
    <script language="javascript">
        window.onload=haha;
        function haha(){console.log(document.getElementById("div1"));}
        if(document.addEventListener){
            function DOMContentLoaded(){
                console.log("DOMContentLoaded");
            }
            document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
        }
    </script>
</head>
<body>
    <div id="div1">a</div>
</body>
</html>

如果你是个jQuery使用者,你可能会经常使用$(document).ready();或者$(function(){}),这都是使用了DOMContentLoaded事件

5 动态载入JavaScript文件

5.1 使用原生js方法

动态创建script标签,并指定script的src属性

function loadJs(url, callback) {
    var script=document.createElement('script');
    script.type="text/javascript";
    if (typeof(callback) !="undefined") {
        if (script.readyState) {
            script.onreadystatechange=function() {
                if (script.readyState=="loaded" || script.readyState=="complete") {
                    script.onreadystatechange=null;
                    callback();
                }
            }
        } else {
            script.onload=function() {
                callback();
            }
        }
    }
    script.src=url;
    document.body.appendChild(script);
}
loadJs("test.js", function() {
    alert('done');
});

还可以使用同样的原理动态加载css文件,只不过插入的的父节点是head标签。

5.2 使用document.write/writeln()方式

该种方式可以实现js文件的动态加载,原理就是在重写文档流,这种方式会导致整个页面重绘。

document.writeln("<script src=\"http://lib.sinaapp.com/js/jquery/1.6/jquery.min.js\"></script>");

需要注意的是特殊字符的转义。

5.3 使用jQuery

使用getScript(url,callback)方法实现动态加载js文件

$.getScript('test.js',function(){
    alert('done');
});

-End-