React 一瞥

最近使用 React 重构了一个内部管理工具的前端界面。在等待 PM 试用提需求的间隙,接到了一个给后端小伙伴科普 React 的任务,便有了本文。本文将从 DOM 以及早期的 Web 开发入手,期望让没有接触过前端开发或者没有 React 经验的后端同学,对 React 有一个入门的认识与兴趣。而对于前端经验丰富的小伙伴,也期望本文能是你们拉后端入坑的文章之一。

0. 什么是 DOM ?

DOM 全称 Document Object Model,即文档对象模型,是前端开发中最重要的概念之一。网页由一个个 HTML 标签组成,而 DOM 则提供了访问和操作 HTML 的标准方法。利用 DOM 提供的 API,我们就能使用 JavaScript 对页面进行各种操作。

1. 石器时代

可以思考一下,如果我们把百度的搜索按钮从蓝色换成红色,需要怎么做呢?(不用考虑 API,只考虑思路)

image-20200621120404685

只需两步即可:

  1. 找到搜索按钮的 HTML 元素
  2. 利用 DOM 提供的 API 把蓝色改成红色

是的,这就是最基础的编辑 HTML 的操作。早期的 Web 开发(jQuery 时代)就是这样的一个面向过程的开发,我们通过直接操作一个个 HTML 元素来实现各种效果。然而随着 Ajax 的出现、Gmail 带来 Web 富应用的可能性以及 V8 的出现,Web 的功能也变得越来越复杂和强大。面向过程冗长且难以维护的缺点开始越发明显。

2. 为什么不偷懒呢 ?

开发过程变得复杂之后,便激发了人们想要偷懒的潜力。把上面的第二步再做一次拆分,整个步骤就分为:

  1. 找到需要修改的目标 HTML 元素
  2. 计算出更新后的数据
  3. 把新的数据更新到目标 HTML 元素

在 Web 开发中,UI 的变化与数据的变化是强相关的,DOM 的操作又是相对固定的(主要给 DOM 做一些增删改查的操作)。

既然数据可以决定 UI,如果有一种类库或框架能让我们只关心数据变化而不用管理 DOM 操作,那我们的开发过程岂不是能轻松很多?

3. React 登场

React 由 Facebook 开发,于 2013 年的 JSConf 上开源。React 的底层帮我们实现了更新 HTML 的操作,让我们把注意力都集中在数据的操作和管理上。从很大程度上减轻了前端开发的工作量,所以一经面世便受到了巨大的反响。目前国内外大厂,都会使用 React 作为自己的前端技术栈之一。

React 的概念比较多,因此上手之前需要了解一些重要的概念。

数据驱动,数据驱动,数据驱动

重要的事情说三遍。比起 React 的语法概念,数据驱动的思想更为重要。前端开发由于有 UI 设计的存在,思维容易过程化。从 jQuery 转过来的小伙伴(包括我自己)刚开始会觉得 React 有点 ”反直觉“,对于页面的操控不直观。这一点在做拖拽等功能时特别明显。

但是 React 中的 UI 都是由数据来决定,数据怎么变化 UI 就怎么变化。在操作 DOM 之前,先思考一下数据是怎么变化的。

JSX、组件、生命周期、State/Props

JSX

JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。

JSX 是一个语法糖,可与允许我们在 JavaScript 中使用 HTML 的语法形式来编写与页面相关的内容。比如 HTML 标签、属性,CSS 相关属性以及自定义的 React 组件。对于习惯了 HTML 的前端开发者来说,这无疑降低了学习成本。这也为 all-in-JS 打下了基础。

1
2
3
4
5
6
// in HTML
<div id="a">1234</div> // Only accept html

// in JSX
<div id="a">1234</div>
<CustomComponent id="custom" /> // Also accept custom component

组件

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。

组件是组成页面单元,其中功能最单一、划分维度最小的称为原子组件(比如 input, button)。通过不同组件的组合与嵌套,可以实现不同功能页面部分,如同化学中分子可以组成不同的物质。下面是掘金首页的组件划分(没有到原子层级):

image-20200621170620546

通过组件可以让代码得到复用,减少开发工作量。最大好处是便于后期维护的,组件可以单独替换来满足不同需求,同时也不用担心影响其他功能。尽管组件在形式上与我们常用的 util 方法类似,但如何划分页面,规划好组件是需要在动手之前就要好好思考的。

生命周期

只看这张图恐怕很难记住 React 的生命周期,其实回想一下 React 帮我们做了什么就会变得容易一些。React 帮我们实现了 DOM 的更新,让我们只需要关注数据即可。那么 DOM 要什么时候才会更新呢?

  1. 页面初次渲染 (componentWillMount –> componentDidMount –> render –> componentWillUnmount)
  2. 页面内有数据发生变动 (componentWillUpdate –> render –> componentDidUpdate)

React 的生命周期就是在这两个的时间点上的拓展,特别是对于数据发生变动时的拓展(Update部分)。也意味着我们在组件的某一个时间点能做哪些,一般情况下,主要关注 componentDidMountcomponentWillUnmount 这两个生命周期即可。

State/Props

React 的特点就是数据发生变化时会更新 UI。而这些可以出发 UI 更新的数据在 React 中分为 stateprops

两者之前的区别就是 props 是传入组件的数据,state 是组件内部数据。类比函数 props 是传入的参数,而 state 是函数内部自己的变量。组件的刷新可以类比为改变入参和内部参数后函数的重新调用。

对于 props 来说,只要值发生变化,那么组件自然就会更新。而对于 state 来说,React 提供了 setState 方法让我们更新组件内部的状态。

对于拥有 state 的组件,一般称为状态组件通常会使用 class 的形式。而没有 state 的组件一般称作无状态组件,可以直接写成 function 的形式。这两种都是 React 组件的写法。

4. 一如即往的 Todo List

光说不练,是没有意义的。所以我们会用一个经典的 Todo List 来作为例子。作为入门示例,这里不会引入过多概念,只会用到上面所提到的概念。

代码地址:CodeSandbox

image-20200621195718366

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import React from "react";
import "./styles.css";

export default class App extends React.Component {
constructor(props) {
super(props);
// 初始化组件用到的 state
this.state = {
todoList: [], // Todo List 列表
todoName: "" // 储存输入框的内容
};

// 使用 function 时需要注意 this 的绑定,这里涉及 JS 中 this 的指向
// 在本例子中不作展开
this.setTodo = this.setTodo.bind(this);
this.addTodoList = this.addTodoList.bind(this);
this.setTodoStatus = this.setTodoStatus.bind(this);
this.deleteTodo = this.deleteTodo.bind(this);
}

setTodo(e) {
// 更新输入框的内容
// 只有更新了 setState 才会同步反应到 HTML
this.setState({
todoName: e.target.value
});
}

addTodoList() {
const { todoList, todoName } = this.state;
todoName &&
this.setState({
// 更新数组时注意 JavaScript 引用类型的操作
todoList: [...todoList, { name: todoName, isDone: false }],
todoName: ""
});
}

setTodoStatus(index) {
let { todoList } = this.state;
const target = todoList[index];
target.isDone = !target.isDone;

if (target.isDone) {
todoList.splice(index, 1);
todoList = [...todoList, target];
}

this.setState({
todoList
});
}

deleteTodo(index) {
const { todoList } = this.state;
todoList.splice(index, 1);
this.setState({
todoList
});
}

render() {
// render 中对应着 HTML 部分,这里的语法便是 JSX
const { todoList, todoName } = this.state;

return (
<div className="App">
<div className="search-bar">
<input
value={todoName}
type="text"
placeholder="Input your todo list"
onChange={this.setTodo}
/>
<button onClick={this.addTodoList}>Add</button>
</div>
<ol>
{todoList.map((todo, index) => {
const { isDone = false, name = "" } = todo;
return (
<li className="todo-item" key={index}>
<div className="item-content">
<span className="label">{index + 1}</span>
<input
type="checkbox"
checked={isDone}
onChange={() => this.setTodoStatus(index)}
/>
<span>{name}</span>
</div>
<button onClick={() => this.deleteTodo(index)}>Delete</button>
</li>
);
})}
</ol>
</div
);
}
}

短短 100 行代码,就能实现一个简单的 Todo List。虽然功能实现了,但上面的代码有许多值得改进的地方。

最大的改进点就是组件化

从效果图来看,Todo List 是一个很明显的上下结构。并且上面的输入框与下面的列表并不耦合,因此可以拆分出两个独立的组件,并让组件自己管理自己的 state。从而实现 “组件状态的自治”。

修改之后的 App.js 如下,把上半部分拆分出去让输入框自己管理状态。而下半部分的列表则纯粹成了一个渲染组件,没有任何的 state 即无状态组件。就可以使用函数的形式再次精简。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React from "react";
import "./styles.css";

import SearchBar from "./searchBar";
import List from "./List";

export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todoList: [] // Todo List 列表
};

this.addTodoList = this.addTodoList.bind(this);
this.setTodoStatus = this.setTodoStatus.bind(this);
this.deleteTodo = this.deleteTodo.bind(this);
}

addTodoList(value, e) {
const { todoList } = this.state;
value &&
this.setState({
todoList: [...todoList, { name: value, isDone: false }]
});
}

setTodoStatus(index) {
let { todoList } = this.state;
const target = todoList[index];
target.isDone = !target.isDone;

if (target.isDone) {
todoList.splice(index, 1);
todoList = [...todoList, target];
}

this.setState({
todoList
});
}

deleteTodo(index) {
const { todoList } = this.state;
todoList.splice(index, 1);
this.setState({
todoList
});
}

render() {
const { todoList } = this.state;

return (
<div className="App">
<SearchBar addTodoList={this.addTodoList} />
<List
todoList={todoList}
setTodoStatus={this.setTodoStatus}
deleteTodo={this.deleteTodo}
/>
</div>
);
}
}

5. 参考资料

  1. React 官网

React 一瞥
https://konta9.github.io/2020/06/21/2020/React 一瞥/
作者
Konata
发布于
2020年6月21日
许可协议