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 也可以设计「抽象父组件」。实现这些组件的「子组件」通过传入特定的参数实现。