从过去直到 React.lazy写一个没有 JSX 的 React执行上下文和执行栈公私有域和方法数组在性能方面的一个注意点从过去直到 React.lazycode-splitting当我们最最开始做前端开发的时候,JavaScript 文件自然就一个个罗列在一起,通过 script 标签引入到 html 里。当然,即使在现在,我们也还是会在写一些 Demo 时使用这样的方式。如今,我们有了如 Webpack、Parcel 等 Module bundler 来为我们更好的组织 JavaScript 文件。我们可以使用各种模块系统如 CommonJS(require、module.exports)或者 ES Modules(import、export)来定义文件之间的依赖。然而,随着我们的应用越来越大,我们就会得到一个巨大的 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 呢?Suspensereact-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 和各自异步的感觉。最后是不是可以准备改造一下项目了呢?源地址:https://hswolff.com/blog/reac…参考:https://reactjs.org/docs/code…写一个没有 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 的可读性原来还算好的了。源地址:https://codeburst.io/how-to-u…执行上下文和执行栈什么是执行上下文这可能是很多书本上都会讲的基础知识,这里我们也带一遍。执行上下文就是 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 引用的外部词汇环境组成。简单来说,词汇环境就是一种维护标识符到变量的映射,这里标识符指变量或函数的名字,而变量指的是一个实际对象(包括函数对象、数组对象)或基本值的引用。每个词汇环境由三部分组成:环境记录外部环境引用this binding我们还需要再继续展开讲:环境记录环境记录,就是变量和函数声明存储在词法环境中的位置。有两种环境记录:声明式环境记录对象环境记录前者主要就是存放变量、函数这类声明了的,后者则是对全局的代码进行记录,例如全局绑定的 window。注意,对于函数,环境记录还会包含 arguments 对象,用于映射传入函数的参数和记录传入参数的个数。我们举个例子就很明白了:function foo(a, b) { var c = a + b;}foo(2, 3);// argument objectArguments: {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 referenceconst 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。源地址:https://blog.bitsrc.io/unders…公私有域和方法这篇文章主要介绍 V8 v7.2 和 Chrome 72 新的 class fields 语法,以及即将出现的 private class fields。我们来创建一个 IncreasingCounter 实例:const counter = new IncreasingCounter();counter.value;// logs ‘Getting the current value!’// → 0counter.increment();counter.value;// logs ‘Getting the current value!’// → 1ES2015 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;// → 0counter._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;// → SyntaxErrorcounter.#count = 42;// → SyntaxErrorPublic 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.142857142857143FakeMath.random();// logs ‘I heard you like random numbers…’// → 4FakeMath.#totallyRandomNumber;// → SyntaxErrorFakeMath.#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/cre…「每日一瞥」是团队内部日常业界动态提炼,发布时效可能略有延后。文章可随意转载,但请保留此 原文链接。非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。