「每日一瞥 📰 」1224 ~ 0104

1,204 阅读17分钟
  • 从过去直到 React.lazy
  • 写一个没有 JSX 的 React
  • 执行上下文和执行栈
  • 公私有域和方法
  • 数组在性能方面的一个注意点

从过去直到 React.lazy

code-splitting

当我们最最开始做前端开发的时候,JavaScript 文件自然就一个个罗列在一起,通过 script 标签引入到 html 里。当然,即使在现在,我们也还是会在写一些 Demo 时使用这样的方式。

如今,我们有了如 Webpack、Parcel 等 Module bundler 来为我们更好的组织 JavaScript 文件。我们可以使用各种模块系统如 CommonJS(requiremodule.exports)或者 ES Modules(importexport)来定义文件之间的依赖。

然而,随着我们的应用越来越大,我们就会得到一个巨大的 JS bundle,而这种慢慢等待加载的体验是绝不能忍受的。因此,code-splitting 就成了一种广泛接受的做法。

下面的例子就是没有拆分过的、只会打包成一份的应用,在加载时会同步全部加载再渲染:

import Description from './Description';

function App() {
  return (
    <div>
      <h1>My Movie</h1>
      <Description />
    </div>
  );
}

现在我们来开始看看,如何让我们的 Module bundler 来懒加载我们的模块呢?

Dynamic import proposal

动态 import 提案为 ES Modules 添加了新特性,使我们可以以异步的方式定义我们的依赖关系。import 语句可以作为一个函数来调用,并返回一个 Promise,这个 Promise 会 resolve 我们想要加载的模块。使用方式只需要从上面的 ES Modules 的 import 方式略加调整:

- import Description from './Description';

+ const Description = import('./Description');

上面的用法就会告诉 Webpack 或 Parcel 我们的 Description 模块并不是立即就需要,而是可以等到加载好后再使用。并且,动态 import 就可以使得 Module bundler 将该模块打包成单独的 js 文件,而这就是所谓的 code-split。

但是还不够,这还只是开始。让我们继续往下走。

React 组件的懒加载

如果我们使用上述动态 import,我们的 App 组件就要修改成如下的方式:

const LoadDescription = () => import('./Description');

class App extends React.Component {
  state = {
    Description: null,
  };

  componentDidMount() {
    LoadDescription.then(Description => {
      this.setState({ Description: Description.default });
    });
  }

  render() {
    const { Description } = this.state;
    return (
      <div>
        <h1>My Movie</h1>
        {Description ? <Description /> : 'Loading...'}
      </div>
    );
  }
}

这样写未免就有点蛋疼了,所幸的是我们有一个非常好用的库,即 react-loadable

react-loadable 会帮我们省掉很多模板代码,改写后的效果如下:

import Loadable from 'react-loadable';

const LoadableDescription = Loadable({
  loader: () => import('./Description'),
  loading() {
    return <div>Loading...</div>;
  },
});

function App() {
  return (
    <div>
      <h1>My Movie</h1>
      <LoadableDescription />
    </div>
  );
}

这样看上去就好多了,我们就不需要再自己去管生命周期之类的事,只需要靠它来 load 我们需要的组件、指定相应的 loading 即可使用。

既然 react-loadable 已经这么好用了,我们还干嘛要用 React.lazy 呢?

Suspense

react-loadable 实际上还是有一些不足的,主要的一点就是它只作用于每一个单独组件。什么意思呢?如果你有一堆想要懒加载的组件,你需要分别为他们指定 loading 状态。当然,你可以使用一个公用的组件,这样你每个 loading 状态都可以复用,但是你仍然会看到每一个懒加载的组件各自有一个 loading。如果你在一个页面有很多懒加载的组件,那就牛逼了,你会看到一堆小菊花,这恐怕也不是什么好的体验。

说到这一缺点,在我们团队的一些项目中,**CLI 目前是在路由层面配合使用 react-router 和 react-loadable 的,一次只会 load 一个组件,因而就不存在一堆要懒加载的组件同时出现在页面上;而 loading 状态,我们也可以设计一个全局的 Spin 来使用。**总的来说,肯定是存在一些方法或替代方案来弥补或避免这些问题的。

但是,在我们目前的工程中,仍然有可以改善的点:

  • 一堆 loadable 文件;
  • react-loadable 有除懒加载以外功能的其他代码,这些可能是我们不需要的;
  • 如果我们想对更深层的子组件做懒加载,就还需要引入 loadable 文件,不优雅。

好的,让我们来看看 React.lazy 可以做到什么吧!

与 react-loadable 不同的是,我们不需要在每一个 React.lazy 处定义一个 loading 状态,我们要搭配使用 Suspense,在 Suspense 这里定义一个 loading 状态。这就意味着,你可以有很多个 React.lazy 组件,但你只需要给对应的 Suspense 指定一个 loading 状态就可以了

此外,我们可以在任意深的地方放入一个 React.lazy 组件,Suspense 会统一的、干干净净的处理好懒加载的任务。

那么我们要怎样使用 React.lazy 来改写上面的代码呢?如下所示:

import React, { Suspense } from 'react';
const Description = React.lazy(() => import('./Description'));

function App() {
  return (
    <div>
      <h1>My Movie</h1>
      <Suspense fallback="Loading...">
        <Description />
      </Suspense>
    </div>
  );
}

Suspense 就像是 try-catch 一样,会「捕获」到 React.lazy 实例,然后会进入同一个 fallback 组件。也就是说,下面的例子中,我们只会渲染同一个 fallback:

import React, { Suspense } from 'react';
const Description = React.lazy(() => import('./Description'));

function App() {
  return (
    <div>
      <h1>My Movie</h1>
      <Suspense fallback="Loading...">
        <Description />
        <div>
          <span>Cast</span>
          <AnotherLazyComponent />
        </div>
      </Suspense>
    </div>
  );
}

// AnotherLazyComponent.js (imagine in another file)
const AndYetAnotherLazyComponent = React.lazy(() =>
  import('./AndYetAnotherLazyComponent')
);

function AnotherLazyComponent() {
  return (
    <div>
      <span>So...so..lazy..</span>
      <AndYetAnotherLazyComponent />
    </div>
  );
}

如果我们想更自由的指定不同的懒加载组件的不同 loading 状态,只需要像下面一样嵌套 Suspense 即可:

function App() {
  return (
    <div>
      <h1>My Movie</h1>
      <Suspense fallback="Loading...">
        <Description />
        <div>
          <Suspense fallback="Sorry for our laziness">
            <span>Cast</span>
            <AnotherLazyComponent />
          </Suspense>
        </div>
      </Suspense>
    </div>
  );
}

厉害的是,如果 AnotherLazyComponent 很久都没有加载完,没关系,他不会影响到其他组件的渲染。React.lazy 和 Suspense 会把 AnotherLazyComponent 和他的子组件们隔离开来,避免它加载的延迟影响到其他内容的渲染。

这样一来,与前面没有另一个 Suspense 的写法相比,后者就不会等待所有懒加载组件都加载好后才能呈现,而是逐个呈现各个组件,这就有些像是 Promise.all 和各自异步的感觉。

最后

是不是可以准备改造一下项目了呢?

源地址:hswolff.com/blog/react-…

参考:reactjs.org/docs/code-s…

写一个没有 JSX 的 React

习惯了 JSX 的写法,今天来感受下没有 JSX 的 React 的酸爽。

我们知道,通常我们在使用 React 时所写的 JSX,都会被 Babel 编译成一些方法,一个很有名的方法就是 React.createElement。

React.createElement 方法需要三个参数:

  • type: HTML 元素或组件的类型(例如: h1、h2、p、button 等等);

  • props: 传入的属性对象;

  • children: 任何可以穿入的夹在元素中的东西。

简单的例子

那么我们把最基本的 React 去掉 JSX 来写,就有下面的代码:

let welcome = React.createElement('h1',{style:{color:"red"}},`Welcome to react world`);

ReactDOM.render(welcome,document.querySelector('#root'));

上面的代码就是纯 React,当然,ReactDOM.render 方法还是一样的。

我们调整下上面的代码,组织成一个组件:

class Welcome extends React.Component{
  render(){
    return React.createElement('h1',{style:{color:"red"}},
            `Welcome to ${this.props.name}`);
  }
}

ReactDOM.render(React.createElement(Welcome,
                {name:"Homepage"},null),document.querySelector('#root'));

我们在 React.createElement 方法传入了第二个参数 {name:"Homepage"},因此在 Welcome 类内部,就可以通过 this.props.name 访问到这个传入的属性。

counter 例子

const  el =  React.createElement;

function Button(props){
  return el('button', { onClick: props.handleClick }, props.name);
}

class Counter extends React.Component{
  state= {
       num: 0,
  }

  handleIncrement = () =>{
    this.setState({
      num: this.state.num + 1,
    });
  }

  handleDecrement = () =>{
    this.setState({
      num: this.state.num - 1,
    });
  }

  render(){
    return el('div',null,
             el(Button, { handleClick: this.handleIncrement, name:'Increment' }, null),
             el(Button,{ handleClick: this.handleDecrement, name:'Decrement' }, null),
             el('p', null, this.state.num),
	}
}

ReactDOM.render(el(Counter,null,null),document.querySelector('#root'))

可以看到,没有 JSX,我们的 render 方法变得复杂了很多。上面代码的效果如下图所示:

我们再回来看看 JSX 的写法:

function Button(props) {
  return <button onClick={props.handleClick}>{props.name}</button>
}

class Counter extends React.Component {
  state = {
    num: 0
  }
  handleIncrement = () => {
    this.setState({
      num: this.state.num + 1
    })
  }
  handleDecrement = () => {
    this.setState({
      num: this.state.num - 1
    })
  }
  render() {
    return (
      <div>
        <p>{this.state.num}</p>
        <Button name="Increment" handleClick={this.handleIncrement} />
        <Button name="Decrement" handleClick={this.handleDecrement} />
      </div>
    )
  }
}

ReactDOM.render(<Counter />, document.querySelector('#root'))

JSX 的可读性原来还算好的了。

源地址:codeburst.io/how-to-use-…

执行上下文和执行栈

什么是执行上下文

这可能是很多书本上都会讲的基础知识,这里我们也带一遍。执行上下文就是 JavaScript 代码求值和执行的环境。不管跑什么代码,都是跑在一个执行上下文里。

执行上下文有 3 种:

  • 全局上下文 程序里只会有一个。
  • 函数上下文 函数上下文可以有人以多个,只要一个新的函数被调用,就会创建一个函数上下文,而且他们会按照一种定义好的顺序逐个执行。
  • Eval 上下文 这个咱们还是不多讲了,危险。

执行栈

其实也就是调用栈。当 JavaScript 引擎开始执行脚本是的时候,会先创建一个全局执行上下文,并将其 push 到当前执行栈,无论何时一个函数被调用,就会创建一个新的(函数)执行上下文并压入栈中。

引擎会执行那些在栈顶的执行上下文。当函数执行完毕,执行栈会将其弹出,并把控制权交给当前栈的下一个上下文。

举个例子:

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

以一个图来展示就是:

怎么个执行真的不需要多说了。我们还是接着讲点不知道的吧。

执行上下文是怎么被创建的?

上面的内容告诉我们 JavaScript 引擎是怎么管理执行上下文的,现在我们来讲下上下文是怎么被创建的。

执行上下文的创建总共分两步:

  • 创建阶段
  • 执行阶段

创建阶段

执行上下文其实就是在创建阶段被创建的。在创建阶段,我们会有两种环境被创建:

  • LexicalEnvironment,我们叫作词汇环境
  • VariableEnvironment,我们叫作变量环境

所以,执行上下文可以从概念上标识如下:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

词汇环境

官方 ES6 是这么定义词汇环境的:

词汇环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构定义标识符与特定变量和函数的关联。词汇环境由环境记录和的可能为 null 引用的外部词汇环境组成。

简单来说,词汇环境就是一种维护标识符到变量的映射,这里标识符指变量或函数的名字,而变量指的是一个实际对象(包括函数对象、数组对象)或基本值的引用。

每个词汇环境由三部分组成:

  1. 环境记录
  2. 外部环境引用
  3. this binding

我们还需要再继续展开讲:

  1. 环境记录

环境记录,就是变量和函数声明存储在词法环境中的位置。有两种环境记录:

  • 声明式环境记录
  • 对象环境记录

前者主要就是存放变量、函数这类声明了的,后者则是对全局的代码进行记录,例如全局绑定的 window。

注意,对于函数,环境记录还会包含 arguments 对象,用于映射传入函数的参数和记录传入参数的个数。我们举个例子就很明白了:

function foo(a, b) {
  var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},
  1. 外部环境引用

对外部环境的引用意味着它可以访问其外部词汇环境。这意味着如果在当前词汇环境中找不到它们,JavaScript 引擎可以在外部环境中查找变量。

  1. this binding

这一部分就是讲 this 是怎么设置的。

在全局执行上下文,this 指向全局对象,比如浏览器环境下就是 window。

【基础知识】在函数执行上下文里,this 就取决于函数调用的方式。如果是通过对象引用,那么 this 就是这个对象,不然的话,this 就会是全局对象或者 undefined (严格模式下)。举个例子:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 'this' refers to 'person', because 'calcAge' was called with //'person' object reference
const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given

综上:词汇环境的伪代码如下:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

变量环境

它也是一个词法环境,因此它具有上面定义的词法环境的所有内容。唯一的不同是,在 ES6 中,词法环境和变量环境这两个,前者用于存储函数声明和变量(let 和 const)绑定,而后者仅用于存储变量(var)绑定。

执行阶段

在这个阶段,变量赋值都结束了,代码也最终被执行掉。

举个例子:

let a = 20;
const b = 30;
var c;
function multiply(e, f) {
 var g = 20;
 return e * f * g;
}
c = multiply(20, 30);

执行上述代码时,JavaScript 引擎会创建一个全局执行上下文来执行全局代码。因此,在创建阶段,全局执行上下文将如下所示:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

在执行阶段,完成变量赋值。因此,在执行阶段,全局执行上下文将看起来像这样:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

当遇到函数 multiply(20,30) 被调用时,会创建一个新的函数执行上下文来执行函数代码。因此,在创建阶段函数执行上下文将如下所示:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

在此之后,执行上下文将走完执行阶段,这意味着完成了对函数内部变量的赋值。因此,在执行阶段函数执行上下文将如下所示:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

函数完成后,返回的值存储在 c 中。因此全局词汇环境得到了更新。之后,全局代码完成,程序结束。

注意!你可能发现一个有意思的东西,就是在创建阶段,let 和 const 定义的变量是「未初始化」状态,而 var 定义的则是 undefined。

这是因为,在定义阶段,代码会扫描变量和函数声明,函数声明会在环境中被完整存着,对 var 定义的就会被初始化成 undefined,而 let 和 const 定义的就成了未初始化状态。

这就是为什么,我们如果在 var 定义的变量定义之前使用它,会得到 undefined,但在 let 或 const 定义的变量定义之前使用会报 error。

这就是我们所说的提升

Javascript Hoisting:In javascript, every variable declaration is hoisted to the top of its declaration context.

另一个点则是,如果在执行阶段,JavaScript 引擎找不到 let 变量实际的值,他就会被赋值为 undefined。

源地址:blog.bitsrc.io/understandi…

公私有域和方法

这篇文章主要介绍 V8 v7.2 和 Chrome 72 新的 class fields 语法,以及即将出现的 private class fields。

我们来创建一个 IncreasingCounter 实例:

const counter = new IncreasingCounter();
counter.value;
// logs 'Getting the current value!'
// → 0
counter.increment();
counter.value;
// logs 'Getting the current value!'
// → 1

ES2015 class

如果使用 ES2015 class 语法,我们应该会这么实现 IncreasingCounter:

class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

该类在原型上添上了一个 value getter 和 increment 方法。类有一个构造函数,它创建一个实例属性 _count,并将其默认值设置为0。我们目前倾向于使用下划线前缀来表示 _count 不应该由该类的使用者直接使用,但这只是一个约定;它不是真正的「私有」属性,只是有这个特殊语义而已。

const counter = new IncreasingCounter();
counter.value;
// logs 'Getting the current value!'
// → 0

// Nothing stops people from reading or messing with the
// `_count` instance property. 😢
counter._count;
// → 0
counter._count = 42;
counter.value;
// logs 'Getting the current value!'
// → 42

我们可以看到,我们仍然可以直接修改这个「约定」的私有变量,也可以通过 getter 来修改。

Public class fields

我们可以通过新的语法来简化类公有变量的定义:

class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

_count 属性在类的顶部声明。我们不再需要构造函数来定义一些字段。很干净嘛!

但是,_count 字段仍然是公共属性。我们希望阻止人们直接访问该属性。

Private class fields

新的私有域语法和公有域是类似的,不同之处就是我们需要用 # 来对私有域进行标记。

class IncreasingCounter {
  #count = 0;
  get value() {
    console.log('Getting the current value!');
    return this.#count;
  }
  increment() {
    this.#count++;
  }
}

私有域在类外是不可访问的:

const counter = new IncreasingCounter();
counter.#count;
// → SyntaxError
counter.#count = 42;
// → SyntaxError

Public and static properties

私有域和私有方法依然不可在类外访问,私有域可以被私有方法和公有方法在类内访问,私有方法可以被公有方法访问。

class FakeMath {
  // `PI` is a static public property.
  static PI = 22 / 7; // Close enough.

  // `#totallyRandomNumber` is a static private property.
  static #totallyRandomNumber = 4;

  // `#computeRandomNumber` is a static private method.
  static #computeRandomNumber() {
    return FakeMath.#totallyRandomNumber;
  }

  // `random` is a static public method (ES2015 syntax)
  // that consumes `#computeRandomNumber`.
  static random() {
    console.log('I heard you like random numbers…')
    return FakeMath.#computeRandomNumber();
  }
}

FakeMath.PI;
// → 3.142857142857143
FakeMath.random();
// logs 'I heard you like random numbers…'
// → 4
FakeMath.#totallyRandomNumber;
// → SyntaxError
FakeMath.#computeRandomNumber();
// → SyntaxError

数组在性能方面的一个注意点

我们知道,在 C 或 C++ 这类比较底层的语言中,不同类型的数组分配的内存空间可能是不一样的,而且我们在申请一个数组的内存空间时(如果没记错应该返回的是个指针)也不能混合各种类型。然而在 JavaScript 里,我们创建数组的时候可以有各种姿势,比如:

const arr = [0,0,0];

const arr = [1, , 3];

const arr = [1, 'a', {}];

那么在 JavaScript 里,引擎是怎么做的,并针对 JavaScript 的特点做了什么样的优化呢?

没有「空洞」的数组

在大多数编程语言中,数组是值的连续序列。在 JavaScript 中,Array 是一个将索引映射到元素的字典。它可以有「空洞」,在某一个索引处没有值,也就是该索引没有映射到某个元素。例如,下面数组在索引 1 处有一个「空洞」:

undefined

没有「空洞」的数组往往更快、更紧凑,引擎可以在内部连续存储它们。如果向数组添加「空洞」,则必须将内部数据结构切换为字典。在一些引擎中,例如 V8,由此产生的性能去优化是永久性的,不能回到之前连续的存储方式。

只有小整型或只有数字类型的数组

V8 还会针对下面的场景进行进一步的优化:

  • 小整型:JavaScript 中整型的最大范围是 53 位加上个标志位,而小整型是指比这个范围更小的。比如说,在 32 位系统上 V8 会使用 30 个位加上标志位来表示。结果就是,小整型场景会存储的更紧凑。
  • 数字:数字组成的数组相比较有着任意值的数组可以有更快的访问速度。

类似的,一旦不满足这些条件触发了去优化,就无法再恢复到优化的状态。

总结

  • 能纯数字表示的就纯数字
  • 数组本质上也是对象

源地址:2ality.com/2018/12/cre…

「每日一瞥」是团队内部日常业界动态提炼,发布时效可能略有延后。

文章可随意转载,但请保留此 原文链接

非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com