React核心概念学习

175 阅读21分钟

一、给组件添加交互功能

注意
为了少输入代码,同时为了避免 this 造成的困扰,我们在这里使用箭头函数 来进行事件处理,如下所示:

onClick={() => this.setState({value: 'X'})} 

注意:此处使用了 onClick={() => this.setState({value: 'X'})}  的方式向 onClick 这个 prop 传入一个函数。 React 将在单击时调用此函数。但很多人经常忘记编写 () =>,而写成了 onClick={this.setState({value: 'X'})} ,这种常见的错误会导致每次这个组件渲染的时候都会触发控制台输出。

通过在 React 组件的构造函数中设置 this.state 来初始化 state。this.state 应该被视为一个组件的私有属性。我们在 this.state 中存储当前每个方格(Square)的值,并且在每次方格被点击的时候改变这个值。

首先,我们向这个 class 中添加一个构造函数,用来初始化 state:

class Square extends React.Component {
  constructor(props) {    
      super(props);    
      this.state = {      
          value: null,    
      };  
  }
}

注意
在 JavaScript class 中,每次你定义其子类的构造函数时,都需要调用 super 方法。因此,在所有含有构造函数的的 React 组件中,构造函数必须以 super(props) 开头。

在 Square 组件 render 方法中的 onClick 事件监听函数中调用 this.setState,我们就可以在每次 <button> 被点击的时候通知 React 去重新渲染 Square 组件。组件更新之后,Square 组件的 this.state.value 的值会变为 'X',因此,我们在游戏棋盘上就能看见 X 了。点击任意一个方格,X 就会出现了。

每次在组件中调用 setState 时,React 都会自动更新其子组件。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"        onClick={() => this.setState({value: 'X'})}      >
        {this.state.value}      </button>
    );
  }
}

每次在组件中调用 setState 时,React 都会自动更新其子组件。

查看此步完整代码示例

1.开发者工具

在 Chrome 或者 Firefox 中安装扩展 React Devtools 可以让你在浏览器开发者工具中查看 React 的组件树。Chrome安装位置

React Devtools

你还可以在 React DevTools 中检查 React 组件的 state 和 props。

安装 React DevTools 之后,右键点击页面的任何一个元素,然后选择“查看”,这样就能打开浏览器的开发者工具了,并且工具栏最后会多展示一个 React 的选项卡(包含 “⚛️ Components” 和 “⚛️ Profiler”)。你可以使用 “⚛️ Components” 来检查组件树。

不过,如果你使用的是 CodePen 在线编辑器的话,还需要几步操作才能正确使用开发工具

  1. 登录或注册,然后在邮件中确认(需要关闭垃圾邮件)。
  2. 点击 “Fork” 按钮。
  3. 点击 “Change View”,然后选择 “Debug mode”。
  4. 上一步会打开一个新的标签页,此时开发者工具就会有一个 React 标签了。

2. 为什么不可变性在 React 中非常重要

一般来说,有两种改变数据的方式。第一种方式是直接修改变量的值,第二种方式是使用新的一份数据替换旧数据。

替换旧数据的优点:

1.简化复杂的功能

不可变性使得复杂的特性更容易实现。在后面的章节里,我们会实现一种叫做“时间旅行”的功能。“时间旅行”可以使我们回顾井字棋的历史步骤,并且可以“跳回”之前的步骤。这个功能并不是只有游戏才会用到——撤销和恢复功能在开发中是一个很常见的需求。不直接在数据上修改可以让我们追溯并复用游戏的历史记录。

2. 跟踪数据的改变

如果直接修改数据,那么就很难跟踪到数据的改变。跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。

跟踪不可变数据的变化相对来说就容易多了。如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。

3.确定在 React 中何时重新渲染

不可变性最主要的优势在于它可以帮助我们在 React 中创建 pure components。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。

查阅性能优化章节,以了解更多有关 shouldComponentUpdate() 函数及如何构建 pure components 的内容。

二、 函数组件

**你想写的组件只包含一个 render 方法,并且不包含 state,**使用函数组件会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写。

把 Square 类替换成下面的函数:

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

注意
当我们把 Square 修改成函数组件时,我们同时也把 onClick={() => this.props.onClick()} 改成了更短的 onClick={props.onClick}(注意两侧没有括号)。

三、JSX 简介

1. 在 JSX 中嵌入表达式

在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式

2.JSX 也是一个表达式

在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。

也就是说,你可以在 if 语句和 for 循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX:

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;  }
  return <h1>Hello, Stranger.</h1>;}

3.JSX 中指定属性

你可以通过使用引号,来将属性值指定为字符串字面量:

const element = <a href="https://www.reactjs.org"> link </a>;

也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:

const element = <img src={user.avatarUrl}></img>;

警告:
因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。
例如,JSX 里的 class 变成了 className,而 tabindex 则变为 tabIndex

4. 使用 JSX 指定子元素

假如一个标签里面没有内容,你可以使用 /> 来闭合标签,就像 XML 语法一样:

const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);

5. JSX 防止注入攻击

你可以安全地在 JSX 当中插入用户输入内容:

const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;

React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

6.JSX 表示对象

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。

以下两种示例代码完全等效:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:

// 注意:这是简化过的结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

这些对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。

四、元素渲染

1. 将一个元素渲染为 DOM

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

2.更新已渲染的元素

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

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

3. React 只更新它需要更新的部分

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

五、组件 & Props

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。本指南旨在介绍组件的相关理念。参考详细组件 API

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

1.函数组件与 class 组件

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

2.渲染组件

之前,我们遇到的 React 元素都只是 DOM 标签:

const element = <div />;

不过,React 元素也可以是用户自定义的组件:

const element = <Welcome name="Sara" />;

当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。

注意:  组件名称必须以大写字母开头。
React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div /> 代表 HTML 的 div 标签,而 <Welcome /> 则代表一个组件,并且需在作用域内使用 Welcome
你可以在深入 JSX 中了解更多关于此规范的原因。

3. 组合组件

组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。

4. 提取组件

在 CodePen 上试试

将组件拆分为更小的组件。
根据经验来看,如果 UI 中有一部分被多次使用(ButtonPanelAvatar),或者组件本身就足够复杂(AppFeedStoryComment),那么它就是一个可提取出独立组件的候选项。

5. Props 的只读性

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

React 非常灵活,但它也有一个严格的规则:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。在下一章节中,我们将介绍一种新的概念,称之为 “state”。在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。

六、State & 生命周期

该节介绍了 React 组件中 state 和生命周期的概念。你可以查阅详细的组件 API 参考文档

理想情况下,我们希望只编写一次代码,便可以让 Clock 组件自我更新
我们需要在 Clock 组件中添加 “state” 来实现这个功能。
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。

1. 将函数组件转换成 class 组件

通过以下五步将 Clock 的函数组件转成 class 组件:

  1. 创建一个同名的 ES6 class,并且继承于 React.Component
  2. 添加一个空的 render() 方法。
  3. 将函数体移动到 render() 方法之中。
  4. 在 render() 方法中使用 this.props 替换 props
  5. 删除剩余的空函数声明。

在 CodePen 上尝试

2. 向 class 组件中添加局部的 state

我们通过以下三步将 date 从 props 移动到 state 中:

  1. 把 render() 方法中的 this.props.date 替换成 this.state.date 

  2. 添加一个 class 构造函数,然后在该函数中为 this.state 赋初值 通过super(props);方式将 props 传递到父类的构造函数中
    Class 组件应该始终使用 props 参数来调用父类的构造函数。

  3. 移除 <Clock /> 元素中的 date 属性:

在 CodePen 上尝试

3. 将生命周期方法添加到 Class 中

在具有许多组件的应用程序中,当组件被销毁时释放所占用的资源是非常重要的。

当 Clock 组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器。这在 React 中被称为“挂载(mount)”。

同时,当 DOM 中 Clock 组件被删除的时候,应该清除计时器。这在 React 中被称为“卸载(unmount)”。

我们可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法,这些方法叫做“生命周期方法”
componentDidMount() 方法会在组件已经被渲染到 DOM 中后运行,所以,最好在这里设置计时器:

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

我们会在 componentWillUnmount() 生命周期方法中清除计时器:

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

使用 this.setState() 来时刻更新组件 state:

在 CodePen 上尝试

概括一下发生了什么和这些方法的调用顺序:

  1. 当 <Clock /> 被传给 root.render()的时候,React 会调用 Clock 组件的构造函数。因为 Clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state。我们会在之后更新 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() 生命周期方法,这样计时器就停止了。

4. 正确地使用 State

不要直接修改 State

例如,此代码不会重新渲染组件:

// Wrong
this.state.comment = 'Hello';

而是应该使用 setState():

// Correct
this.setState({comment: 'Hello'});

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

State 的更新可能是异步的

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

State 的更新会被合并

当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。

数据是向下流动的

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

七、事件处理

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。 在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault

在 CodePen 上尝试

你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined

这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 (),例如 onClick={this.handleClick},你应该为这个方法绑定 this

如果觉得使用 bind 很麻烦,这里有两种方式可以解决。\

你可以使用 public class fields 语法 to correctly bind callbacks:

Create React App 默认启用此语法。
如果你没有使用 class fields 语法,你可以在回调中使用箭头函数

onClick={() => this.handleClick()}

1. 向事件处理程序传递参数

在循环中,通常我们会为事件处理函数传递额外的参数。两种方式:

<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 的方式,事件对象以及更多的参数将会被隐式的进行传递。

八、条件渲染

在 React 中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。

React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if 或者条件运算符去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。

1. 元素变量

你可以使用变量来储存元素。 它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。

2. 与运算符 &&

通过花括号包裹代码,你可以在 JSX 中嵌入表达式。这也包括 JavaScript 中的逻辑与 (&&) 运算符。

3. 三目运算符

另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 condition ? true : false

4. 阻止组件渲染

在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。

if (!props.warn) {    
    return null;  
}

在组件的 render 方法中返回 null 并不会影响组件的生命周期

九、列表 & Key

1. 渲染多个组件

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

在 CodePen 上尝试

2. 基础列表组件

给每个列表元素分配一个 key 属性来解决警告a key should be provided for list items

在 CodePen 上尝试

3. key

key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key
当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key

用 key 提取组件

元素的 key 只有放在就近的数组上下文中才有意义。

在 CodePen 上尝试

一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性。

key 值在兄弟节点之间必须唯一

在 CodePen 上尝试

4. 在 JSX 中嵌入 map()

在 CodePen 上尝试

这么做有时可以使你的代码更清晰,但有时这种风格也会被滥用。如果一个 map() 嵌套了太多层级,那可能就是你提取组件的一个好时机。

十、表单

1. 受控组件

在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

在 CodePen 上尝试

2. textarea 标签

在 React 中,<textarea> 使用 value 属性代替。这样,可以使得使用 <textarea> 的表单和使用单行 input 的表单非常类似:

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {      value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'    };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {    this.setState({value: event.target.value});  }
  handleSubmit(event) {
    alert('提交的文章: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          文章:
          <textarea value={this.state.value} onChange={this.handleChange} />        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}

3. select 标签

在 CodePen 上尝试
总的来说,这使得 <input type="text"><textarea> 和 <select> 之类的标签都非常相似—它们都接受一个 value 属性,你可以使用它来实现受控组件。

注意
你可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项:

<select multiple={true} value={['B', 'C']}>

4. 文件 input 标签

处理多个输入

当需要处理多个 input 元素时,我们可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。

在 CodePen 上尝试

这里使用了 ES6 计算属性名称的语法更新给定输入名称对应的 state 值:
例如:

this.setState({
  [name]: value});

等同 ES5:

var partialState = {};
partialState[name] = value;this.setState(partialState);

另外,由于 setState() 自动将部分 state 合并到当前 state, 只需调用它更改部分 state 即可。

受控输入空值

受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value,但输入仍可编辑,则可能是你意外地将 value 设置为 undefined 或 null

下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)

ReactDOM.createRoot(mountNode).render(<input value="hi" />);

setTimeout(function() {
  ReactDOM.createRoot(mountNode).render(<input value={null} />);
}, 1000);

受控组件的替代品 => 非受控组件

成熟的解决方案

如果你想寻找包含验证、追踪访问字段以及处理表单提交的完整解决方案,使用 Formik 是不错的选择。然而,它也是建立在受控组件和管理 state 的基础之上 —— 所以不要忽视学习它们。

十一、状态提升

通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。

在 CodePen 上尝试

1. 学习小结

在 CodePen 上尝试

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

十二、组合 vs 继承

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

1.包含关系

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

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

在 CodePen 上尝试

少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop。

在 CodePen 上尝试

<Contacts /> 和 <Chat /> 之类的 React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。

2. 特例关系

有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 WelcomeDialog 可以说是 Dialog 的特殊实例。

在 React 中,我们也可以通过组合来实现这一点。“特殊”组件可以通过 props 定制并渲染“一般”

在 CodePen 上尝试

3. 那么继承呢?

在 Facebook,我们在成百上千个组件中使用 React。我们并没有发现需要使用继承来构建组件层次的情况。

Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

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

十三、React 哲学

React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。

React 最棒的部分之一是引导我们思考如何构建一个应用。

1. 从设计稿开始

第一步:将设计好的 UI 划分为组件层级

第二步:用 React 创建一个静态版本

第三步:确定 UI state 的最小(且完整)表示

第四步:确定 state 放置的位置

第五步:添加反向数据流

十四、参考文献:

react入门-核心概念