资讯专栏INFORMATION COLUMN

「每日一瞥

qujian / 1101人阅读

摘要:当引擎开始执行脚本是的时候,会先创建一个全局执行上下文,并将其到当前执行栈,无论何时一个函数被调用,就会创建一个新的函数执行上下文并压入栈中。当函数执行完毕,执行栈会将其弹出,并把控制权交给当前栈的下一个上下文。

从过去直到 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 (
    

My Movie

); }

现在我们来开始看看,如何让我们的 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 (
      

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/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 
}

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 (
      

{this.state.num}

) } } ReactDOM.render(, 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 = ,
  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/unders...

公私有域和方法

这篇文章主要介绍 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.            
               
                                           
                       
                 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100756.html

相关文章

  • 每日一瞥

    摘要:目前前端主要有以下四种方法会触发对应的回调方法方法客户端回调客户端回调参考地址每日一瞥是团队内部日常业界动态提炼,发布时效可能略有延后。 showImg(https://segmentfault.com/img/remote/1460000017975436?w=1200&h=630); 「ES2015 - ES2018」Rest / Spread Properties 梳理 Thr...

    xiangzhihong 评论0 收藏0
  • 每日一瞥

    摘要:另一部分就是类型的属性,也就是返回的属性。,一开始提案为,其作用就是递归的将数组展平到指定深度,默认深度为。目前在使用时,我们唯一的选择是命令行界面。 showImg(https://segmentfault.com/img/remote/1460000017516912?w=1200&h=630); TypeScript 3.3 更新梳理 Object.assign vs Obje...

    刘明 评论0 收藏0
  • 每日 30 秒 ⏱ 大家一起被捕吧

    showImg(https://segmentfault.com/img/remote/1460000018793640?w=900&h=500); 简介 安全、注入攻击、XSS 13岁女学生被捕:因发布 JavaScript 无限循环代码。 这条新闻是来自 2019年3月10日 很多同学匆匆一瞥便滑动屏幕去看下一条消息了,并没有去了解这段代码是什么,怎么办才能防止这个问题。事情发生后为了抗议日本...

    lbool 评论0 收藏0
  • 每日一瞥

    摘要:首先,我们需要一个基本框架来处理表单域变化和表格提交。最起码我们需要提供一个来告诉如果用户还没有对表单域进行改动,就不必展示错误。我们需要一个来标识用户已尝试提交表单,还需要来标识表单是否正在提交以及每个表单域是否正在进行异步校验。 showImg(https://segmentfault.com/img/remote/1460000017516912?w=1200&h=630); ...

    XboxYan 评论0 收藏0
  • Django静态文件一瞥

    摘要:配置在设置项中确认包含增加设置项,值为一个字符串路径,必须以结尾在模板中这样引用在的目录存放静态文件开发期间使用极度低效时有别的做法注意默认为,一个列表,表示独立于的静态文件存放位置。 配置 1.在INSTALLED_APPS设置项中确认包含django.contrib.staticfiles 2.增加STATIC_URL设置项,值为一个字符串(路径),必须以‘/’结尾 3.在模板中...

    binta 评论0 收藏0

发表评论

0条评论

qujian

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<