React 学习笔记

阅读 React文档 后的笔记

# 我的感想

React 几乎利用了原生 JavaScript 的所有特质。在对 JavaScript 比较熟练的情况下,React 不会给你带来更多的惊喜。不过,相较于 Vue,写 React 也给人一种踏实的感觉。

# JSX

const element = <h1>Hello, world!</h1>;

JSX 是 一套 Javascript 拓展。它将 html元素嵌入到 JavaScript 代码里。

# 元素

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));

React DOM 元素框架维护的一个静态不可变最小单位。因此 React 声称它的开销非常小。
由于是不可变单位,每次页面的更新实际上是新元素对旧元素的覆盖。

# 组件

React 将元素和底层组件组合,抽象成更高维度的可复用组件。

组件的定义有两种方式:函数组件和类组件(ES6 Later)

//函数
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
//类
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

用户定义的组件要求首字母大写。定义完成后,可以直接在 JSX 里调用。

function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);

React 在文档里举了一例,用于展示将组件提取多个小组件给程序编写带来的好处。这其中,React 建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。

此外,组件也必须保证自己不对传入的 props 做任何修改。

# 状态

函数组件是一种比较简单的组件,不支持其它特性。使用类组件时,可以通过维护类的「成员变量」,达到组件存储状态的目的。

下面的例子在组件装载的时候维护了自己的state。state 是一个 JS 对象。

class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

然而,对于this.state的直接修改是不会被 React 捕获从而更新到页面上的。也许是众多对React负面评价的原因?

需要调用 this.setState(object) 函数来更新状态。这个函数有若干特点:

  1. 更新是异步的,因此不可以依赖 state 的值计算下一个状态。
  2. this.setState() 可以接受函数作为参数。这个函数的第一个参数是上一次状态,第二个参数是组件的参数。使用函数作为参数可以解决异步更新出错的问题。
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
  1. State 的更新是浅更新。this.setState(small_obj) 不会覆盖掉small_obj以外的其他属性。

# 向下流动

各类绘制 GUI 的语言都不断强调一个概念 —— 一致性。 前端经常在不同组件中对同一数据来源做显示和修改。例如用户在设置页面中更改用户名之后,所有显示用户名的地方需要同步更新。如果让每个组件自己维护更新操作,就进入了 bug 多发地。

例如,iOS 前端框架 SwiftUI 处处强调「Single Source of Truth」的概念。 React 虽然没有严谨的状态筛查的概念,但是可以将 state 的内容作为下一个 props 传入子组件。React 形象地称这种写法为「向下流动」。

然而,React 禁止任何对于传入属性的修改。如果我们想在子组件里修改单一数据源,我们需要调用唯一修改函数,也就是调用持有 state 的祖先实现的修改函数(《React 哲学》中称做「反向数据流」)。这点非常麻烦。 React 声称这是为了方便追踪 BUG,因为数据的修改只有一个入口。

# 生命周期

在组件被放到 DOM 里去之后,会调用 componentDidMount() 函数。 在即将被销毁的时候,会调用 componentWillUnmount() 函数,释放所占用的资源。

class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

componentWillUnmount() {
clearInterval(this.timerID);
}

tick() {
this.setState({
date: new Date()
});
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

# 事件处理

React 元素事件和 DOM 元素相似。在语法上,React 使用小驼峰(onClick),并且传入一个 JSX 包装的函数,而不是 DOM 中的字符串。

<!-- 传统的 DOM 写法 -->
<button onclick="handler()">
A button
</button>
<!--  React 元素写法-->
<button onClick={handler}>
A button
</button>

可以给事件处理程序传递参数。

<button onClick={(e) => this.handler(id, e)}>
A button
</button>

其中e是 React 的事件对象。

💡️TIPS 由于 JavaScript 设计的原因,JSX 的回调函数默认不会携带 this 引用(因为 class 的方法不会绑定 this)。 所以在定义组件的时候,需要将 this 事先绑上去。

参考阅读:
JavaScript 中至关重要的 Apply, Call 和 Bind

class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};

// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}

render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}

有一个实验性语法可以解决这个问题。将 handleClick() 函数写成这样:

handleCLick() = () => {
console.log(this);
}

这样做无需在外部显式绑定 this,就可以在函数中调用 this。
或者也可以利用 JSX 这样写:

<button onClick={() => this.handleClick()}>
A button
</button>

# 条件渲染

React 通过函数组件的返回值,或者类组件的render()函数来渲染元素。因而,只要在组件中使用条件判断,返回不同的 JSX,就能实现条件渲染。

function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}

ReactDOM.render(
// Try changing to isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);

值得注意的是,每次调用 setState() 函数,React 都会自动重新执行 rander()
所以,可以创造出这种有状态的组件:

class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {isLoggedIn: false};
}

handleLoginClick() {
this.setState({isLoggedIn: true});
}

handleLogoutClick() {
this.setState({isLoggedIn: false});
}

render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}

return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}

有状态组件可以在render()函数里可以先根据状态计算出需要显示的元素。

还有一个更帅的写法:

<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have
{unreadMessages.length} unread messages.
</h2>
}
</div>

在 JavaScript 中,true && express 会返回 express。相反,false && express会返回 false。如果返回 false, React 会忽略这组元素的渲染。

当然,三目运算符也可以用于条件渲染:

 <div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>

# 列表

类似于 Vue 中的 v-for,使用 React 也可以做到「偷懒」的重复组件渲染。

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);

上段代码中,listItems 是一个 JSX 的数组。React 很灵活地将它插入页面中。

最好给列表元素绑定 key。这样,React 可以识别哪些元素被改变了。绑定 key 的地方应该在数组的上下文中。例如,通常在 map() 方法中设置 key 属性。

function ListItem(props) {
return <li>{props.value}</li>;
}

function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);

# 表单

前面提到,React 推荐一种「向下流动」的写法。也就是说,可以保证 React 中处理的数据来自同一来源。在 html 中,表单元素通常由浏览器自己维护自己的状态。例如:

<form>
<label>
<input type="text" name="name" />
</label>
<input type="submit" value="提交" />
</form>

其中输入框的值是由浏览器自己维护的。事实上,我们可以让 React 接管状态,从而实现唯一数据源的目标。被 React 接管的表单元素称为「受控组件」。

<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>

input, textarea, select 等标签都可以被 React 接管。

# 组合

React 推荐使用 JS 的语法进行组件间的代码重用。

# 包含关系

组件套用的推荐写法:

function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}

function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}

通过 prop.children 将子组件传递到渲染结果里去。如果我们在 JSX 里指定标签参数的名字,可以不用 children

# 特例关系

React 也可以设计「抽象父组件」。实现这些组件的「子组件」通过传入特定的参数实现。