React

元素渲染

元素是构成React应用的最小砖块。

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

与浏览器的DOM不同,React元素是创建开销极小的普通对象。React DOM会负责更新DOM来与React元素保持一致。

元素和组件不是一个概念,组件是由元素构成的。

将一个元素渲染为DOM

<div id="root"></div>

id为root的div称为DOM节点,该节点内的所有内容都将由React DOM管理。

仅使用React构建的应用通常只有单一的根DOM节点。如果将React集成进一个已有应用,那么可以在应用中包含任意多的独立根DOM节点。

想要将一个React元素渲染到根DOM节点中,只需要把它们一起传入ReactDOM.render():

const element = <h1>Hello, world</h1>;
ReactDom.render(element, document.getElementById('root'));
# 渲染的组件element
# 组件挂载的位置 document.getElementById('root')

更新已渲染的元素

React元素是不可改变的。一旦被创建,就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的UI。

根据我们已有的知识,更新UI唯一的方式就是创建一个全新的元素,并将其传入ReactDOM.render()

# 计时器例子
function tick() {
  const element = (
    <div>
    	<h1>Hello, world!</h1>
      <h2>It is {new Date().toLocalTimeString()}.</h2>
    </div>
  )
  ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000)
# 每秒都调用tick方法

在实践中,大多数React应用只会调用一次ReactDOM.render()。可以将代码封装到有状态组件中。

实际上,React DOM会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使DOM达到预期的状态。

组件&Props

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

组件在概念上类似于JavaScript函数。它接受任意的入参(即props),并返回用于描述页面展示内容的React元素。

函数组件与class组件

定义组件最简单的方式就是编写JavaScript函数:

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

该函数是一个有效的React组件,因为它接收唯一带有数据的props对象并返回一个React元素。这些组件被称为函数组件,因为它本质上就是JavaScript函数。

你同时还可以使用 ES6 的 class 来定义组件:

class Welcome extend React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
# 与上面等价

渲染组件

元素可以是DOM标签,也可以是用户自定义的组件:

const element = <div/>;
const element = <Welcome name="Sara" />;
function Welcom(props) {
  return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara"/>;
ReactDOM.render(
  element,
  document.getElementById('root');
);
  1. 调用ReactDOM.render()函数,并传入<Welcome name="Sara" />作为参数。
  2. React调用Welcome组件,并将{name: 'Sara'}作为props传入。
  3. Welcome组件将<h1>Hello, Sara</h1>元素作为返回值。
  4. React DOM将DOM高效的更新为<h1>Hello, Sara</h1>

注意:组件名称必须大写字母开头。

React会将以小写字母开头的组件视为原生DOM标签。例如,<div /> 代表 HTML 的 div 标签,而 <Welcome /> 则代表一个组件,并且需在作用域内使用 Welcome

组合组件

<div id="root"></div>

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

function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

通常来说,每个新的React应用程序的顶层组件都是App组件。但是,如果将React集成到现有的应用中,可能需要像Button这样的小组件,并自下而上的将这些组件逐步应用到视图层的每一处。

提取组件

将组件拆分为更小的组件。

Props的只读性

组件无论是使用函数声明还是通过clss声明,都决不能更改自身的props。

function sum(a, b) {
  return a + b
}

这种函数被称为纯函数,因为该函数不会尝试更改入参,并且多次调用下相同的入参始终返回相同的结果。

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

State & 生命周期

State和props类似,但是state是私有的,并且完全受控于当前组件。

将函数组件转换成class组件
向class组件中添加局部的state
将生命周期方法添加到Class中
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.toLocalTimeString()}.</h2>
      </div>
    )
  }
}
ReactDOM.render(
  <Clock />,
  document.getElementById('root');
);
  1. <Clock /> 被传给 ReactDOM.render()的时候,React 会调用 Clock 组件的构造函数。因为 Clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state
  2. 之后 React 会调用组件的 render() 方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配 Clock 渲染的输出。
  3. Clock 的输出被插入到 DOM 中后,React 就会调用 ComponentDidMount() 生命周期方法。在这个方法中,Clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法。
  4. 浏览器每秒都会调用一次 tick() 方法。 在这方法之中,Clock 组件会通过调用 setState() 来计划进行一次 UI 更新。得益于 setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。
  5. 一旦 Clock 组件从 DOM 中被移除,React 就会调用 componentWillUnmount() 生命周期方法,这样计时器就停止了。
正确的使用State
不要直接修改State
this.state.comment = 'Hello';		# 这段代码不会重新渲染组件
this.setState({comment: 'Hello'}); 	# 修改State应该使用`setState()`

构造函数是唯一可以给this.state赋值的地方。

State的更新可能是异步的
State的更新会被合并
数据是向下流动的

不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是class 组件。这就是为什么称state为局部的或是封装的原因。除了拥有并设置了它的组件,其他组件都无法访问。

组件可以选择把它的state作为props向下传递到它的子组件中:

<FormattedDate date={this.state.date} />
function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimesString()}</h2>;
}

这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。

如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。

事件处理

绑定事件三种方式:

# 1.  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>)
  }
}
# 2.  Create React App 默认使用此语法此方式为class field语法
class Toggle extends React.Component {
  // 此语法确保`handleClick`内的this已被绑定。
  handleClick = () => {
    console.log('this is :', this)
  }
  render() {
		return (<button onclick={this.handleClick}>{this.state.isToggleOn ? 'ON' : 'OFF'}</button>)
  }
}
# 3. 使用箭头函数如果该回调函数作为prop传入子组件时这些组件可能会进行额外的重新渲染通常建议在构造器中绑定或使用class fields 语法来避免这类性能问题
class Toggle extend React.Component {
  handleClick() {
    console.log('this is :', this);
  }
  render() {
		return (<button onclick={() => this.handleClick()}>{this.state.isToggleOn ? 'ON' : 'OFF'}</button>)
  }
}
向事件处理程序传递参数
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

上述两种方式等价,分别使用箭头函数Function.prototype.bind来实现。

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

条件渲染

元素变量
与运算符 &&
三目运算符
阻止组件渲染

列表 & Key

渲染多个组件

可以通过{}在JSX内构建一个元素集合

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((item) => 
	<li>{item}</li>
);
ReactDOM.render(<ul>{listItems}</ul>, document.getElementById('root'));
基础列表组件
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((item) => 
		<li key={item.toString()}>{item}</li>
  );
	return (<ul>{listItems}</ul>);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
	<Number numbers={numbers}/>,
  document.getElementById('root');
);
key

key帮助React识别哪些元素改变了,比如被添加或删除。因此需要给数组中的每一个元素赋予一个确定的标识。如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。如果不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。

表单

受控组件
文件input标签
处理多个输入
class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isGoing: true, numberOfGuests: 2};
    this.handleInputChange = this.handleInputChange.bind(this);
  }
  handleInputChange(event) {
    const target = event.target;
    const value = target.name === 'isGoing' ? target.checked : target.value;
    const name = target.name;
    this.setState({
      [name]: value
    });
    # 这里使用了 ES6 计算属性名称的语法更新给定输入名称对应的 state 
  }
  render() {
    return (<form>
        <label>
        参与:
          <input 
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange}/>
        </label>
        <br/>
        <label>
        来宾人数:
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange}/>
        </label>
      </form>)
  };
}
受控输入空值
受控组件的替代品

状态提升

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

如果某些数据可以由 props 或 state 推导得出,那么它就不应该存在于 state 中。

组合 vs 继承

React有十分强大的组合模式。推荐使用组合而非继承来实现组件重用。

包含关系

有些组件无法提前知道它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。

建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中:

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
    	{props.children}
    </div>
  );
}
# 使别的组件可以通过jsx嵌套将任意组件作为子组件传递给它们
function WelcomDialog() {
  return (
  	<FancyBorder color="blue">
      <h1 className="Dialog-title">Welcom</h1>
      <p className="Dialog-message">thanks...</p>
    </FancyBorder>
  );
}

预留模式

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 />
      } />
  );
}

这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。

注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。