React.js 入门(二)


本章主要写 React 的数据操作。

state 状态

状态就像是组件自己的数据存储,用于处理随时间改变的数据和用户互动产生的数据。

state 对于拥有和设置它的 React 组件是 private 的(任何其他组件都不接近)。组件可以向下传递 state 及其派生的状态给子组件。

当数据变化时,通过调用 this.setState(data, callback) 把数据合并到组件私有属性 this.state 中,驱动组件重新 render 自己。其中 callback 是可选的。

  • constructor 是唯一可以给 this.state 赋值的地方
  • 直接修改 state 的值不会重新渲染组件,必须使用 setState()
  • 出于性能考虑,React 可能会把多个 setState() 调用合并成一个更新。因为 this.props 和 this.state 可能会异步更新,所以不要依赖他们的值来更新下一个状态

解决上面的问题,使用 setState 方法的第二种形式,以 function 作为入参,而不是 object。

// 第一个参数是 previous state,第二个参数是更新时的 props
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

Lifting State Up 状态提升

在 React 中,将多个组件中共享的 state 向上移动到最近的共同父组件中,实现共享 state,称之为“状态提升”。

在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。

依赖于自上而下的数据流,“存在”于组件中的任何 state,仅有组件自己能够修改它,因此 bug 的排查范围被大大缩减了。

什么样的组件应该拥有 State

有时你需要响应用户输入服务器请求或者时间的流逝,这时就要用到 State。

但是尽可能使你的大部分组件 stateless 无状态,减少冗余。

一个常见的模式就是创建一些没有状态的组件,仅用来渲染数据,然后把它们内嵌在一个 stateful 的父组件里。父组件通过 props 把 state 传递给子组件。

// Stateless Function
const Greeting = (props) => (
  <h1>Hello, {props.name}</h1>
);
ReactDOM.render(
  <Greeting name="Sebastian" />,
  document.getElementById('example')
);

什么不应该放在 State 中

  • 根据其他 state 或 props 计算出该数据的值
  • 由父组件通过 props 传递而来的。凡是有例外:当需要指定 previous 的值时,可以把 props 中取得的值存在私有状态中。因为父组件重新渲染时,props 值也会变。
  • 随时间的推移而保持不变

Handling Events 事件处理

尽管事件处理器看似被内联地渲染(直接写在 React 组件上,看起来像 inline 绑定),但它们本质是使用了事件委托的方式处理同一类事件。无论有多少个同类事件出现,最后只在顶层DOM节点上添加一个事件处理函数。

命名规范:

  • 将代表事件监听属性命名为 on[Event]
  • 将处理事件监听方法命名为 handle[Event]
  • 对于内置组件,如 <button> 元素的点击事件使用 onClick 这对于 React 有特殊的含义

事件名以小驼峰式 camelCase 绑定事件处理函数,同 JSX 用法要求

在 React 中不能通过return false 的方式阻止默认行为,必须显式地使用 preventDefault

React 事件处理函数里,入参 e 是一个合成事件。React 根据 W3C 规范来定义这些合成事件,不需要担心跨浏览器的兼容性问题。

函数式组件,事件处理函数自动绑定它所属的组件实例:

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

使用 ES6 class 语法(class组件)时,需手动绑定 this:

class SayHello extends React.Component {
  constructor(props) {
    super(props);
    // 为了在回调中使用 `this`,必须手动绑定 this 到当前实例对象
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    alert('Hello!');
  }

  render() {
    // Because `this.handleClick` is bound, we can use it as an event handler.
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

如果嫌 bind 麻烦,建议使用 class fields 语法,该语法在 Create React App 里默认支持:

class LoggingButton extends React.Component {
  // This syntax ensures `this` is bound within handleClick.
  // Warning: this is *experimental* syntax.
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

你也可以在 callback 里使用箭头函数。

这种语法的问题是每次组件 render 都创建一个新的 callback,大部分情况下ok。但如果该 callback 作为 props 传递给子组件,子组件可能会做一个额外的 re-rendering,带来性能影响。

给事件处理函数传参

两种方式下 React event 作为 id 后的参数传入,只不过箭头函数需要显式的传递。

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

当在循环里绑定函数时,可以考虑使用 data-* 属性存储数据。参考

条件渲染

在 JSX 中使用大括号 {} 内嵌条件渲染表达式:

通过 expression && element 在 JSX 中实现 inline 的 if 渲染组件。因为 true && expression 总是返回 expression,而 false && expression 结果为 false,React 会忽略。

通过 condition ? elementA : elementB 在 JSX 中实现 inline 的 if-else 渲染组件。

阻止组件渲染

某些情况下你希望一个组件隐藏,即使它曾经被其他组件渲染。方式: return null。这不影响组件的生命周期方法执行。

Lists and Keys

在 React 中将数组转为 React elements 组成的列表,与使用 js array 方法产生新数组是一样的。

在构建动态列表的时候,强烈建议指定一个合适的 key.

如果没有指定任何 key,React 会发出警告,并且会把数组的索引当作默认的 key。

如果不会对列表进行重新排序、插入、删除操作,以数组索引作为 key 是安全的,反之,则不然。

组件的 key 值并不需要在全局都保证唯一,只需要在当前的同一级元素(兄弟节点)之间保证唯一即可。

Key 是 React 特殊保留属性,不会传递给子组件(不能通过 props.key 获取)。

render() {
  return (
    <ol>
      {this.props.results.map((result, index) => (
        <li key={index}>{result.text}</li>
      ))}
    </ol>
  );
}

这个 key 属性必须直接在数组方法中提供给组件:

// WRONG! There is no need to specify the key here:
class ListItemWrapper extends React.Component {
  render() {
    return <li key={this.props.data.id}>{this.props.data.text}</li>;
  }
}
class MyComponent extends React.Component {
  render() {
    return (
      <ul>
        {this.props.results.map((result) => ( // The key should have been specified here:
          <ListItemWrapper data={result} />   // <ListItemWrapper key={result.id} data={result} />
        ))}
      </ul>
    );
  }
}

原理:当列表重新渲染,React 将拿每个新列表项的 key 与旧列表项进行比对。如果当前列表中的 key 在旧列表不存在,则创建一个新组件;如果当前列表中缺少旧列表中的某个key,则销毁旧列表中对应的组件;如果两个列表中 key 匹配,则更新对应组件。

Keys 告诉 React 每个组件的身份,以便 React 在重新渲染之间保持状态。如果组件的 key 改变,则组件被销毁,并以全新的 state 重新创建。

Composition

有些组件无法提前知晓子组件的具体内容,可以使用一个特殊的 {props.children} 在组件里预留位置,将任意子组件传递到渲染结果中。

少数情况下,你可能需要在一个组件中预留出多个“洞”。这种情况下,可以自行约定,自定义 props 的属性(类似 vue 的具名插槽)

在 React 中没有“槽”这一概念的限制,组件可以接受任意 props,包括原始值、React 元素以及函数。

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

props和composition在自定义组件时提供了足够的灵活性。如果你想要复用非UI功能,建议将其抽出为单独的 js 模块。无需 extend,通过 import 进来使用。