# React 基础
先看看 react 主要的几个特点
- 声明式:以声明式编写 UI,代码更加可靠,且方便调试
- 组件化:构建管理自身状态的封装组件,然后对齐组合以构成复杂 UI。组件的逻辑使用 JavaScript 进行编写而不使用模板,可以轻松的传递数据,并保持 DOM 的分离。
声明式编程和命令式编程
命令式编程描述代码如何工作,而声明式编程则表明想要实现什么目的。
- 给我来杯啤酒
命令式用生活中的一个例子来说,就好比当你去和服务员要一杯啤酒时对服务员做这样的指示:
- 从架子拿一个玻璃杯
- 将杯子放到酒桶前
- 打开酒桶开关,将杯子装满
- 把杯子递给我
如果用的是声明式,你只需要说:“请给我一杯啤酒”。使用这种方式来点啤酒,我们需要假设服务员知道如何提供啤酒。这是声明式编程工作的一个重要方面。
- JavaScript 将数组元素大写字符串转成小写
再看看一个 JavaScript 的例子,实现一个将数组中的元素的大写字符串转成小写字符串
toLowerCase(['Foo', 'BAR']); // ['foo', 'bar']
解决这个问题,用命令式来实现就是:
const toLowerCase = input => {
const output = [];
for (let i = 0; i < input.length; i++) {
output.push(input[i].toLowerCase())
}
return output()
}
首先我们需要定一个数组,然后遍历输入数组的所有元素,把它们转为小写之后 push
新的数组中,最后将新的数组返回。
如果使用的是声明式:
const toLowerCase = input => input.map(value => value.toLowerCase())
输入的数组的元素会传递到 map
中,然后 map
函数会返回小写值的数组。
相对于之前的方法来说,这个更加的优雅、简洁和易读。另外,声明式编程无需使用变量,也不用在执行过程中持续更新变量的值。事实上,声明式编程往往避免了创建和修改状态。
- 在 React 的 UI 中
在开发中实现一个这样的需求,展示带有标记的地图。使用 JavaScript 的实现如下:
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: myLatLng,
})
const marker = new google.maps.Marker({
position: myLatLng,
title: 'Hello World!',
})
marker.setMap(map)
这显然是编程式的,在代码中我们逐条描述了创建地图、创建标志、以及在地图上添加标志的指令。
改用 React 组件,那么方式就是:
<Gmaps zoom={4} center={myLatLng}>
<Marker position={myLatlng} title="Hello World!" />
</Gmaps>
使用声明式,我们只需要描述我们想要的功能,无须列出实现效果的所有步骤。
声明式的 UI 使得 React 很容易使用,最终的代码也比较简单,这样产生的 bug 更少。
# 1. JSX 语法
所谓的 JSX,就是 JavaScript 的语法扩展(eXtension)。它看起来和 HTML 比较类似,但它并不局限于 HTML 中的元素
- 它的元素可以是任何一个 React 组件。React 判断一个元素是 HTML 元素还是 React 组件的原则就是看第一个字母是否是大写。
- 在 JSX 可以通过 onClick 这样的方式给一个元素添加一个事件处理函数。当然在 HTML 中也可以用 onclick,但在 HTML 中直接写 onclick 一直就是为人诟病的,会容易让人混乱,比较倡导使用 jQuery 的方法来添加处理函数。
使用的是 JSX 语法,最后经过 babel 编译成 JavaScript 来执行。
# 为什么使用 JSX
既然不推荐在 HTML 直接使用 onclick,那为什么 JSX 中我们缺使用这样的方式来添加事件处理函数呢?
以前我们
- 用 HTML 来代表内容
- 用 CSS 来代表样式
- 用 JavaScript 来定义交互行为
然后将这三种语言分在不同的文件中,实际上是“把技术分开管理,而不是分而治之”。
React 认为渲染逻辑本质和其他的 UI 逻辑有内在耦合,比如 UI 中需要绑定事件,在某些状态更新需要通知 UI。React 没有将标记(HTML)与逻辑进行分离到不同文件中这种人为的分离方式,而是通过将两者放在称为“组件”的松散耦合单元中,来实现关注点的分离。
做同一件事的代码应该高耦合,应该把实现这个功能的所有代码集中在一个文件里。除了在组件中定义交互行为,还可以在 React 组件中定义样式。
通过 React 组件可以把 JavaScript、HTML 和 CSS 的功能都写在一个文件中,实现了真正的组件封装。
class List extends React.Component {
constructor(props) {
super(props);
this.onClickButton = this.onClickButton.bind(this);
this.state = {count: 0};
}
onClickButton() {
this.setState({count: this.state.count + 1})
}
render() {
const countStyle = {
fontSize: '14px'
}
return (
<div className="list">
<button onClick={this.onClickButton}>Click Me</button>
<div style={countStyle}>
Click Count: <span id="clickCount">{this.state.count}</span>
</div>
</div>
)
}
}
会被编译成
return React.createElement('div', {className: 'list'},
React.createElement('button', /*... h1 children */),
React.createElement('div', /*... ul children */),
)
最后会创建这样的对象,它们就是“react 的元素”,描述你所在页面上看到的内容。react 通过读取这些对象,用来创建最后的 DOM,并保持随时更新。
{
type: 'div',
props: {
className: 'list',
children: [
//...
]
}
}
# Virtual DOM
接下来我们看看 react 的工作理念是怎么样的。这里我们用 jQuery 来对比一下
<html>
<body>
<button id="clickMe">Click Me</button>
<div>Click Count: <span id="clickCount">0</span>
<script src="https://code.jquery.com/jquery-1.12.4.min.js">
<script src="./clickCounter.js"></script>
</body>
</html>
// clickCounter.js
$(function() {
$('#clickMe').click(function() {
const $clickCount = $('#clickCount');
const count = parseInt($clickCount.text(), 10);
$clickCount.text(count + 1);
})
})
在👆这种实现的方式中,是一种命令式的方式,首先我们找到了按钮,然后挂上一个匿名函数。在事件的处理函数中,选中那个需要修改的 DOM 元素,读取其中的文本值,加 1 之后,修改这个 DOM 元素。
这种方式确实比较容易理解,不过在大型项目中,这种模式会造成代码结构复杂难以维护。
而 React 相对会更加“高级”,我们并不需要“选中一些 DOM 然后做一些事情的操作”。我们只需要关心“我想要显示什么”,而不用操心“怎么做”。
React 的理念,可以归结为一个公式
UI = render(data)
- UI 就是用户界面,它是 render 函数执行的结果
- render 是一个纯函数,只接受 data(数据)
用户看到的界面完全取决于输入的数据。当我们想要更新页面的时候,要做的就是更新数据,用户界面就会自动的作出响应,这也是 React 为什么叫 React 的原因。
在每次 render 函数被调用时,并不是简单的把整个组件重新绘制一次,因为那样明显是比较浪费的。React 利用了 Virtual DOM,让每次渲染时只重新渲染最少的 DOM。
- DOM 是结构化文本(HTML文本)的抽象表达形式,每个 HTML 中的元素都对应一个 DOM 节点,基于这种形式,DOM 节点自然就构成了一个树形结构——DOM树。
- 浏览器为了渲染 HTML,会先用 HTML 来构建 DOM 树,然后根据 DOM 树渲染用户看见的界面,当需要修改界面时,就去修改 DOM 树上的节点。
- 为了提升性能,我们需要“减少 DOM 操作”。因为一个 DOM 操作,可能会引起浏览器对网页进行重新布局、绘制
- React 不会直接将 JSX 语句用来构建 DOM 树,而是构建 Virtual DOM。DOM 树是 HTML 的抽象,那么 Virtual DOM 就是 DOM 树的抽象。它不会直接触及浏览器,而只是存在 JavaScript 空间的一个树形结构。
- 每次渲染时,会对比这一次产生的 Virtual DOM 和上一次渲染的 Virtual DOM,如果发现差别,修改真正的 DOM 树时只需要修改差别中的部分就行。
所以在上面的计数中,当用户点击之后,这一次的重新渲染,实际也只会更新 span 元素中的内容而已 0 -> 1。
# 2. 组件
React 的组件使用的是一个名为 render()
方法。
- 可以向组件传递参数,组件内部可以通过
this.props
获取。 - 组件内部也可以维护自己的状态,可以通过
this.state
访问,当组件的数据变化时,组件会再次调用render()
方法重新渲染更新
import React, { Component } from 'react';
class TodoApp extends Component {
constructor(props) {
super(props);
// 定义自己的状态
this.state = { items: [], text: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
render() {
return (
<div>
<h3>TODO</h3>
<TodoList items={this.state.items} />
<form onSubmit={this.handleSubmit}>
<label htmlFor="new-todo">
What needs to be done?
</label>
<input
id="new-todo"
onChange={this.handleChange}
value={this.state.text}
/>
<button>
Add #{this.state.items.length + 1}
</button>
</form>
</div>
);
}
handleChange(e) {
// 更新 state,重新进行渲染
this.setState({ text: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
if (this.state.text.length === 0) {
return;
}
const newItem = {
text: this.state.text,
id: Date.now()
};
this.setState(state => ({
items: state.items.concat(newItem),
text: ''
}));
}
}
class TodoList extends React.Component {
render() {
return (
<ul>
{this.props.items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
}
ReactDOM.render(
<TodoApp />,
document.getElementById('todos-example')
);
这里还有几个小知识🤔️
import
了Component
,Component
作为所有组件的基类,提供了很多组件共有的功能。可以通过extents Component
来创建组件类- 另外我们有必要
import React
,看起来没有被用到?事实上这很有必要,在使用 JSX 的代码文件中,即使我们用不到React
,也需要将它倒入,因为 JSX 最后会被编译成依赖于React
的表达式。例如前面看到的React.createElement
深入了解组件 →