React 和JavaScript 类
关于 React 类组件,需要用到有关 JavaScript 类的先验知识。JavaScript 类的概念相对较新。之前,只有 JavaScript 的原型链可用于实现继承。JavaScript 类以原型继承为基础,让继承体系变得更简单。
定义 React 组件的一种方法是使用 JavaScript 类。
class Developer {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
getName() {
return this.firstname + ' ' + this.lastname;
}
}
var me = new Developer('Robin', 'Wieruch');
console.log(me.getName());
一个类描述了一个实体,用于创建实体的实例。在使用 new 语句创建类的实例时,会调用这个类的构造函数。类的属性通常位于构造函数中。此外,类方法(例如 getName())用于读取(或写入)实例的数据。类的实例在类中使用 this 对象来表示,但在外部,仅指定给 JavaScript 变量。
在面向对象编程中,类通常用来实现继承。在 JavaScript 中也一样,extends 语句可用于让一个类继承另一个类。一个子类通过 extends 语句继承了一个父类的所有功能,还可以添加自己的功能。
class Developer {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
getName() {
return this.firstname + ' ' + this.lastname;
}
}
class ReactDeveloper extends Developer {
getJob() {
return 'React Developer';
}
}
var me = new ReactDeveloper('Robin', 'Wieruch');
console.log(me.getName());
console.log(me.getJob());
基本上,要理解 React 的类组件,知道这些就够了。JavaScript 类用于定义 React 组件,React 组件继承了从 React 包导入的 React Component 类的所有功能。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div>
<h1>Welcome to React</h1>
</div>
);
}
}
export default App;
这就是为什么 render() 方法在 React 类组件中是必需的:从 React 包导入的 React Component 用它在浏览器中显示某些内容。此外,如果不从 React Component 继承,将无法使用其他生命周期方法(包括 render() 方法)。例如,如果不继承,那么 componentDidMount() 生命周期方法就不存在,因为现在这个类只是一个普通 JavaScript 类的实例。除了生命周期方法不可用,React 的 API 方法(例如用于本地状态管理的 this.setState())也不可用。
我们可以通过 JavaScript 类来扩展通用类的行为。因此,我们可以引入自己的类方法或属性。
import React, { Component } from 'react';
class App extends Component {
getGreeting() {
return 'Welcome to React';
}
render() {
return (
<div>
<h1>{this.getGreeting()}</h1>
</div>
);
}
}
export default App;
现在你应该知道为什么 React 使用 JavaScript 类来定义 React 类组件。当你需要访问 React 的 API(生命周期方法、this.state 和 this.setState())时,可以使用它们。接下来,你将看到如何以不同的方式定义 React 组件,比如不使用 JavaScript 类,因为有时候你可能不需要使用类方法、生命周期方法或状态。
尽管我们可以在 React 中使用 JavaScript 类继承,但这对于 React 来说不是一个理想的结果,因为 React 更倾向于使用组合而不是继承。因此,你的 React 组件需要扩展的唯一类应该是 React Component。
React 中的箭头函数
箭头函数是 ES6 新增的语言特性之一,让 JavaScript 向函数式编程更近了一步。
// JavaScript ES5 function
function getGreeting() {
return 'Welcome to JavaScript';
}
// JavaScript ES6 arrow function with body
const getGreeting = () => {
return 'Welcome to JavaScript';
}
// JavaScript ES6 arrow function without body and implicit return
const getGreeting = () =>
'Welcome to JavaScript';
在 React 应用程序中使用 JavaScript 箭头函数通常是为了让代码保持简洁和可读。我很喜欢箭头函数,总是尝试将我的函数从 JavaScript ES5 重构成 ES6。在某些时候,当 JavaScript ES5 函数和 JavaScript ES6 函数之间的差异很明显时,我会使用 JavaScript ES6 的箭头函数。不过,对 React 新手来说,太多不同的语法可能会让人不知所措。因此,在 React 中使用它们之前,我会尝试解释 JavaScript 函数的不同特点。在以下部分,你将了解到如何在 React 中使用 JavaScript 箭头函数。
在React 中将函数视为组件
React 使用了不同的编程范式,这要归功于 JavaScript 是一门“多面手”编程语言。在面向对象编程方面,React 的类组件可以很好地利用 JavaScript 类(React 组件 API 的继承、类方法和类属性,如 this.state)。另一方面,React(及其生态系统)也使用了很多函数式编程的概念。例如,React 的函数无状态组件是另一种定义 React 组件的方式。那么,如果可以像函数那样使用组件,将会怎样?
function (props) {
return view;
}
这是一个接收输入(例如 props)并返回 HTML 元素(视图)的函数。它不需要管理任何状态(无状态),也不需要了解任何方法(类方法、生命周期方法)。这个函数只需要使用 React 组件的 render() 方法来进行渲染。
function Greeting(props) {
return <h1>{props.greeting}</h1>;
}
功能无状态组件是在 React 中定义组件的首选方法。它们的样板代码较少,复杂性较低,并且比 React 类组件更易于维护。不过,这两者都有自己存在的理由。
之前提到了 JavaScript 箭头函数以及它们可以提升代码的可读性,现在让我们将这些函数应用无状态组件中。之前的 Greeting 组件在 JavaScript ES5 和 ES6 中的写法有点不一样:
// JavaScript ES5 function
function Greeting(props) {
return <h1>{props.greeting}</h1>;
}
// JavaScript ES6 arrow function
const Greeting = (props) => {
return <h1>{props.greeting}</h1>;
}
// JavaScript ES6 arrow function without body and implicit return
const Greeting = (props) =>
<h1>{props.greeting}</h1>
JavaScript 箭头函数是让 React 无状态组件保持简洁的一个不错的方法。
React 类组件语法
React 定义组件的方式一直在演化。在早期阶段,React.createClass() 方法是创建 React 类组件的默认方式。现在不再使用这个方法,因为随着 JavaScript ES6 的兴起,之前的 React 类组件语法成为默认语法。
不过,JavaScript 也在不断发展,因此 JavaScript 爱好者一直在寻找新的方式。这就是为什么你会发现 React 类组件使用了不同的语法。使用状态和类方法来定义 React 类组件的一种方法如下:
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
};
this.onIncrement = this.onIncrement.bind(this);
this.onDecrement = this.onDecrement.bind(this);
}
onIncrement() {
this.setState(state => ({ counter: state.counter + 1 }));
}
onDecrement() {
this.setState(state => ({ counter: state.counter - 1 }));
}
render() {
return (
<div>
<p>{this.state.counter}</p>
<button onClick={this.onIncrement} type="button">Increment</button>
<button onClick={this.onDecrement} type="button">Decrement</button>
</div>
);
}
}
不过,在实现大量的 React 类组件时,构造函数中的类方法绑定和构造函数本身就变成了繁琐的实现细节。所运的是,有一个简短的语法可用来摆脱这两个烦恼:
class Counter extends Component {
state = {
counter: 0,
};
onIncrement = () => {
this.setState(state => ({ counter: state.counter + 1 }));
}
onDecrement = () => {
this.setState(state => ({ counter: state.counter - 1 }));
}
render() {
return (
<div>
<p>{this.state.counter}</p>
<button onClick={this.onIncrement} type="button">Increment</button>
<button onClick={this.onDecrement} type="button">Decrement</button>
</div>
);
}
}
通过使用 JavaScript 箭头函数,可以自动绑定类方法,不需要在构造函数中绑定它们。通过将状态直接定义为类属性,在不使用 props 时就可以省略构造函数。(注意:请注意,JavaScript 还不支持类属性。)因此,你可以说这种定义 React 类组件的方式比其他版本更简洁。
React 中的模版字面量
模板字面量是 JavaScript ES6 附带的另一种 JavaScript 语言特性。之所以提到这个特性,是因为当 JavaScript 和 React 新手看到它们时,可能会感到困惑。以下面的连接字符串的语法为例:
function getGreeting(what) {
return 'Welcome to ' + what;
}
const greeting = getGreeting('JavaScript');
console.log(greeting);
// Welcome to JavaScript
// 模板字面量可以用于达到相同的目的,被称为字符串插值:
function getGreeting(what) {
return `Welcome to ${what}`;
}
// 你只需使用反引号和 ${}来插入 JavaScript 原语。字符串字面量不仅可用于字符串插值,还可用于多行字符串:
function getGreeting(what) {
return `
Welcome
to
${what}
`;
}
// 这样就可以格式化多行文本块。
React 中Map、Reduce 和Filter
在向 React 新手教授 JSX 语法时,我通常会先在 render() 方法中定义一个变量,然后将其用在返回代码块中。
import React, { Component } from 'react';
class App extends Component {
render() {
var greeting = 'Welcome to React';
return (
<div>
<h1>{greeting}</h1>
</div>
);
}
}
export default App;
你只需使用花括号来操作 HTML 中的 JavaScript。不管是渲染字符串还是渲染一个复杂的对象,并没有太大不同。
import React, { Component } from 'react';
class App extends Component {
render() {
var user = { name: 'Robin' };
return (
<div>
<h1>{user.name}</h1>
</div>
);
}
}
export default App;
接下来的问题是:如何渲染项目列表?React 没有提供特定的 API(例如 HTML 标记的自定义属性)用于渲染项目列表。我们可以使用纯 JavaScript 代码来迭代项目列表,并返回每个项目的 HTML。
import React, { Component } from 'react';
class App extends Component {
render() {
var users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
return (
<ul>
{users.map(function (user) {
return <li>{user.name}</li>;
})}
</ul>
);
}
}
export default App;
通过使用 JavaScript 箭头函数,你可以摆脱箭头函数体和 return 语句,让渲染输出更加简洁。
import React, { Component } from 'react';
class App extends Component {
render() {
var users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
return (
<ul>
{users.map(user => <li>{user.name}</li>)}
</ul>
);
}
}
export default App;
很快,每个 React 开发人员都习惯了 JavaScript 内置的 map() 方法。对数组进行 map 并返回每个项的渲染输出,这样做非常有用。在某系情况下,结合使用 filter() 或 reduce() 会更有用,而不只是为每个被 map 的项渲染输出。
import React, { Component } from 'react';
class App extends Component {
render() {
var users = [
{ name: 'Robin', isDeveloper: true },
{ name: 'Markus', isDeveloper: false },
];
return (
<ul>
{users
.filter(user => user.isDeveloper)
.map(user => <li>{user.name}</li>)
}
</ul>
);
}
}
export default App;
通常,React 开发人员习惯于使用 JavaScript 的这些内置函数,而不必使用 React 特定的 API。它只是 HTML 中的 JavaScript。
React 中的var、let和 const
对于 React 的新手来说,使用 var、let 和 const 来声明变量可能也会给他们造成混淆,虽然它们不是 React 相关的。我会尝试在教学中尽早介绍 let 和 const,并从在 React 组件中交替使用 const 和 var 开始:
import React, { Component } from 'react';
class App extends Component {
render() {
const users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
return (
<ul>
{users.map(user => <li>{user.name}</li>)}
</ul>
);
}
}
export default App;
然后我给出了一些使用这些变量声明的经验法则:
- 不要使用 var,因为 let 和 const 更具体
- 默认使用 const,因为它不能被重新分配或重新声明
- 如果要重新赋值变量则使用 let
let 通常用于 for 循环中,const 通常用于保持 JavaScript 变量不变。尽管在使用 const 时可以修改对象和数组的内部属性,但变量声明表达了保持变量不变的意图。
React 中的三元运算符
如果要通过 if-else 语句进行条件渲染该怎么办?我们不能直接在 JSX 中使用 if-else 语句,但可以从渲染函数中提前返回。如果不需要显示内容,返回 null 在 React 中是合法的。
import React, { Component } from 'react';
class App extends Component {
render() {
const users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
const showUsers = false;
if (!showUsers) {
return null;
}
return (
<ul>
{users.map(user => <li>{user.name}</li>)}
</ul>
);
}
}
export default App;
不过,如果要在返回的 JSX 中使用 if-else 语句,可以使用 JavaScript 的三元运算符:
import React, { Component } from 'react';
class App extends Component {
render() {
const users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
const showUsers = false;
return (
<div>
{
showUsers ? (
<ul>
{users.map(user => <li>{user.name}</li>)}
</ul>
) : (
null
)
}
</div>
);
}
}
export default App;
如果你只返回条件渲染的一个方面,可以使用 && 运算符:
import React, { Component } from 'react';
class App extends Component {
render() {
const users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
const showUsers = false;
return (
<div>
{
showUsers && (
<ul>
{users.map(user => <li>{user.name}</li>)}
</ul>
)
}
</div>
);
}
}
export default App;
详细的原理我就不说了,但如果你感兴趣,可以在这篇文章(https://www.robinwieruch.de/conditional-rendering-react/)里了解到更详细的信息以及与条件渲染相关的其他技术。React 中的条件渲染告诉我们,大多数 React 都是与 JavaScript 有关,而不是 React 特定的内容。
React 中的导入和导出语句
在 JavaScript 中,我们可以通过 import 和 export 语句来导入和导出在 JavaScript ES6 文件中定义的功能。在开始你的第一个 React 应用程序之前,这些 import 和 export 语句是另一个需要了解的话题。create-react-app 项目已经在使用 import 语句:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
这对初始项目来说非常棒,因为它为你提供了一个全面的体验,可以导入和导出其他文件。不过,在刚开始接触 React 时,我会试着避免这些导入。相反,我会专注于 JSX 和 React 组件。在需要将 React 组件或 JavaScript 函数分离到单独的文件中时,才需要引入导入和导出语句。
那么这样使用这些导入和导出语句呢?假设你想要导出一个文件的如下变量:
const firstname = 'Robin';
const lastname = 'Wieruch';
export { firstname, lastname };
然后,你可以通过第一个文件的相对路径将它们导入到另一个文件中:
import { firstname, lastname } from './file1.js';
console.log(firstname);
// output: Robin
因此,它不一定只是与导入或导出组件或函数有关,它可以是共享可分配给变量的所有东西(我们只谈 JS)。你还可以将另一个文件导出的所有变量作为一个对象导入:
import * as person from './file1.js';
console.log(person.firstname);
// output: Robin
导入可以有别名。当从多个文件导入具有相同导出名称的功能时,就需要用到别名。
import { firstname as username } from './file1.js';
console.log(username);
// output: Robin
之前所有的例子都是命名的导入和导出。除此之外,还有默认的导入和导出。它可以用于以下一些场景:
- 导出和导入单个功能;
- 强调一个模块导出 API 的主要功能;
- 作为导入功能的后备。
const robin = {
firstname: 'Robin',
lastname: 'Wieruch',
};
export default robin;
在使用默认导入时可以省略大括号:
import developer from './file1.js';
console.log(developer);
// output: { firstname: 'Robin', lastname: 'Wieruch' }
此外,导入名称可以与导出的默认名称不同。你还可以将它与命名的 export 和 import 语句一起使用:
const firstname = 'Robin';
const lastname = 'Wieruch';
const person = {
firstname,
lastname,
};
export {
firstname,
lastname,
};
export default person;
在另一个文件中导入:
import developer, { firstname, lastname } from './file1.js';
console.log(developer);
// output: { firstname: 'Robin', lastname: 'Wieruch' }
console.log(firstname, lastname);
// output: Robin Wieruch
// 你还可以节省一些行,直接导出命名的变量:
export const firstname = 'Robin';
export const lastname = 'Wieruch';
这些是 ES6 模块的主要功能。它们可以帮助你更好地组织代码,并设计出可重用的模块 API。
React 中的库
React 只是应用程序的视图层。React 提供了一些内部状态管理,但除此之外,它只是一个为浏览器渲染 HTML 的组件库。API(例如浏览器 API、DOM API)、JavaScript 或外部库可以为 React 添加额外的东西。为 React 应用程序选择合适的库并不是件容易的事,但一旦你对不同的库有了很好的了解,就可以选择最适合你的技术栈的库。
例如,我们可以使用 React 原生的获取数据的 API 来获取数据:
import React, { Component } from 'react';
class App extends Component {
state = {
data: null,
};
componentDidMount() {
fetch('https://api.mydomain.com')
.then(response => response.json())
.then(data => this.setState({ data }));
}
render() {
...
}
}
export default App;
但你也可以使用另一个库来获取数据,Axios 就是这样的一个流行库:
import React, { Component } from 'react';
import axios from 'axios';
class App extends Component {
state = {
data: null,
};
componentDidMount() {
axios.get('https://api.mydomain.com')
.then(data => this.setState({ data }));
}
render() {
...
}
}
export default App;
因此,一旦你知道了需要解决什么问题,React 的生态系统就可以为你提供大量的解决方案。这可能与 React 本身无关,而是有关了解如何选择可用于弥补 React 应用程序的各种 JavaScript 库。
React 中的高阶函数
高阶函数是函数式编程中的一个非常棒的概念。在 React 中,了解这些函数是非常有意义的,因为在某些时候你需要处理高阶组件,如果已经了解了高阶函数,那么就可以更好地了解这些高阶组件。
我们假设可以根据一个输入字段的值对用户列表进行过滤。
import React, { Component } from 'react';
class App extends Component {
state = {
query: '',
};
onChange = event => {
this.setState({ query: event.target.value });
}
render() {
const users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
return (
<div>
<ul>
{users
.filter(user => this.state.query === user.name)
.map(user => <li>{user.name}</li>)
}
</ul>
<input
type="text"
onChange={this.onChange}
/>
</div>
);
}
}
export default App;
我们并不总是希望通过提取函数的方式来实现,因为这样会增加不必要的复杂性。但是,通过提取函数,我们可以对其进行单独的测试。因此,让我们使用内置的 filter 函数来实现这个例子。
import React, { Component } from 'react';
function doFilter(user) {
return query === user.name;
}
class App extends Component {
...
render() {
const users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
return (
<div>
<ul>
{users
.filter(doFilter)
.map(user => <li>{user.name}</li>)
}
</ul>
<input
type="text"
onChange={this.onChange}
/>
</div>
);
}
}
export default App;
这个实现还起不到作用,因为 doFilter() 函数需要知道 state 的 query 属性。我们可以通过另一个包装函数来传递它,也就是高阶函数。
import React, { Component } from 'react';
function doFilter(query) {
return function (user) {
return query === user.name;
}
}
class App extends Component {
...
render() {
const users = [
{ name: 'Robin' },
{ name: 'Markus' },
];
return (
<div>
<ul>
{users
.filter(doFilter(this.state.query))
.map(user => <li>{user.name}</li>)
}
</ul>
<input
type="text"
onChange={this.onChange}
/>
</div>
);
}
}
export default App;
基本上,高阶函数是可以返回函数的函数。通过使用 JavaScript ES6 的箭头函数,你可以让高阶函数变得更简洁。此外,这种简化的方式让将函数组合成函数变得更吸引人。
const doFilter = query => user =>
query === user.name;
现在可以将 doFilter() 函数从文件中导出,并将其作为纯(高阶)函数进行单独的测试。在了解了高阶函数之后,就为学习 React 的高阶组件奠定了基础。
将这些函数提取到 React 组件之外的(高阶)函数中也助于单独测试 React 的本地状态管理。
export const doIncrement = state =>
({ counter: state.counter + 1 });
export const doDecrement = state =>
({ counter: state.counter - 1 });
class Counter extends Component {
state = {
counter: 0,
};
onIncrement = () => {
this.setState(doIncrement);
}
onDecrement = () => {
this.setState(doDecrement);
}
render() {
return (
<div>
<p>{this.state.counter}</p>
<button onClick={this.onIncrement} type="button">Increment</button>
<button onClick={this.onDecrement} type="button">Decrement</button>
</div>
);
}
}
函数式编程非常强大,转向函数式编程有助于了解 JavaScript 将函数作为一等公民所带来的好处。
React 中的解构和展开运算符
JavaScript 中引入的另一种语言特性称为解构。通常情况下,你需要在组件的 state 或 props 中访问大量的属性。你可以在 JavaScript 中使用解构赋值,而不是逐个将它们分配给变量。
// no destructuring
const users = this.state.users;
const counter = this.state.counter;
// destructuring
const { users, counter } = this.state;
这对函数式无状态组件来说特别有用,因为它们可以在函数签名中收到 props 对象。通常,你用到的不是 props,而是 props 里的内容,因此你可以对函数签名中已有的内容进行解构。
// no destructuring
function Greeting(props) {
return <h1>{props.greeting}</h1>;
}
// destructuring
function Greeting({ greeting }) {
return <h1>{greeting}</h1>;
}
解构也适用于 JavaScript 数组。另一个很棒的特性是剩余解构。它通常用于拆分对象的一部分属性,并将剩余属性保留在另一个对象中。
// rest destructuring
const { users, ...rest } = this.state;
uesrs 可以在 React 组件中渲染,而剩余状态可以用在其他地方。这就是 JavaScript 展开(spread)运算发挥作用的地方,它可以将对象的剩余部分转到下一个组件。
JavaScript 多过 React
React 只提供了一个细小的 API 表面区域,因此开发人员必须习惯于 JavaScript 提供的所有功能。这句话并非没有任何理由:“成为 React 开发者也会让你成为更好的 JavaScript 开发者”。让我们通过重构一个高阶组件来回顾一下学到的 JavaScript 的一些方面。
function withLoading(Component) {
return class WithLoading extends {
render() {
const { isLoading, ...props } = this.props;
if (isLoading) {
return <p>Loading</p>;
}
return <Component { ...props } />;
}
}
};
}
这个高阶组件用于显示条件加载进度条,当 isLoading 被设为 true 时,就可以显示加载进度条,否则就渲染输入组件。在这里可以看到(剩余)解构和展开运算符的实际应用。后者可以在渲染的 Component 中看到,因为 props 对象的剩余属性被传给了那个 Component。
让高阶组件变得更简洁的第一步是将返回的 React 类组件重构为函数式无状态组件:
function withLoading(Component) {
return function ({ isLoading, ...props }) {
if (isLoading) {
return <p>Loading</p>;
}
return <Component { ...props } />;
};
}
可以看到,剩余解构也可以被用在函数的签名中。接下来,使用 JavaScript ES6 箭头函数让高阶组件变得更加简洁:
const withLoading = Component => ({ isLoading, ...props }) => {
if (isLoading) {
return <p>Loading</p>;
}
return <Component { ...props } />;
}
通过使用三元运算符可以将函数体缩短为一行代码。因此可以省略函数体和 return 语句。
const withLoading = Component => ({ isLoading, ...props }) =>
isLoading
? <p>Loading</p>
: <Component { ...props } />
如你所见,高阶组件使用的是各种 JavaScript 而不是 React 相关技术:箭头函数、高阶函数、三元运算符、解构和展开运算符。