React 是 Javascript 库,其目标在于简化可视化界面的开发。学习它为何如此受欢迎,以及它解决了什么问题。
React 是一个 Javascript 库,其目标在于简化可视化界面的开发。
由 Facebook 开发并在 2013 年公布于世,它驱动着世界上使用最广泛的代码,为 Facebook 和 Instagram 以及其他许多软件公司提供支持。
Its primary goal is to make it easy to reason about an interface and its state in any point in time, by dividing the UI into a collection of components.
React 被用来构建单页 Web 应用,其中,有许多库和框架都先于 React 诞生。
React 已经风靡 Web 前端开发的世界。为什么呢?
React 面世的时候,Ember.js 和 Angular 1.x 作为框架是优秀的选择。这两者在代码中都增加了许多约定,对于移植现有应用来说根本不方便。React 很容易集成到现有应用中,因为他们只有在 Facebook 上这么做才能将其引进现有代码库。同时,这 2 个框架也带来了许多不足,因为 React 只选择实现视图层,而不是整个 MVC。
当时,谷歌宣布了 Angular 2.x,但它带来了重大变化及不向后兼容。从 Angular 1 迁移到 2 就像迁移到不同的框架,因此,随着 React 承诺的执行速度的改善,使得它成为开发者跃跃欲试的东西。
由 Facebook 支持显然对一个项目非常有益,但不保证,因为你能看到许多由 Facebook 和 Google 支持的失败的开源项目。
尽管我说过 React 比其替代框架更简单,深入 React 仍然是很复杂的,但大多数是因为和 React 集成的配套技术,例如 Redux、Relay 或者 GraphQL。
React 内部只有很少的 API。
React 中的主要概念有:
- Components
- JSX
- State
- Props
许多开发者,包括写这篇文章的人,第一眼看到 JSX 是非常可怕的,并且会迅速排斥 React。
尽管它们说 JSX 不是必须的,但使用没有 JSX 的 React 是痛苦的。
我花了几年时间偶尔看一下它才开始消化 JSX,现在我更喜欢它胜过替代方案,即:使用模板。
使用 JSX 最大的好处就是你只与 Javascript 对象交互,而不是模板字符串。
JSX 不是嵌入式的 HTML
许多针对 React 初学者的教程喜欢推迟 JSX 的介绍,因为他们假设读者没有它会更好,但是现在我是 JSX 的粉丝,我会立即跳到这个部分。
这里是你如何定义一个包含字符串的 h1 标记:
const element = <h1>Hello, world!</h1>;
它看起来像是奇怪的 HTML 和 Javascript 的混合,但实际上它全部是 Javascript。
看上去是 HTML,实际上是用来定义组件和它们在标记中的定位的语法糖。
在 JSX 表达式内,可以轻松添加属性:
const myId = 'test';
const element = <h1 id={myId}>Hello, world!</h1>;
你只需要注意当属性有横线(-
)时会被转换成小驼峰形式,这是 2 种特殊情况:
class
变成className
for
变成htmlFor
因为它们是 Javascript 中的关键字。
这是一段 JSX 片段,在 div
标记中包裹着两个组件:
<div>
<BlogPostList />
<Sidebar />
</div>
一个标签通常需要闭合,因为这是比 HTML 更加 XML 的语法(如果你记得 XHTML,则是很相似的,但是自那以后 HTML 弱语法胜出了)。在这种情况下使用了自闭合标签。
React 引入 JSX 不再是 React 独有的技术。
一个组件即是一个独立的接口。举个例子,在一个典型的博客主页面,你可能会找到 Sidebar 组件 和 Blog Post Lists 组件。它们又有组件本身组成,所以你可能会有一个 Blog post 组件的列表,每篇博客文章对应一个组件,而且每个组件拥有自身的属性。
cccc
React 使得这些非常简单:一切皆是组件。
甚至纯 HTML 标记本身也是组件,而且默认情况下它们已经被添加进来了。
下面这 2 行代码是等价的,因为它们做了同样的事情。一个有 JSX,另一个没有,都是在 id 为 app
的元素中注入 <h1>Hello World!</h1>
。
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<h1>Hello World!</h1>, document.getElementById('id'));
ReactDOM.render(
React.DOM.h1('Hello World!'),
document.getElementById(null, 'app')
);
看,React.DOM
给了我们一个 h1
组件。其它的 HTML 标记适用么?全部适用!你可以通过在浏览器控制台键入 React.DOM
来检查它提供了什么:
(截图省略,列表太长)
内建组件非常好,但你的需求很快就会超过它们。React 擅长的是通过构建自定义组件来组成我们的 UI。
在 React 种有两种方式定义组件。
无状态组件不管理内部状态,它只是一个函数:
const BlogPostExcerpt = () => {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
);
};
有的状态组件是一个 class,它管理自身属性中的状态。
import React, { Component } from 'react';
class BlogPostExcerpt extends Component {
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
);
}
}
它们是等价的因为现在还没有状态管理(将在下几篇文章中引入)。
这是第三种语法,它使用了 ES5
/ES2015
语法,没有 classes:
import React from 'react';
React.createClass({
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
);
}
});
你在现代 > ES6
的代码库中几乎看不到这种语法。
Props 是组件获取它们自身属性的方式。从顶层组件开始,每个子组件都从其父组件获取它的 props。在无状态组件中,它获取的 props 都是传递进来的,通过在函数参数中添加 props
就可以使用。
const BlopPostExcerpt = props => {
return (
<div>
<div>{props.title}</div>
<p>{props.description}</p>
</div>
);
};
在有状态组件中,props 是默认传递的。不需要做任何特别的处理,在组件实例中可以通过 this.props
访问。
import React, { Component } from 'react';
class BlogPostExcerpt extends Component {
redner() {
return (
<div>
<h1>{this.props.title}</h1>
<p>{this.props.description}</p>
</div>
);
}
}
将 props 向下传递给子组件是在应用程序中传值的好方法。组件既可以拥有数据(状态),也可以通过 props 接收数据。
以下情况会变得复杂:
- 你需要访问好几层以下的子组件的状态
- 你需要访问一个毫无关联的组件的状态
Redux 传统上非常流行,这也是许多教程中包含它的原因。
最近 React(16.3.0 版本)引入了 Context API,它使得 Redux 在这种简单的情况下有些冗余。
我们将在稍后讨论 Context API。
Redux 已经有用,如果你:
- 出于一些原因,要将数据迁移出应用程序
- 创建复杂的 reducers 和 actions 来以任何你想要的方式操作数据
但是现在 Redux 不再是 React 应用程序的“必需品”了。
注意,我把返回值用一个 div
组件只能返回一个单独的元素,如果你有多个元素,你就需要将其包裹在另外一个容器标签中。
无论怎样这都将导致在页面中产生不必要的 div
标签。你可以通过使用 React.Fragment
来避免这种情况:
import React, { Component } from 'react';
class BlogPostExcerpt extends Component {
render() {
return (
<React.Fragment>
<h1>{this.props.title}</h1>
<p>{this.props.description}</p>
</React.Fragment>
);
}
}
同时它有一个非常简短语法 <></>
,仅在最近的版本中支持(Babel 7+):
import React, { Component } from 'react';
class BlogPostExcerpt extends Component {
render() {
return (
<>
<h1>{this.props.title}</h1>
<p>{this.props.description}</p>
</>
);
}
}
由于 Javascript 是一门动态类型语言,我们没有办法在编译时规定变量的类型,而且当我们传递无效的类型时,它们将在运行时出错,或者虽然类型兼容,但并不是我们所期望的结果。
Flow 和 Typescript 能帮到许多,但是 React 有方法直接检查属性类型,甚至在代码运行之前,当我们传递了错误的值时,我们的工具(编辑器、linters)能够帮助我们检测到:
import PropTypes from 'prop-types';
import React, { Component } from 'react';
class BlogPostExcerpt extends Component {
render() {
return (
<div>
<h1>{this.props.title}</h1>
<p>{this.props.description}</p>
</div>
);
}
}
BlogPostExcerpt.propTypes = {
title: PropTypes.string,
description: PropTypes.string
};
export default BlogPostExcerpt;
这些是我们可接收的基本类型:
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
- PropTypes.symbol
我们可以接收两种类型之一:
PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
我们可以接收许多值之一:
PropTypes.oneOf(['Test1', 'Test2']);
我们可以接收一个类实例:
PropTypes.instanceOf(Something);
我们可以接收任何 React 节点:
PropTypes.node;
或者是任何类型:
PropTypes.any;
数组有一种特殊的语法,我们可以用来接收特定类型的数组:
PropTypes.arrayOf(PropTypes.string);
对象:
PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
});
把 isRequired
设置为任何 PropTypes 的属性时,如果这个属性丢失,React 将会返回错误:
PropTypes.arrayOf(PropTypes.string).isRequired;
PropTypes.string.isRequired;
如果任意一个值不是必须的,我们就需要为其指定一个默认值,以免在组件初始化时丢失:
BlogExcerpt.propTypes = {
title: PropTypes.String,
description: PropTypes.String
};
BlogExcerpt.defaultProps = {
title: '',
description: ''
};
一些像 ESLint 的工具能够强制定义组件中没有明确要求的 propTypes 的默认 props。
在初始化组件时,以类似于 HTML 属性的方式传递 props:
const desc = 'A description'
<BlogExcerpt title="A blog post" description={desc} />
我们将 title 作为普通字符串传递(这事儿我们只能以字符串来做),而 description 作为变量传递。
有一个特殊的 prop 是 children
。它包含着传递到组件 <body>
中的任何值,例如:
<BlogExcerpt title="A title" description="A description">
Something
</BlogExcerpt>
在这个例子中,我们可以通过 this.props.children
来访问 BlogExcerpt
内部的 “Something”。
Props 允许组件从它的父组件接受属性,例如被“指示”打印一些数据,state 允许组件从它自身获取数据,并且独立于周围的环境。
记住:只有基于类的组件才有 state,所以如果你要在无状态(基于函数)的组件中管理状态,你首先要将其“升级”为 Class 组件:
const BlogExcerpt = () => {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
);
};
变成:
import React, { Component } from 'react';
class BlogExcerpt extends Component {
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
);
}
}
在组件的构造函数中初始化 this.state
。例如 BlogExcerpt 组件可能会有一个 clicked
状态:
class BlogExcerpt extends Component {
constructor(props) {
super(props);
this.state = { clicked: false };
}
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
);
}
}
可以通过引用 this.state.clicked
来访问 clicked
状态:
class BlogExcerpt extends Component {
constructor(props) {
super(props);
this.state = { clicked: false };
}
render() {
return (
<div>
<h1>Title</h1>
<p>Description</p>
<p>Clicked: {this.state.clicked}</p>
</div>
);
}
}
改变状态应该永不使用:
this.state.clicked = false;
而是你应该总是使用 setState()
来代替,为其传递一个对象:
this.setState({ clicked: false });
这个对象应该包含状态的子集或者超集。只有你传递的属性才会被改变,省略的部分会保持现有的状态。
原因是使用这种方法,React 就会知道 state 已经被改变了。然后它会开始一系列的事件,这些事件将会促使组件重新渲染以及任何 DOM 更新。
一个组件的父组件是无法告诉其子组件是否为有状态或者无状态的,同样适用于组件的子组件。
有状态或无状态(基于类或函数)完全是实现细节,而其他组件不需要关心。
这就导致了单向数据流。
状态总是被组件所拥有。受此状态影响的数据只能影响其下方的组件:其子组件。
改变组件的状态永远不会影响它的父组件或者兄弟组件,或者任何其他组件:仅限于其子组件。
这也是状态经常被移动到组件树上的原因。
由于单向数据流原则,如果两个组件要共享一个状态,那么这个状态就需要向上移动到其共同的祖先。
许多时候最近的祖先是管理状态的好地方,但这不是强制性规定。
状态会通过 props 向下传递到需要它的组件上:
class Converter extends React.Component {
constructor(props) {
super(props);
this.state = { currency: '€' };
}
render() {
return (
<div>
<Display currency={this.state.currency} />
<CurrencySwitcher currency={this.state.currency} />
</div>
);
}
}
The state can be mutated by a child component by passing a mutating function down as a prop:
class Converter extends React.Component {
constructor(props) {
super(props);
this.state = { currency: '€' };
}
handleChangeCurrency = event => {
this.setState({
currency: this.state.currency === '€' ? '$' : '?'
});
};
render() {
return (
<div>
<Display currency={this.state.currency} />
<CurrencySwitcher
currency={this.state.currency}
handleChangeCurrency={this.handleChangeCurrency}
/>
</div>
);
}
}
const CurrencySwicher = props => {
return (
<button onClick={props.handleChangeCurrency}>
Current currency: {props.currency}. Change it!
</button>
);
};
const Display = props => {
return <p>Current currency: {props.currency}</p>;
};
React 提供了一种简单的方式管理事件。准备和 addEventListener
说再见 :)
在之前文章中关于状态的例子,你可以看到:
const CurrencySwitch = props => {
return (
<button onClick={props.handleChangeCurrency}>
Current currency: {props.currency}. Change it!
</button>
);
};
如果你使用 JavaScript 已经有一段时间了,这看起来像是普通的旧的 JavaScript 事件处理器,只是这次你在 JavaScript 中定义的一切,而不是 HTML,并且你传递的是一个函数,而非字符串。
事实上,事件名称有些许不同,因为在 React 中你使用了小驼峰形式,所以 onclick
变成 onClick
,onsubmit
变成了 onSubmit
。
参考:这是老式的 HTML 混合了 JavaScript 事件:
<button onclick="handleChangeCurrency()">
...
</button>
将事件处理程序定义在组件类上是一种约定:
class Converter extends React.Component {
handleChangeCurrency = event => {
this.setState({
currency: this.state.currency === '€' ? '$' : '?'
});
};
}
所有的处理程序都接收一个事件对象,该事件对象跨浏览器,并且遵守 W3C UI 事件规范。
不要忘记绑定方法。ES6 类的方法默认不绑定。这意味着除非你讲方法定义为箭头函数,否则 this
将是未定义的:
class Converter extends React.Component {
handleClick = event => {
/*...*/
};
// ...
}
当使用带有 Babel 的属性初始值设定语法时(在 create-react-app 中默认启用)会绑定 this
,否则,你需要在构造函数中手动绑定:
class Converter extends React.Component {
constructor(props) {
super(props);
this.hanleClick = this.handleClick.bind(this);
}
handleClick(e) {}
}
有大量事件已经被支持,这里是一个摘要列表:
- onCopy
- onCut
- onPaste
- onCompositionEnd
- onCompositionStart
- onCompositionUpdate
- onKeyDown
- onKeyPress
- onKeyUp
- onFocus
- onBlur
- onChange
- onInput
- onSubmit
- onClick
- onContextMenu
- onDoubleClick
- onDrag
- onDragEnd
- onDragEnter
- onDragExit
- onDragLeave
- onDragOver
- onDragStart
- onDrop
- onMouseDown
- onMouseEnter
- onMouseOver
- onMouseLeave
- onMouseMove
- onMouseOut
- onMouseUp
- onSelect
- onTouchCancel
- onTouchEnd
- onTouchMove
- onTouchStart
- onScroll
- onWheel
- onAbort
- onCanPlay
- onCanPlayThrough
- onDurationChange
- onEmptied
- onEncrypted
- onEnded
- onErroe
- onLoadedData
- onLoadedMetadata
- onLoadStart
- onPause
- onPlay
- onPlaying
- onProgress
- onRateChange
- onSeeked
- onStalled
- onSuspend
- onTimeUpdate
- onVolumeChange
- onWating
- onLoad
- onError
- onAnimationStart
- onAnimationEnd
- onAnimationIteration
- onTransitionEnd
你将会遇到将 React 描述为声明式构建 UIs 的方法的文章。
查看声明式编程来查看更多关于声明式编程。
React 使得“声明式方法”非常流行,因此它也随着 React 一起渗透到了前端开发的世界。
它真的是一个非常新的概念,而且 React 为构建 UIs 带来了比 HTML 模板更多的声明式:即使不用直接接触 DOM,你也可以构建 Web 接口,你拥有一个事件系统,为不用去和实际的 DOM 事件交互。
例如,使用 jQuery 或者 DOM 事件在 DOM 中查找元素是一种迭代方法。
React 的声明式为我们抽象了这些东西。我们只需要告诉 React 我们需要以一种特定的方式来渲染一个组件,而且我们可以永远不和 DOM 交互。
许多现存的,在 React 出现之前的框架,在每次更改时都会直接操作 DOM。
什么是 DOM,首先 DOM(Document Object Model)是页面的树表示,从 <html>
标签开始,向下进入每个子节点,称之为节点。
它保存在浏览器内存中,并且直接链接到你在页面中看到的内容。DOM 有一套 API 供你使用,可以访问每个单独的节点,筛选它们,修改它们。
API 你可能见过很多次,如果你没有使用 jQuery 提供的抽象 API:
document.getElementById(id);
document.getElementsByTagName(name);
document.createElement(name);
parentNode.appendChild(node);
element.innerHTML;
element.style.left;
element.getAttribute();
element.setAttribute();
element.addEventListener();
window.content;
window.onload;
window.dump();
window.scrollTo();
React 保留了 DOM 表示的副本,用来解决 React 的渲染:Virtual DOM。
每次 DOM 发生变化,浏览器都会去做两个密集的操作:重绘()和重排()。
当需要在页面上发生变动时,React 使用 Virtual DOM 来帮助浏览器使用更少的资源。
当你在组件上调用 setState()
时,指定了一个与之前不同的状态,React 会将组件标记为脏的。这是关键:React 只会更新状态发生明确变化的组件。
接下来会发生:
- React 会更新与标记为脏组件(一些额外的检查,例如触发
shouldComponentUpdate()
)相关联的 Virtual DOM - 运行 Diff 算法来协调更改
- 更新真实 DOM
事情的关键在于,React 通过更改所有需要同时更新的元素来批量处理大部分改动,并且对真实 DOM 执行唯一性更新,所以浏览器必须执行的重绘和重排(用以呈现更改)就只执行一次。
Context API 的引入允许你在应用程序中传递 state,而不必再使用 props。
如果你只有少量的几个层级的子元素需要传递 state,React 团队建议仍然使用 props,因为这是比 Context API 更简单的方案。
在许多情况下,它能避免我们使用 Redux 以简化我们的应用程序,并且也可以学习如何使用 React。
它是怎样工作的呢?
使用 React.createContext()
创建上下文,这个方法会返回一个 Context 对象:
const { Provider, Consumer } = React.createContext();
然后再创建一个返回 Provider 组件的包装组件,然后将其添加为你需要访问这个上下文的所有组件的子元素:
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
something: 'hey'
};
}
render() {
return (
<Provider value={{ state: this.state }}>{this.props.children}</Provider>
);
}
}
class HelloWorld extends React.Component {
render() {
return (
<Container>
<Button />
</Container>
);
}
}
我使用名为 Container 的组件,因为它是一个全局的 provider。你也可以创建更小的上下文。
在由 Provider 包装的组件的内部,你可以使用 Comsumer 组件来使用上下文:
class Button extends React.Component {
render() {
return (
<Consumer>
{context => <button>{context.state.something}</button>}
</Consumer>
);
}
}
你也可以为 Provider 的 value 传递函数,这些函数将会被 Consumer 组件用来更新上下文的状态:
<Provider value={{
state: this.state,
updateSomething: () => this.setState({ something: 'ho!' })
}}>
{this.props.children}
</Provider>
/*...*/
<Consumer>
{(context) => (
<button onClick={context.updateSomething}>{context.state.something}</button>
)}
</Consumer>
你可以在 Glitch) 查看。
你可以创建多个上下文,让状态在组件间共享,然后公开它们,在你想要的任何组件中访问。
当使用多个文件时,你可以将内容创建在一个文件中,然后在你要使用的任何地方导入它:
// context.js
import React from 'react'
export default React.createContext()
// component1.js
import Context from './context'
// use Context.Provider
// component2.js
import Context from './context'
// use Context.Provider