「每日一瞥 📰 」1224 ~ 0104 - 图1

  • 从过去直到 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 就成了一种广泛接受的做法。
    下面的例子就是没有拆分过的、只会打包成一份的应用,在加载时会同步全部加载再渲染: ```javascript import Description from ‘./Description’; function App() { return ( My Movie

); }

  1. 现在我们来开始看看,如何让我们的 Module bundler 来懒加载我们的模块呢?
  2. <a name="2b01851c"></a>
  3. ## Dynamic import proposal
  4. 动态 import 提案为 ES Modules 添加了新特性,使我们可以以异步的方式定义我们的依赖关系。`import` 语句可以作为一个函数来调用,并返回一个 Promise,这个 Promise resolve 我们想要加载的模块。使用方式只需要从上面的 ES Modules import 方式略加调整:
  5. ```javascript
  6. - import Description from './Description';
  7. + const Description = import('./Description');

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

React 组件的懒加载

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

  1. const LoadDescription = () => import('./Description');
  2. class App extends React.Component {
  3. state = {
  4. Description: null,
  5. };
  6. componentDidMount() {
  7. LoadDescription.then(Description => {
  8. this.setState({ Description: Description.default });
  9. });
  10. }
  11. render() {
  12. const { Description } = this.state;
  13. return (
  14. My Movie
  15. {Description ? : 'Loading...'}
  16. );
  17. }
  18. }

这样写未免就有点蛋疼了,所幸的是我们有一个非常好用的库,即 react-loadable
react-loadable 会帮我们省掉很多模板代码,改写后的效果如下:

  1. import Loadable from 'react-loadable';
  2. const LoadableDescription = Loadable({
  3. loader: () => import('./Description'),
  4. loading() {
  5. return Loading...
  6. ;
  7. },
  8. });
  9. function App() {
  10. return (
  11. My Movie
  12. );
  13. }

这样看上去就好多了,我们就不需要再自己去管生命周期之类的事,只需要靠它来 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 来改写上面的代码呢?如下所示:

  1. import React, { Suspense } from 'react';
  2. const Description = React.lazy(() => import('./Description'));
  3. function App() {
  4. return (
  5. My Movie
  6. );
  7. }

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

  1. import React, { Suspense } from 'react';
  2. const Description = React.lazy(() => import('./Description'));
  3. function App() {
  4. return (
  5. My Movie
  6. Cast
  7. );
  8. }
  9. // AnotherLazyComponent.js (imagine in another file)
  10. const AndYetAnotherLazyComponent = React.lazy(() =>
  11. import('./AndYetAnotherLazyComponent')
  12. );
  13. function AnotherLazyComponent() {
  14. return (
  15. So...so..lazy..
  16. );
  17. }

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

  1. function App() {
  2. return (
  3. My Movie
  4. Cast
  5. );
  6. }

厉害的是,如果 AnotherLazyComponent 很久都没有加载完,没关系,他不会影响到其他组件的渲染。React.lazy 和 Suspense 会把 AnotherLazyComponent 和他的子组件们隔离开来,避免它加载的延迟影响到其他内容的渲染。
这样一来,与前面没有另一个 Suspense 的写法相比,后者就不会等待所有懒加载组件都加载好后才能呈现,而是逐个呈现各个组件,这就有些像是 Promise.all 和各自异步的感觉。

最后

是不是可以准备改造一下项目了呢?
源地址:https://hswolff.com/blog/react-lazy-and-suspense/
参考:https://reactjs.org/docs/code-splitting.html

写一个没有 JSX 的 React

习惯了 JSX 的写法,今天来感受下没有 JSX 的 React 的酸爽。
我们知道,通常我们在使用 React 时所写的 JSX,都会被 Babel 编译成一些方法,一个很有名的方法就是 React.createElement。
React.createElement 方法需要三个参数:

  • type: HTML 元素或组件的类型(例如: h1、h2、p、button 等等);
  • props: 传入的属性对象;
  • children: 任何可以穿入的夹在元素中的东西。

    简单的例子

    那么我们把最基本的 React 去掉 JSX 来写,就有下面的代码:
    1. let welcome = React.createElement('h1',{style:{color:"red"}},`Welcome to react world`);
    2. ReactDOM.render(welcome,document.querySelector('#root'));
    上面的代码就是纯 React,当然,ReactDOM.render 方法还是一样的。
    我们调整下上面的代码,组织成一个组件:
    1. class Welcome extends React.Component{
    2. render(){
    3. return React.createElement('h1',{style:{color:"red"}},
    4. `Welcome to ${this.props.name}`);
    5. }
    6. }
    7. ReactDOM.render(React.createElement(Welcome,
    8. {name:"Homepage"},null),document.querySelector('#root'));
    我们在 React.createElement 方法传入了第二个参数 {name:"Homepage"},因此在 Welcome 类内部,就可以通过 this.props.name 访问到这个传入的属性。

    counter 例子

    1. const el = React.createElement;
    2. function Button(props){
    3. return el('button', { onClick: props.handleClick }, props.name);
    4. }
    5. class Counter extends React.Component{
    6. state= {
    7. num: 0,
    8. }
    9. handleIncrement = () =>{
    10. this.setState({
    11. num: this.state.num + 1,
    12. });
    13. }
    14. handleDecrement = () =>{
    15. this.setState({
    16. num: this.state.num - 1,
    17. });
    18. }
    19. render(){
    20. return el('div',null,
    21. el(Button, { handleClick: this.handleIncrement, name:'Increment' }, null),
    22. el(Button,{ handleClick: this.handleDecrement, name:'Decrement' }, null),
    23. el('p', null, this.state.num),
    24. }
    25. }
    26. ReactDOM.render(el(Counter,null,null),document.querySelector('#root'))
    可以看到,没有 JSX,我们的 render 方法变得复杂了很多。上面代码的效果如下图所示:
    「每日一瞥 📰 」1224 ~ 0104 - 图2
    我们再回来看看 JSX 的写法: ```javascript function Button(props) { return {props.name} } class Counter extends React.Component { state = { num: 0 } handleIncrement = () => { this.setState({
    1. num: this.state.num + 1
    }) } handleDecrement = () => { this.setState({
    1. num: this.state.num - 1
    }) } render() { return (
    1. {this.state.num}
  1. )

} } ReactDOM.render(, document.querySelector(‘#root’))

  1. JSX 的可读性原来还算好的了。<br />源地址:[https://codeburst.io/how-to-use-react-create-element-38020c6d40e8](https://codeburst.io/how-to-use-react-create-element-38020c6d40e8)
  2. <a name="fff3f9cf"></a>
  3. # 执行上下文和执行栈
  4. <a name="f9b53a96"></a>
  5. ## 什么是执行上下文
  6. 这可能是很多书本上都会讲的基础知识,这里我们也带一遍。执行上下文就是 JavaScript 代码求值和执行的环境。不管跑什么代码,都是跑在一个执行上下文里。<br />执行上下文有 3 种:
  7. - 全局上下文<br />
  8. 程序里只会有一个。
  9. - 函数上下文<br />
  10. 函数上下文可以有人以多个,只要一个新的函数被调用,就会创建一个函数上下文,而且他们会按照一种定义好的顺序逐个执行。
  11. - Eval 上下文<br />
  12. 这个咱们还是不多讲了,危险。
  13. <a name="90e4a7b0"></a>
  14. ## 执行栈
  15. 其实也就是调用栈。当 JavaScript 引擎开始执行脚本是的时候,会先创建一个全局执行上下文,并将其 push 到当前执行栈,无论何时一个函数被调用,就会创建一个新的(函数)执行上下文并压入栈中。<br />引擎会执行那些在栈顶的执行上下文。当函数执行完毕,执行栈会将其弹出,并把控制权交给当前栈的下一个上下文。<br />举个例子:
  16. ```javascript
  17. let a = 'Hello World!';
  18. function first() {
  19. console.log('Inside first function');
  20. second();
  21. console.log('Again inside first function');
  22. }
  23. function second() {
  24. console.log('Inside second function');
  25. }
  26. first();
  27. console.log('Inside Global Execution Context');

以一个图来展示就是:
「每日一瞥 📰 」1224 ~ 0104 - 图3
怎么个执行真的不需要多说了。我们还是接着讲点不知道的吧。

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

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

  • 创建阶段
  • 执行阶段

    创建阶段

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

  • LexicalEnvironment,我们叫作词汇环境

  • VariableEnvironment,我们叫作变量环境

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

  1. ExecutionContext = {
  2. LexicalEnvironment = ,
  3. VariableEnvironment = ,
  4. }

词汇环境

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

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

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

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

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

  1. 环境记录

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

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

前者主要就是存放变量、函数这类声明了的,后者则是对全局的代码进行记录,例如全局绑定的 window。
注意,对于函数,环境记录还会包含 arguments 对象,用于映射传入函数的参数和记录传入参数的个数。我们举个例子就很明白了:

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

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

  1. this binding

这一部分就是讲 this 是怎么设置的。
在全局执行上下文,this 指向全局对象,比如浏览器环境下就是 window。
【基础知识】在函数执行上下文里,this 就取决于函数调用的方式。如果是通过对象引用,那么 this 就是这个对象,不然的话,this 就会是全局对象或者 undefined (严格模式下)。举个例子:

  1. const person = {
  2. name: 'peter',
  3. birthYear: 1994,
  4. calcAge: function() {
  5. console.log(2018 - this.birthYear);
  6. }
  7. }
  8. person.calcAge();
  9. // 'this' refers to 'person', because 'calcAge' was called with //'person' object reference
  10. const calculateAge = person.calcAge;
  11. calculateAge();
  12. // 'this' refers to the global window object, because no object reference was given

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

  1. GlobalExectionContext = {
  2. LexicalEnvironment: {
  3. EnvironmentRecord: {
  4. Type: "Object",
  5. // Identifier bindings go here
  6. }
  7. outer: ,
  8. this:
  9. }
  10. }
  11. FunctionExectionContext = {
  12. LexicalEnvironment: {
  13. EnvironmentRecord: {
  14. Type: "Declarative",
  15. // Identifier bindings go here
  16. }
  17. outer: ,
  18. this:
  19. }
  20. }

变量环境

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

执行阶段

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

  1. let a = 20;
  2. const b = 30;
  3. var c;
  4. function multiply(e, f) {
  5. var g = 20;
  6. return e * f * g;
  7. }
  8. c = multiply(20, 30);

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

  1. GlobalExectionContext = {
  2. LexicalEnvironment: {
  3. EnvironmentRecord: {
  4. Type: "Object",
  5. // Identifier bindings go here
  6. a: < uninitialized >,
  7. b: < uninitialized >,
  8. multiply: < func >
  9. }
  10. outer: ,
  11. ThisBinding:
  12. },
  13. VariableEnvironment: {
  14. EnvironmentRecord: {
  15. Type: "Object",
  16. // Identifier bindings go here
  17. c: undefined,
  18. }
  19. outer: ,
  20. ThisBinding:
  21. }
  22. }

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

  1. GlobalExectionContext = {
  2. LexicalEnvironment: {
  3. EnvironmentRecord: {
  4. Type: "Object",
  5. // Identifier bindings go here
  6. a: 20,
  7. b: 30,
  8. multiply: < func >
  9. }
  10. outer: ,
  11. ThisBinding:
  12. },
  13. VariableEnvironment: {
  14. EnvironmentRecord: {
  15. Type: "Object",
  16. // Identifier bindings go here
  17. c: undefined,
  18. }
  19. outer: ,
  20. ThisBinding:
  21. }
  22. }

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

  1. FunctionExectionContext = {
  2. LexicalEnvironment: {
  3. EnvironmentRecord: {
  4. Type: "Declarative",
  5. // Identifier bindings go here
  6. Arguments: {0: 20, 1: 30, length: 2},
  7. },
  8. outer: ,
  9. ThisBinding: ,
  10. },
  11. VariableEnvironment: {
  12. EnvironmentRecord: {
  13. Type: "Declarative",
  14. // Identifier bindings go here
  15. g: undefined
  16. },
  17. outer: ,
  18. ThisBinding:
  19. }
  20. }

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

  1. FunctionExectionContext = {
  2. LexicalEnvironment: {
  3. EnvironmentRecord: {
  4. Type: "Declarative",
  5. // Identifier bindings go here
  6. Arguments: {0: 20, 1: 30, length: 2},
  7. },
  8. outer: ,
  9. ThisBinding: ,
  10. },
  11. VariableEnvironment: {
  12. EnvironmentRecord: {
  13. Type: "Declarative",
  14. // Identifier bindings go here
  15. g: 20
  16. },
  17. outer: ,
  18. ThisBinding:
  19. }
  20. }

函数完成后,返回的值存储在 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。
源地址:https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0

公私有域和方法

这篇文章主要介绍 V8 v7.2 和 Chrome 72 新的 class fields 语法,以及即将出现的 private class fields。
我们来创建一个 IncreasingCounter 实例:

  1. const counter = new IncreasingCounter();
  2. counter.value;
  3. // logs 'Getting the current value!'
  4. // → 0
  5. counter.increment();
  6. counter.value;
  7. // logs 'Getting the current value!'
  8. // → 1

ES2015 class

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

  1. class IncreasingCounter {
  2. constructor() {
  3. this._count = 0;
  4. }
  5. get value() {
  6. console.log('Getting the current value!');
  7. return this._count;
  8. }
  9. increment() {
  10. this._count++;
  11. }
  12. }

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

  1. const counter = new IncreasingCounter();
  2. counter.value;
  3. // logs 'Getting the current value!'
  4. // → 0
  5. // Nothing stops people from reading or messing with the
  6. // `_count` instance property. 😢
  7. counter._count;
  8. // → 0
  9. counter._count = 42;
  10. counter.value;
  11. // logs 'Getting the current value!'
  12. // → 42

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

Public class fields

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

  1. class IncreasingCounter {
  2. _count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this._count;
  6. }
  7. increment() {
  8. this._count++;
  9. }
  10. }

_count 属性在类的顶部声明。我们不再需要构造函数来定义一些字段。很干净嘛!
但是,_count 字段仍然是公共属性。我们希望阻止人们直接访问该属性。

Private class fields

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

  1. class IncreasingCounter {
  2. #count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this.#count;
  6. }
  7. increment() {
  8. this.#count++;
  9. }
  10. }

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

  1. const counter = new IncreasingCounter();
  2. counter.#count;
  3. // → SyntaxError
  4. counter.#count = 42;
  5. // → SyntaxError

Public and static properties

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

  1. class FakeMath {
  2. // `PI` is a static public property.
  3. static PI = 22 / 7; // Close enough.
  4. // `#totallyRandomNumber` is a static private property.
  5. static #totallyRandomNumber = 4;
  6. // `#computeRandomNumber` is a static private method.
  7. static #computeRandomNumber() {
  8. return FakeMath.#totallyRandomNumber;
  9. }
  10. // `random` is a static public method (ES2015 syntax)
  11. // that consumes `#computeRandomNumber`.
  12. static random() {
  13. console.log('I heard you like random numbers…')
  14. return FakeMath.#computeRandomNumber();
  15. }
  16. }
  17. FakeMath.PI;
  18. // → 3.142857142857143
  19. FakeMath.random();
  20. // logs 'I heard you like random numbers…'
  21. // → 4
  22. FakeMath.#totallyRandomNumber;
  23. // → SyntaxError
  24. FakeMath.#computeRandomNumber();
  25. // → SyntaxError

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

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

  1. const arr = [0,0,0];
  2. const arr = [1, , 3];
  3. const arr = [1, 'a', {}];

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

没有「空洞」的数组

在大多数编程语言中,数组是值的连续序列。在 JavaScript 中,Array 是一个将索引映射到元素的字典。它可以有「空洞」,在某一个索引处没有值,也就是该索引没有映射到某个元素。例如,下面数组在索引 1 处有一个「空洞」:
「每日一瞥 📰 」1224 ~ 0104 - 图4
没有「空洞」的数组往往更快、更紧凑,引擎可以在内部连续存储它们。如果向数组添加「空洞」,则必须将内部数据结构切换为字典。在一些引擎中,例如 V8,由此产生的性能去优化是永久性的,不能回到之前连续的存储方式。

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

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

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

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

总结

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

源地址:http://2ality.com/2018/12/creating-arrays.html

「每日一瞥」是团队内部日常业界动态提炼,发布时效可能略有延后。 文章可随意转载,但请保留此 原文链接。 非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com