React 学习笔记
React 几乎利用了原生 JavaScript 的所有特质。在对 JavaScript 比较熟练的情况下,React 不会给你带来更多的惊喜。不过,相较于 Vue,写 React 也给人一种踏实的感觉。
阅读 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)
函数来更新状态。这个函数有若干特点:
- 更新是异步的,因此不可以依赖 state 的值计算下一个状态。
this.setState()
可以接受函数作为参数。这个函数的第一个参数是上一次状态,第二个参数是组件的参数。使用函数作为参数可以解决异步更新出错的问题。
this.setState((state, props) => ({
counter: state.counter + props.increment,
}));
- 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 也可以设计「抽象父组件」。实现这些组件的「子组件」通过传入特定的参数实现。
© LICENSED UNDER CC BY-NC-SA 4.0