- 从过去直到 React.lazy
- 写一个没有 JSX 的 React
- 执行上下文和执行栈
- 公私有域和方法
- 数组在性能方面的一个注意点
从过去直到 React.lazy
code-splitting
当我们最最开始做前端开发的时候,JavaScript 文件自然就一个个罗列在一起,通过 script 标签引入到 html 里。当然,即使在现在,我们也还是会在写一些 Demo 时使用这样的方式。
如今,我们有了如 Webpack、Parcel 等 Module bundler 来为我们更好的组织 JavaScript 文件。我们可以使用各种模块系统如 CommonJS(require
、module.exports
)或者 ES Modules(import
、export
)来定义文件之间的依赖。
然而,随着我们的应用越来越大,我们就会得到一个巨大的 JS bundle,而这种慢慢等待加载的体验是绝不能忍受的。因此,code-splitting 就成了一种广泛接受的做法。
下面的例子就是没有拆分过的、只会打包成一份的应用,在加载时会同步全部加载再渲染: ```javascript import Description from ‘./Description’; function App() { return ( My Movie
); }
现在我们来开始看看,如何让我们的 Module bundler 来懒加载我们的模块呢?
<a name="2b01851c"></a>
## Dynamic import proposal
动态 import 提案为 ES Modules 添加了新特性,使我们可以以异步的方式定义我们的依赖关系。`import` 语句可以作为一个函数来调用,并返回一个 Promise,这个 Promise 会 resolve 我们想要加载的模块。使用方式只需要从上面的 ES Modules 的 import 方式略加调整:
```javascript
- 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 (
My Movie
{Description ? : 'Loading...'}
);
}
}
这样写未免就有点蛋疼了,所幸的是我们有一个非常好用的库,即 react-loadable:
react-loadable 会帮我们省掉很多模板代码,改写后的效果如下:
import Loadable from 'react-loadable';
const LoadableDescription = Loadable({
loader: () => import('./Description'),
loading() {
return Loading...
;
},
});
function App() {
return (
My Movie
);
}
这样看上去就好多了,我们就不需要再自己去管生命周期之类的事,只需要靠它来 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 (
My Movie
);
}
Suspense 就像是 try-catch 一样,会「捕获」到 React.lazy 实例,然后会进入同一个 fallback 组件。也就是说,下面的例子中,我们只会渲染同一个 fallback:
import React, { Suspense } from 'react';
const Description = React.lazy(() => import('./Description'));
function App() {
return (
My Movie
Cast
);
}
// AnotherLazyComponent.js (imagine in another file)
const AndYetAnotherLazyComponent = React.lazy(() =>
import('./AndYetAnotherLazyComponent')
);
function AnotherLazyComponent() {
return (
So...so..lazy..
);
}
如果我们想更自由的指定不同的懒加载组件的不同 loading 状态,只需要像下面一样嵌套 Suspense 即可:
function App() {
return (
My Movie
Cast
);
}
厉害的是,如果 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 来写,就有下面的代码:
上面的代码就是纯 React,当然,ReactDOM.render 方法还是一样的。let welcome = React.createElement('h1',{style:{color:"red"}},`Welcome to react world`);
ReactDOM.render(welcome,document.querySelector('#root'));
我们调整下上面的代码,组织成一个组件:
我们在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 例子
可以看到,没有 JSX,我们的 render 方法变得复杂了很多。上面代码的效果如下图所示: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 的写法: ```javascript function Button(props) { return {props.name} } class Counter extends React.Component { state = { num: 0 } handleIncrement = () => { this.setState({
}) } handleDecrement = () => { this.setState({num: this.state.num + 1
}) } render() { return (num: this.state.num - 1
{this.state.num}
)
} } ReactDOM.render(, document.querySelector(‘#root’))
JSX 的可读性原来还算好的了。<br />源地址:[https://codeburst.io/how-to-use-react-create-element-38020c6d40e8](https://codeburst.io/how-to-use-react-create-element-38020c6d40e8)
<a name="fff3f9cf"></a>
# 执行上下文和执行栈
<a name="f9b53a96"></a>
## 什么是执行上下文
这可能是很多书本上都会讲的基础知识,这里我们也带一遍。执行上下文就是 JavaScript 代码求值和执行的环境。不管跑什么代码,都是跑在一个执行上下文里。<br />执行上下文有 3 种:
- 全局上下文<br />
程序里只会有一个。
- 函数上下文<br />
函数上下文可以有人以多个,只要一个新的函数被调用,就会创建一个函数上下文,而且他们会按照一种定义好的顺序逐个执行。
- Eval 上下文<br />
这个咱们还是不多讲了,危险。
<a name="90e4a7b0"></a>
## 执行栈
其实也就是调用栈。当 JavaScript 引擎开始执行脚本是的时候,会先创建一个全局执行上下文,并将其 push 到当前执行栈,无论何时一个函数被调用,就会创建一个新的(函数)执行上下文并压入栈中。<br />引擎会执行那些在栈顶的执行上下文。当函数执行完毕,执行栈会将其弹出,并把控制权交给当前栈的下一个上下文。<br />举个例子:
```javascript
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 = ,
VariableEnvironment = ,
}
词汇环境
官方 ES6 是这么定义词汇环境的:
词汇环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构定义标识符与特定变量和函数的关联。词汇环境由环境记录和的可能为 null 引用的外部词汇环境组成。
简单来说,词汇环境就是一种维护标识符到变量的映射,这里标识符指变量或函数的名字,而变量指的是一个实际对象(包括函数对象、数组对象)或基本值的引用。
每个词汇环境由三部分组成:
- 环境记录
- 外部环境引用
- this binding
我们还需要再继续展开讲:
- 环境记录
环境记录,就是变量和函数声明存储在词法环境中的位置。有两种环境记录:
- 声明式环境记录
- 对象环境记录
前者主要就是存放变量、函数这类声明了的,后者则是对全局的代码进行记录,例如全局绑定的 window。
注意,对于函数,环境记录还会包含 arguments 对象,用于映射传入函数的参数和记录传入参数的个数。我们举个例子就很明白了:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},
- 外部环境引用
对外部环境的引用意味着它可以访问其外部词汇环境。这意味着如果在当前词汇环境中找不到它们,JavaScript 引擎可以在外部环境中查找变量。
- 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: ,
this:
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
}
outer: ,
this:
}
}
变量环境
它也是一个词法环境,因此它具有上面定义的词法环境的所有内容。唯一的不同是,在 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: ,
ThisBinding:
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined,
}
outer: ,
ThisBinding:
}
}
在执行阶段,完成变量赋值。因此,在执行阶段,全局执行上下文将看起来像这样:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: 20,
b: 30,
multiply: < func >
}
outer: ,
ThisBinding:
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined,
}
outer: ,
ThisBinding:
}
}
当遇到函数 multiply(20,30)
被调用时,会创建一个新的函数执行上下文来执行函数代码。因此,在创建阶段,函数执行上下文将如下所示:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: ,
ThisBinding: ,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: undefined
},
outer: ,
ThisBinding:
}
}
在此之后,执行上下文将走完执行阶段,这意味着完成了对函数内部变量的赋值。因此,在执行阶段,函数执行上下文将如下所示:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: ,
ThisBinding: ,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: 20
},
outer: ,
ThisBinding:
}
}
函数完成后,返回的值存储在 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 实例:
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 处有一个「空洞」:
没有「空洞」的数组往往更快、更紧凑,引擎可以在内部连续存储它们。如果向数组添加「空洞」,则必须将内部数据结构切换为字典。在一些引擎中,例如 V8,由此产生的性能去优化是永久性的,不能回到之前连续的存储方式。
只有小整型或只有数字类型的数组
V8 还会针对下面的场景进行进一步的优化:
- 小整型:JavaScript 中整型的最大范围是 53 位加上个标志位,而小整型是指比这个范围更小的。比如说,在 32 位系统上 V8 会使用 30 个位加上标志位来表示。结果就是,小整型场景会存储的更紧凑。
- 数字:数字组成的数组相比较有着任意值的数组可以有更快的访问速度。
类似的,一旦不满足这些条件触发了去优化,就无法再恢复到优化的状态。
总结
- 能纯数字表示的就纯数字
- 数组本质上也是对象
源地址:http://2ality.com/2018/12/creating-arrays.html
「每日一瞥」是团队内部日常业界动态提炼,发布时效可能略有延后。 文章可随意转载,但请保留此 原文链接。 非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。