camiler

现代javascript概念解释:第2部分

camiler · 2017-05-19翻译 · 916阅读 原文链接

篇幅较长,大概说明下:在现代JS概念解释系列的第一部分中,我们学习了关于函数式编程,响应式编程,以及函数式响应编程的概念。在第二部分,我们将对作用域、闭包、Tree-shaking、组件、等等这些概念进行了解,以及当前对javascript应用谈论的话题,比如数据流变化检测进行理解。


前言

近年来,现代JavaScript出现大规模的扩散,而且并没有放缓的迹象。在JS博客和文档中出现的许多概念对很多前端开发人员来说仍然很陌生。在这一系列的文章中,我们将学习当下前端编程领域中一些中高级概念,并探讨它们如何应用于现代JavaScript,JS框架和JS应用程序。


概念

在本文中,我们将介绍理解现代JavaScript和JS应用程序至关重要的概念,包括作用域和闭包,数据流,变化检测,组件,编译等

你可以在这里直接转到每个概念那一节,或者继续阅读,以便顺序了解它们。


作用域(全局,局部,词法)和闭包

解释下闭包吧”是一个臭名昭著的JavaScript技术面试问题。实际上,很多熟练的JS开发人员也难以解释,即使在概念上理解(甚至使用过)闭包。我们再来回顾下,讨论看看回答这个问题所需的概念。

作用域

为了领会闭包,我们首先要理解作用域。作用域简单讲就是代码的上下文:可以访问变量和函数。

以下示例演示了两种作用域,全局作用域和局部作用域

// Global scope
var globalVar = 'Hello, ';
console.log(localVar); // Uncaught ReferenceError: localVar is not defined

someFunction() {
  // Local scope
  var localVar = 'World!';
  console.log(globalVar + localVar); // 'Hello, World!'
}

所有对象都可以访问全局作用域。如果我们打开一个空的.js文件并输入var globalVar,那么这个变量可以被我们创建的任何其他对象所访问。如果我们在浏览器中执行这个文件,globalVar的函数作用域就是window

注意:如果我们在声明一个新变量时不用var,它将被放置在全局作用域内,无论它位于代码中哪个位置。你以前可能遇到过这个问题(或许是意外碰到过)。

someFunction函数创建自己的局部作用域。它还继承了对全局作用域的访问。在someFunction中,我们可以随意的使用globalVar。但是,全局作用域无法访问嵌套上下文,比如someFunction的局部作用域。如果我们尝试在全局作用域打印localVar,我们会得到一个错误,因为localVar未在全局作用域内定义。

简而言之,嵌套函数有其自己的作用域。在另一个函数中声明的函数也可以访问其父函数的作用域。这就叫作用域链

词法作用域(或者说静态作用域)是指每个被嵌套的函数可以访问包含它的那个函数的作用域。

思考这个例子:

// Lexical scope and scope chain
var a = 1;

function outerFunc() {
  var b = 2;
  console.log(a + b);

  function middleFunc() {
    var c = 3;
    console.log(a + b + c);

    function innerFunc() {
      var d = 4;
      console.log(a + b + c + d);
    }

    innerFunc(); // logs 10 (1 + 2 + 3 + 4)
  }

  middleFunc(); // logs 6 (1 + 2 + 3)
}

outerFunc(); // logs 3 (1 + 2)

这段代码运行在JSFiddle:JS Scope(请打开浏览器控制台查看结果)。innerFunc是最内层的函数。它声明在middleFunc中,而middleFunc又在outerFunc中声明。

innerFunc函数可以访问在其所有父作用域中声明的变量。它的作用域链允许访问:

  • 来自全局作用域a
  • 来自outerFuncb
  • 来自middleFuncc,以及
  • 来自innerFunc其自身局部作用域的d

这只针对于向下嵌套的函数,向上的行不通。例如,在innerFunc中声明的局部作用域变量d,就不能被middleFuncouterFunc或全局作用域访问。

闭包

现代JavaScript概念术语的第一部分中,我们了解到高阶函数和函数作为第一类对象。不太熟悉的话,请花点时间复习下高阶函数部分。

现在让我们回顾一下我们在第1部分中看到的以下高阶函数示例:

// Higher-order function
function whenMeetingJohn() {
  return function() {
    alert('Hi!');
  }
}
var atLunchToday = whenMeetingJohn();

atLunchToday(); // alerts "Hi!"

这是一个函数,它返回的是另一个函数。我们更新下这个例子,在whenMeetingJohn的局部作用域中添加一个行参(salutation)和一个greeting变量。我们将之前返回的匿名函数命名为alertGreeting,以便后续更方便地进行引用:

// Closures
function whenMeetingJohn(salutation) {
  var greeting = salutation + ', John!';

  function alertGreeting() {
    alert(greeting);
  }
  return alertGreeting;
}
var atLunchToday = whenMeetingJohn('Hi');

atLunchToday(); // alerts "Hi, John!"
whenMeetingJohn('Whassup')(); // alerts "Whassup, John!"

代码运行在JSFiddle:JS闭包

当外部函数(whenMeetingJohn)中声明的内部函数(alertGreeting)可以引用来自外部函数的局部作用域中的变量时(如greeting变量),就形成了闭包

“闭包”指的是那些在函数被声明时其中的函数和词法环境(也就是说闭包被创建时,当前作用域中的任何局部变量)。

在执行LunchToday()时,会弹出,在调用时传递的参数(这个例子中是'Hi')和alertGreeting的词法环境中可访问的greeting变量。

注意:我们也可以不通过分配变量的形式调用,直接调用返回函数(alertGreeting)。像这样:whenMeetingJohn('Whassup')()

希望可以通过这个简单的例子看清楚闭包中的值。我们可以通过一些不同的salutations(称呼)参数得到不同的输出。每次调用,就创建了一个在作用域中可访问的特定salutation数据的闭包。

另一个展示闭包的常见示例是使用简单的加法表达式:

// Closures
function addCreator(x) {
  return function(y) {
    alert(x + y);
  }
}
var add1 = addCreator(1);
var add5 = addCreator(5);

add1(2); // alerts 3
add5(2); // alerts 7

代码运行在JSFiddle:JS 闭包 - 加法

add1add5都是针对x参数存放着不同值的词法环境的闭包。这些值被每个闭包的词法环境“封闭”保护起来。我们可以使用addCreator(x)工厂函数来创建任意多的add_函数。

作用域和闭包补充说明

作用域是许多JS开发人员早期学习的东西,但可能没有必要用言语或具体的例子来解释。理解作用域对编写出优秀的JavaScript代码至关重要。

注意:关于作用域还有很多知识,不止我们这里讲到的。有一些很棒的资源可以参考以求达到更深入的理解,特别是关于this关键字。更多链接,请参阅下文。

闭包将数据与一个函数在其声明时所在的词法环境关联起来。

要了解更多有关作用域和闭包的知识(以及this),请查看以下资源:


单向数据流和双向数据绑定

随着JavaScript框架和单页面应用程序(SPA)的蓬勃发展,理解数据流/数据绑定等概念,以及我们使用的工具如何管理数据,这对JS开发人员至关重要。

单向数据流

具有单向数据流的应用程序或框架是使用模型来作为单一的真实来源。React就是被广泛认可的单向数据流(或单向数据绑定)的模范。来自UI的消息以事件的形式,通知模型进行更新。

看看下面的React示例:

// One-way data flow with React
class OneWay extends React.Component {
  constructor() {
    super();
    this.handleChange = this.handleChange.bind(this);
    // set initial this.state.text to an empty string
    this.state = {
      text: ''
    };
  }
  handleChange(e) {
    // get new input value from the event and update state
    this.setState({
      text: e.target.value
    });
  }
  render() {
    return (
      <div>
        <input type="text" onChange={this.handleChange} />
        <p>Text: {this.state.text}</p>
      </div>
    );
  }
}

代码访问:JSFiddle:React单向数据流

我们可以看到state对象模型是在constructor函数中建立的。this.state.text的初始值是一个空字符串。在render()函数中,我们向input元素中添加一个onChange句柄。通过这个句柄函数进行setState(),通过input输入框不断输入的新值,发送state对象模型来更新text属性。

数据沿着一个方向流动:由模型向下。UI输入不能直接访问模型。如果要想更新状态以响应UI的更改,那么input输入框必须发送携带有效载荷的消息。UI可以影响模型的唯一方法就是通过事件以及setState( )方法。UI永远不会自动更新模型。

注意:为了反应从模型到UI的更改,React会创建一个新的虚拟DOM,并比较更新前后的虚拟DOM。只有不同之处才会被渲染到真实的DOM上。我们将在变更检测那部分中详细介绍这一块。

双向数据绑定

双向数据绑定中,数据在两个方向上流动。这也就是说JS可以更新模型,UI也可以。AngularJS就是双向数据绑定的一个常见例子。

注意:根据Angular的产品指南,在本文中,AngularJS特指该框架的1.x版本,而Angular指的是2.x及更高版本。

我们用AngularJS双向数据绑定的方式来实现上面同一个例子:

// AngularJS two-way data binding
// script.js
(function() {
  angular
    .module('myApp', [])
    .controller('MyCtrl', function($scope) {
      // set initial $scope.text to an empty string
      $scope.text = '';
      // watch $scope.text for changes
      $scope.$watch('text', function(newVal, oldVal) {
        console.log(Old value: ${oldVal}. New value: ${newVal});
      });
    });
}());

<!-- AngularJS two-way data binding -->
<!-- index.html -->
<body ng-app="myApp">
  <div ng-controller="MyCtrl">
    <input type="text" ng-model="text" />
    <p>Text: {{text}}</p>
  </div>
</body>

代码实现 Plunker:AngularJS双向绑定

在控制器中,我们设置了$scope.text模型。在模板中,通过使用ng-model ="text"将模型与<input>标签关联起来。当我们更改UI中的input输入值时,控制器中的模型也会被更新。我们可以在$watct()中看到日志。

注意:在控制器中使用$watch()是有争议的用法。我们在这里用只是为了举例说明。在你自己的AngularJS应用程序中,可以考虑在控制器中使用$watch()的替代方法(比如事件),如果你确实要使用$watch(),请在$onDestroy中注销监听

这就是AngularJS中的双向绑定。如你所见,我们没有设置任何事件或处理函数来明确地告诉控制器模型在UI中被更新了。绑定在模板中的text数据会自动通过监听器将更改的数据显示到模型。我们也可以使用$watch()方法来监听模型。通常监听应该在服务或指令link函数中完成,而不是在控制器中。

注意:AngularJS使用我们称之为摘要循环(脏检查)来比较当前值和先前值。您可以在变更检测部分阅读更多关于AngularJS中的脏检查。

题外话:Angular中的双向数据绑定

等等!Angular(v2 +)有个概念叫“banana-in-a-box”,就是[(ngModel)],对吧?从表面上看,这或许看起来是自动双向数据绑定的一种延续。但情况并非如此。Angular的双向绑定[()]语法只是简化了模板中的属性和事件绑定,并且ngModel指令提供了一个ngModelChange事件。要了解更多信息,请参阅这篇关于Angular双向绑定的文章。

以下内容在功能上和上面代码等同,只是为了示范ngModel指令的用法:

// ngModel directive: two-way binding syntax
<input [(ngModel)]="text" />
<p>{{text}}</p>

// ngModel property and event binding
<input [ngModel]="text" (ngModelChange)="text=$event" />
<p>{{text}}</p>

Angular双向绑定的文档彻底覆盖了这种语法。

数据流和绑定的补充说明

当前许多JavaScript框架和库都运用的是单向数据流(ReactAngularInfernoRedux等)。为什么?单向数据流,对于数据在应用程序中如何流动这个问题,倡导的是干净整洁的架构。应用程序的状态也更容易管理,更新数据更可预测,性能也更好。

虽然,早在2009年,自动双向数据绑定已经就是AngularJS最受欢迎的演示之一,但Angular将这一特点抛弃了。开始,一些Angular开发人员对此很遗憾,但最终,许多人发现遗弃之后带来的性能提升和更强大的控制超过了自动双向绑定带来的便利。

正如我们在上面的React示例中看到的,重要的是要记住,单向数据流并不意味着根据UI的变化来更新存储是很难的一件事。它只意味着这些更新需要谨慎地根据特定的指令完成。少了点自动更新的神奇之处,但多了些管理的方便。

注意:一般说来,当开发人员在单向数据流(如React)框架中提到“实现双向数据绑定”时,他们是指UI的更改会告知应该被更新的状态这样的必要步骤。他们并不是想找到一种方式来实现自动双向绑定。

要了解有关单向数据流和双向数据绑定的更多信息,请查看以下资源:


JS框架中的变更检测:脏检查,访问器,虚拟DOM

变更检测对于任何动态JavaScript单页面应用(SPA)都很重要。当用户更新某些内容时,应用程序必须有一种方法来检测变更并对该变更做出对应的响应。因此,某种变更检测对于SPA框架至关重要。

让我们在一个相对高的层次上,来探讨下目前流行的JavaScript框架中使用的几种变更检测方法。

脏检查

尽管Angular已经发布,但目前在众多生产或开发的应用程序中,使用AngularJS仍然居多。AngularJS使用众所周知的摘要循环来检测应用程序中的变更。从底层来讲,摘要循环就是脏检查。这是什么意思呢?

脏检查是指为了检查变化的值,对视图中所有运行的模型进行深层比较。AngularJS的摘要循环为每一个添加到$scope并绑定在UI中的属性增加了一个监听器。当我们想使用$scope.$watch()观察值的更改时,又会添加另一个监听器。

AngularJS和Angular的创建者Miško Hevery说:“AngularJS中,记住改变的值并将其与旧值比较。这是脏检查的基础。如果值有变化,那么它会触发变更事件。"

摘要循环就是一个循环。AngularJS通过运行其监听器列表和检查列表来查看是否有任何被监视的$scope变量变化了(也就是“脏了”)。如果这个变量没有改变,接着转到下一个被监听的变量。如果找到一个脏数据,它会记住它的新值并重新进入循环。直到整个监听列表中再没有检测到新的变更时,DOM就被更新。

脏检查的主要优点就在于它的简单和可预测:没有扩展的对象,没有API涉及。然而,这也很低效。因为只要有任何变化,就会触发摘要循环。因此,在AngularJS中创建监听器时要格外注意。每当$scope属性绑定到UI时,都会添加一个监听器。每次执行$watch()时,又会添加另一个监听器。许多指令也会添加监听器,对于作用域变量,过滤器和中继器也是如此。

虽然在一个简单的应用程序中脏检查足够快,但轻而易举地,我们就能看到,它在一个复杂的实现中会如何失控。这也就是为什么会有类似11种提高AngularJS性能的秘诀:1.最小化或避免监听器、还有加快AngularJS的$digest循环这样的文章。

注意:Angular(v2 +)不再使用脏检查

存取器

EmberBackbone使用数据访问器(getters和setters)来进行变更检测。Ember object继承自Ember的API,并具有用于更新数据绑定模型必要的get( )set( )方法。这样就可以实现UI和数据模型之间的绑定,然后Ember就会明确地知道什么改变了。反过来,只有修改后的数据才会触发变更事件从而更新应用程序。

注意:在Backbone中,这是由带有get( )set( )方法的Backbone模型完成的。

这种方法直截了当,并强制应用开发者要非常慎重的对待数据绑定。然而,从另一方面来讲,这样偶尔会导致混乱,因为Ember.Objects只是在数据绑定到模板时才会使用。如果没有UI数据绑定,更新就不必使用Ember.Objects。这种混合的方法可能导致开发人员因为忘记进行gettersetter没有更新数据而抓狂。

虚拟DOM

React(和Inferno.js)使用虚拟DOM来实现变更检测。React不会具体检测每个变化。相反,当发生更改时,使用虚拟DOM来区分UI的先前状态和新状态。React通过使用setState( )方法通知这些更改,这会触发render( )方法来执行diff。

虚拟DOM(有时称为V-DOM)是一种代表真实DOM树的JavaScript数据模型。当生成虚拟DOM时,浏览器不会渲染任何内容。将旧模型和新模型进行比较,一旦React确定了虚拟DOM的变更的部分,那么就只有那部分会在真实DOM中进行修补。

变更检测补充

JavaScript框架管理变更检测的方法有很多,还有很多没有讲到。他们都有优点和缺点,但现代的趋势是偏向更加谨慎,少点自动化的方法,所以许多框架底层运用的是观察者模式。

要了解有关JS框架中变更检测的更多信息,请查看以下资源:


Web组件

Web组件是基于Web平台API封装的,可复用的小部件。它们由四个标准组成:

Web组件使得我们可以构建并导入一种自定义元素,这种元素将JS行为与模板自动关联,利用影子DOM来提供CSS范围和DOM封装。

Web组件由一组web 平台 APIs组成。有一系列库(如Polymer)还有兼容插件(比如webcomponents.js)可以兼容当前的浏览器和未来的web API。

假设我们要创建一个简单的Web组件(my-component),显示一些静态文本。我们希望使用HTML属性使组件更改其文本颜色并打印日志到控制台。要在我们的网站或应用程序中显示“自定义元素”,我们可能得像这样导入并使用它:

<!-- index.html -->
<html>
  <head>
    ...
    <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.24/webcomponents.min.js"></script>
    <link rel="import" href="my-web-cmpnt.html">
  </head>
  <body>
    <my-web-cmpnt color="red" log="Hello"></my-web-cmpnt>
    ...

要使用影子DOM创建my-component web组件,我们的my-component.html看起来像这样:

<!-- my-component.html -->
<template>
  <style>
    .my-component {
      display: block;
      padding: 20px;
    }
  </style>

  <div class="my-component">
    <p>This is a custom element!</p>
  </div>
</template>

<script>
  (function(window, document, undefined) {
    var doc = document;
    // my-component document
    var self = (doc._currentScript || doc.currentScript).ownerDocument;
    var template = self.querySelector('template').content;

    // ShadowCSS shim, if needed
    if (window.ShadowDOMPolyfill) {
      WebComponents.ShadowCSS.shimStyling(template, 'my-component');
    }

    class MyComponent extends HTMLElement {
      constructor() {
        super();
      }
      connectedCallback() {
        // get attributes
        var color = this.getAttribute('color');
        var log = this.getAttribute('log');
        // utilize shadow DOM
        var shadowRoot = this.attachShadow({mode:'open'});  
        var clone = doc.importNode(template, true);
        var myComponent;

        shadowRoot.appendChild(clone);
        myComponent = shadowRoot.querySelector('.my-component');

        // style with color and output log
        myComponent.style.color = color;
        console.log(log);
      }
    }
    window.customElements.define('my-component', MyComponent);
  }(window, document));
</script>

查看运行源码Plunker:Web组件

<template>定义元素的CSS样式和HTML标签。那么,为了利用影子DOM和JS的功能,我们在my-component.html文件中,闭合标签</template>之后加上一个<script>标签,在这里实现我们所需的自定义元素JS功能,获取元素的属性,使用它们来改变文本颜色并打印信息。

当使用Chrome开发者工具进行审查时,我们的组件就是这样:

modern js glossary: custom web component in Chrome inspector

想要更深入的教程,请查看Web组件:如何制作自定义组件

Web组件为浏览器带来强大的框架式功能,虽然规范和支持仍在最后定稿中,但这些概念启发了诸如Angular(最先尝试使用Web API 的Web组件,但以自己的实现结束)之类的框架。许多JS框架都(AngularReactEmberVue)利用了与Web组件API不同程度上的相似构成概念。

Web组件补充

Web组件允许我们使用JS创建并使用自定义HTML元素。还可以利用影子DOM来提供CSS范围和DOM封装。Web组件自己本身并不能代替SPA框架的所有功能。但是,它们的核心概念在今天使用的许多框架中占据着重要地位,在前端领域,它们的未来将为日益增长的资源社区提供许多机会。

要了解有关Web组件的更多信息,请查看以下资源:


智能和笨拙组件

正如我们上面讨论的,一些现代的JS框架严重依赖于组件。这就引出了组件架构和组件通信的概念。在一些例子中(如ReactAngular),组件架构使用智能和笨拙的组件。它们也被称为“容器”(智能)和“展示”(笨拙)组件。

智能组件

也被称为容器组件,智能组件可以管理和应用程序的状态和数据的交互。它们处理业务逻辑,响应子组件中触发的事件(通常是来自笨拙组件)。

笨拙组件

也称为展示组件,笨拙组件依赖于其父组件提供的输入,对应用程序状态是不知道的。它们有时被认为是纯粹的,并且是模块化和可复用的。它们可以在响应事件时与父组件通信,但它们自己并不会处理事件。

React中的展示和容器组件

Dan AbramovReduxCreate React AppReact Hot Loader,还有很多其他框架的创始人。他最初写过关于展示组件和容器组件及其在React中的含义,尤其是当与Flux或Redux这样的状态管理一起使用时。简而言之,这些概念可概括如下:

展示(又名笨拙)组件:

  • 专注于“事情如何展现 ”
  • 允许通过this.props.children控制
  • 不依赖于应用程序的其余部分(即,没有Flux或Redux的actions或stores)
  • 只接收数据;不会加载或者改变数据
  • 通常是函数式组件(也会有例外)

容器(又名智能)组件:

  • 专注于“事情如何运行
  • 向其他组件提供数据和行为
  • 通常没有或很少有DOM标签,没有样式
  • 通常是有状态的数据源
  • 通常由高阶组件生成

查看他的文章获取更多的细节和解释。

示例:Angular中的智能组件和笨拙组件

针对不涉及状态容器的快速示例,我们来看一些用Angular实现的简单的智能和笨拙组件。

假设我们想要一个madlib风格的功能,当选择hungry,tired,或者debugging时,就对应的显示为什么而道歉。完成后,在浏览器中如下图:

modern js glossary: Angular smart and dumb components

智能(容器)组件像这样:

// app/smart.component.ts
import { Component } from '@angular/core';
import { DumbComponent } from './dumb.component';

@Component({
  selector: 'my-smart-cmpnt',
  template: 
    <h1>I'm sorry for what I said when I was {{selectedOption}}.</h1>

    <my-dumb-cmpnt
      [options]="optionsArr"
      (changedOption)="onOptionChange($event)"></my-dumb-cmpnt>
  
})
export class SmartComponent { 
  optionsArr = ['hungry', 'tired', 'debugging'];
  selectedOption = '______';

  onOptionChange(e: string) {
    this.selectedOption = e;
  }
}

该组件管理此功能所需的所有数据。它提供了madlib(optionsArr)的选项,然后在用户选择选项(onOptionChange())时处理。这里面存储了selectedOption,传递可能的选项给展示组件,然后当展示组件里触发changedOption事件时再设置选中的选项。

// app/dumb.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'my-dumb-cmpnt',
  template: 
    <div *ngFor="let option of options">
      <button (click)="select(option)">{{option}}</button>
    </div>
  
})
export class DumbComponent { 
  @Input() options: Array;
  @Output() changedOption = new EventEmitter();

  select(option) {
    this.changedOption.emit(option);
  }
}

反过来,展示组件接受选项数组作为输入,再迭代每个项目用来创建选择按钮。单击某个选项时,changedOption事件就会被触发并且携带上选中的信息。然后,父容器组件处理此事件并设置其selectedOption,更新状态显示到UI。

查看运行源码Angular的智能和笨拙组件

智能和笨拙组件补充

智能(容器)组件管理数据,实现业务逻辑和事件处理。笨拙(展示)组件接受输入并触发由父容器组件处理的事件。笨拙组件是模块化的,并且可以在整个应用程序中重用,因为它们是无状态的。当使用像Redux或者ngrx/store这样的状态容器时,只有智能组件才会分发actions或与store进行交互。

要了解有关智能组件和笨拙组件的更多信息,请查看以下资源:


JIT(Just-In-Time)编译

Just-In-time(JIT)编译是指在编程语言中编写的代码在运行时(在程序或应用执行期间)翻译成机器代码的过程。在运行时,某些动态信息是可以获得的,如类型识别。JIT编译器监视检测多次运行的函数或循环代码 - 该代码被认为是“warm”。接着这些代码段就会被编译。如果它们相当普遍地被执行(认为是“hot”),JIT将会优化它们,并存储经过优化的编译代码以供执行。

当编译器优化热代码时,它会根据以前执行的一致性对其类型和模型做出假设。在任何迭代中,如果这些假设被证明不准确,则优化的代码将被丢弃。

浏览器使用JIT编译来运行JavaScript。在现代JavaScript框架环境中,像Angular框架中的编译器可以使用JIT将本地开发的TypeScript和Angular代码编译为JS,并在运行时分别编译每个文件翻译成机器代码。这样有几个优点,例如在监听代码更改时无需重新构建项目,以及可以更快的初始构建。

JIT编译补充

浏览器使用JIT编译在运行时编译JavaScript,并通过像Angular这样的框架来提供快速的本地开发体验。

要了解有关JIT编译的更多信息,请查看以下资源:


AOT(Ahead-Of-Time)编译

Ahead-Of-Time(AOT)编译是指,在编程语言中编写的代码在运行之前(与在运行时对立)翻译成机器代码的过程。这样做减少了运行时开销,并将所有文件编译在一起,而不是单独编译。

对于JavaScript应用程序来说,例如Angular使用的AOT,这就意味着可以嵌入HTML和CSS,并且能在没有编译器的情况下进行部署,从而节省相当大的空间资源。由于是预编译过的,这样浏览器也可以立即呈现应用程序。

对于生产环境构建,AOT有如下好处:

  • 更少的异步请求:模板和样式内联到JS中
  • 更小的下载量:如果应用程序已经编译,编译器就无需再下载了
  • 更早的检测模板错误:编译器在构建时检测绑定错误,而不是在运行时
  • 更好的安全性:评测已经完成,这降低了脚本注入的几率

AOT也可以用tree shaking。在浏览器中,使用AOT编译的应用程序由于预编译提升了加载和引导的总时间。由于需要解析的代码较少,初始渲染时间也会减少。

然而,AOT比JIT需要更长的初始构建时间,如果有任何更改,则需要对整个应用程序进行重新编译。

AOT编译补充

AOT编译在运行之前进行,并将所有文件绑定在一起。与JIT相比,它允许tree shaking,具有更小的下载量和更高的安全性。

要了解有关AOT编译的更多信息,请查看以下资源:


Tree Shaking

Tree Shaking是一个JavaScript模块捆绑术语,它指的是对所有导入代码的静态分析,以及对任何没有实用代码的排除机制。

打个更直观的比方,假设有一棵活着的树。摇晃这棵树,会有一些已死的树叶落下来,留下那些正积极地进行着光合作用的树叶。树木摇晃背后的概念就是保存要用的代码:一开始就包含需要的部分,而不是最后删除不必要的部分(无用代码消除)。

Tree Shaking依赖于ES2015模块的importexportimportexport声明构成了一个应用的静态模块结构。当模块被打包部署时,Tree shaker分析静态模块结构,以便排除无用的导出,减少最终包的大小。

ES2015允许我们指定明确的导入。例如,我们可以准确地只导入我们想要的内容,而不是导入整个RxJS库:

import { BehaviorSubject } from 'rxjs/BehaviorSubject';

这与CommonJS或AMD使用的动态require语句不同。Tree shaking使用这一原则来走查依赖关系图,并排除不需要的东西,从而减少部署打包的大小。

tree shaking的原理并不新鲜,但最近由于rollup.js这个模块集合框架流行起来。Webpack 2也运用了Tree shaking 的原理。Tree shaking的概念也普遍存在于Angular AOT编译中。

Tree Shaking补充

Tree shaking是在模块打包中对JavaScript有用代码进行包含的一种技术术语,使用ES2015静态导入和导出从而可以从更小级别上“抖出”不需要的依赖关系。

要了解更多有关Tree shaking的信息,请查看以下资源:


题外话:学习和实现Auth0的身份验证

在应用程序中实现的最复杂的功能之一就是用户身份验证和身份管理。认证和身份的安全性,其本身而言就是一个完整的术语

Auth0的登录页

如果您需要实现一个健壮的,高度可定制的身份和访问管理系统,并且可以很容易的对接JavaScript SPA和Node API,Auth0可以帮到您。Auth0提供单页应用程序快速入门指南网页SDK,大量文档以及其中博客板块的文章和教程。您可以注册一个免费的Auth0帐户开始使用。


总结

随着JavaScript单页面应用程序框架和基于组件的范例的迅速增长,理解和JS相关的作用域,数据流,组件,编译和打包这些概念就相当重要了。

以此术语表为起点,可以开始学习这些概念和程序范例,以此巩固加深JavaScript专业知识。如果关于这些话题还有什么不清楚的,请参考每个部分的链接,获得更多的资源。还可以查看现代JS概念术语表的第一部分,了解那些对于理解函数式编程,响应式编程和函数式响应编程所必需的概念。

相关文章