【译】一份通俗易懂的React.js基础指南-2018

612 阅读19分钟

原文链接:tylermcginnis.com/reactjs-tut… by Tyler McGinnis

React.js学习指南

这篇文章最初发表于2015年1月,但最近被更新为React 16.3以及它所包含的所有优点。

React.js基础:

组件是React的构建快。如果你拥有Angular背景,组件非常类似于Direactives.如果你来自于不同的背景,它们本质上是小工具或者模块。你可以认为组件就是由HTML,CSS,JS和组件内部一些特定的数据组成的集合。它们拥有着你需要的一切东西,被包裹在一个美妙的组合包中。这些组件要么用纯JavaScript定义,要么可以在React团队所称的“JSX”中定义。如果你决定使用JSX(你最愿意的方式,很标准的——也是我们将使用到的工具),你需要一些编译工具将JSX转换成JavaScript,我们之后再说这个。

React之所以能够如此方便地构建用户界面,是因为数据既可以来自组件的父组件,也能够包含在组件本身中。在我们进入到代码之前,我们要确保对组件高度理解。

上图是我Twitter账户的图片。如果我们使用React重构这个页面,我们将把不同的部分拆分成不同的组件(敲黑板)。请注意,组件可以在其内部嵌套组件。我们可以命名左边的组件(粉红框部分)为UserInfo组件。在UserInfo组件中有另一个组件(橙色块),可以将其定义为UserImages组件。这中父/子关系的工作方式是:UserInfo组件(父组件)存放着其本身和UserImages组件(子组件)的“状态”数据。如果我们想要在自组件中使用任何父级组件的数据,我们会将数据作为属性传递给自组件。在这个例子中,我们传递所有用户的图片给UserImages组件(这些图片都存放在UserInfo组件中)。我们将详细的在代码中讨论,但是我希望你能够明白上面👆的图片发生了些什么。这种父/子结构使得我们管理数据的方式更加方便,因为我们不需要精确地知道我们的数据到底存放在哪的并且我们不应该在其它地方操纵这些数据。

下面要讨论的话题都是关于React基础方面的东西。如果你了解它们以及它们的原理,阅读完本教程后,你将更上一层楼。

JSX - 允许我们使用HTML语法类似的写法,能够被转换成轻量级的JavaScript对象。

Virtual DOM - 实际DOM的JavaScript表示。

React.Component - 创建一个新组件的方式。

render(方法)- 为特定的组件描述UI的样子。

ReactDOM.render = 将React组件渲染成DOM节点。

state - 组件内部的数据存储(对象)。

construtor (this.state) - 在组件中创建内部状态(state)的方式。

setState - 组件内部更新状态(state)的工具方法并会重新渲染UI。

props - 父组件中传递给子组件的数据。

propTypes - 允许你控制传递给子组件的属性(props)的存在或者类型。

defaultProps - 允许你为自己的组件设置默认的属性(props)。

Component LifeCycle(生命周期)
    - componentDidMount - 组件被挂载时执行
    - componentWillUnmount - 组件注销前执行
    - getDerivedStateFromProps - 当组件被挂载并且props发生变化时执行。用于更新组件中的状态(state)当props改变时。

Events
    - onClick
    - onSubmit
    - onChange
    ...

我知道这看起来很多,但是你很快就会看到,在使用React构建健壮应用时,这每一个部分都是非常重要的(当我说我希望这是一个全面的指南时,我也不是在开玩笑)。

在这一点上,你应该高度理解React是如何工作的。现在,让我们看一些代码。


创建你的第一个组件(JSX,Virtual DOM,render,ReactDOM.render)

让我们继续创建我们的第一个组件吧!

为了创建React组件,你将使用到ES6中的类。

import React from 'react'
import ReactDOM from 'react-dom'

class HelloWorld extends React.Component {
    render() {
        return (
            <div>Hello World!</div>
        )
    }
}

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

注意到我们的类中只有唯一的一个方法就是render。每个组件都需要有一个render方法。使用render方法的理由是它能描述组件的UI(user interface)。因此在这个例子中Hello world会被渲染出来展示在屏幕上。现在我们看看ReactDOM是做什么的。ReactDOM.render方法有两个参数。第一个参数是你想要渲染的组件,第二个参数是你渲染组件的地方(哪个DOM 节点中)。(注意我们使用的是ReactDOM.render而不是React.render。这个改变出现在React.14中为了使React更加模块化。当你认为React可以呈现比DOM元素更多的内容时,这是有意义的)。在上面的例子中,我们告诉React获取HelloWorld组件并在IDroot的元素中渲染。正如我们前面提到的React中的父/子关系,你通常只需要在应用中用到一次ReactDOM.render方法,因为通过渲染最父级的组件,所有的自组件也都会被渲染。

此时你可能觉得在Javascript中写“HTML”有些奇怪。在你开始学习web开发时,就有人告诉你应该将逻辑层和视图层分开,AKA也就是保持JavaScript与HTML的松耦合。这种模式很强大,但它也有一些缺点。由于篇幅原因,这里将不再详述,你可以查看这篇文档一探究竟。随着你对React的了解越来越多,这种不安应该会很快消退。在render方法中写的“HTML”并不是真正的HTML而是React所称的“JSX”。JSX允许我们很容易地编写类似HTML的语法,这些语法(最终)被转换为轻量级JavaScript对象。然后,React可以获取这些JavaScript对象,并将其创建成“虚拟DOM”或实际DOM的JavaScript表示。这创建了双赢的局面,你可以使用JavaScript的强大功能来访问模板。

看看下面的例子,这就是你的JSX最终被编译的样子:

   class HelloWorld extends React.Component {
       render() {
           return React.createElement('div', null, 'Hello world');
       }
   }

现在,您可以放弃JSX -> JS转换阶段,像上面的代码一样编写您的React组件,但正如你所能想象的,这将是相当棘手的。我不知道有谁没有使用JSX。有关JSX编译的更多信息,你可以查看React Elements vs React Components

到目前为止,我们还没有真正强调我们将要进入的这个新的虚拟DOM范式的重要性。React团队采用这种方法的原因是,因为虚拟DOM是实际DOM的JavaScript表示,React能够持续跟踪当前虚拟DOM(在一些数据更改之后计算)和前一个虚拟DOM(在一些数据更改之前计算)的不同。React将新旧虚拟DOM之间的变化隔离开来,然后只更新需要更改的实际DOM。通常UI有许多状态,这使得管理状态变得困难。每次状态改变都会重新渲染Virtual DOM, React让你更容易思考应用程序处于什么状态。过程是这样的:

一些用户事件改变了应用程序的状态 -> 重新渲染Virtual DOM -> 使用Diff算法计算出新Virtual DOM和前一个Virtual DOM间的差异 -> 只更新实际DOM中必要的变化

因为存在从JSXJS的转换过程,所以在开发过程中需要设置某种类型的转换阶段。在本系列的第2部分中,我将介绍WebpackBabel来进行这种转换。

让我们回顾一下我们的“最重要的React的部分”清单,看看我们现在到哪儿了:


JSX - 允许我们使用HTML语法类似的写法,能够被转换成轻量级的JavaScript对象。

Virtual DOM - 实际DOM的JavaScript表示。

React.Component - 创建一个新组件的方式。

render(方法)- 为特定的组件描述UI的样子。

ReactDOM.render = 将React组件渲染成DOM节点。

state - 组件内部的数据存储(对象)。

construtor (this.state) - 在组件中创建内部状态(state)的方式。

setState - 组件内部更新状态(state)的工具方法并会重新渲染UI。

props - 父组件中传递给子组件的数据。

propTypes - 允许你控制传递给子组件的属性(props)的存在或者类型。

defaultProps - 允许你为自己的组件设置默认的属性(props)。

Component LifeCycle(生命周期) - componentDidMount - 组件被挂载时执行 - componentWillUnmount - 组件注销前执行 - getDerivedStateFromProps - 当组件被挂载并且props发生变化时执行。用于更新组件中的状态(state)当props改变时。

Events - onClick - onSubmit - onChange ...


我们的速度很快。所有粗体部分都是我们已经介绍过了,你至少应该能够解释这些特定组件如何适应React生态系统了。

添加state到你的组件中

接下来是state。之前我们提到过管理用户界面是困难的,因为他们总是有大量的状态。这是React真正发光的地方。每个组件都有能力管理它们自己的状态并且在需要的时候将这些状态传递给他们的子组件。回到前面Twitter账户的例子,UserInfo组件(上面的粉红块)负责管理用户信息状态(或者数据)。如果另外一个组件也需要这个state/data,但那个状态(state)不是UserInfo组件的直接子组件,然后你将创建另一个组件,它将是UserInfo和另一个组件(或两个组件都需要该状态)的直接父组件,然后将状态(state)作为属性(props)传递到子组件中。换句话说,如果你有一个多组件层次结构,那么公共父组件应该管理状态,并通过属性(props)将其传递给其子组件。

让我们看一个使用其内部状态的示例组件:

class HelloUser extends React.Component {
    constructor(props) {
        super(props)
        
        this.state = {
            username: 'tylermcginnis'
        }
    }
    
    render() {
        return (
            <div>
                Hello {this.state.username}
            </div>
        )
    }
}

我们在这个例子中引入了一些新的语法。第一个你注意到的是constructor方法。从上面的定义得知,constructor方法是组件中的状态(state)定义的方式。换句话说,任何你在constructor中设置在this.state上的数据都会成为组件状态(state)的一部分。在上面的代码中我们告诉组件我们想要持续跟踪username的状态。username现在可以被使用通过调用this.state.username的方式,我们在render方法中确实是这样做的。

关于state我们需要讨论的最后一件事是,我们的组件需要有能力改变它自身内部的状态(state)。我们可以通过setState来做这件事。还记得我们前面我们提到过当数据发生改变的时候都会出发re-rendering重新渲染virtual dom吗?

通知我们的应用程序一些数据发生了变化 -> 重新渲染virtual DOM -> 使用diff算法计算前一个virtual DOM和新virtual DOM之间的区别 -> 实际的DOM节点进行必要的更新。

通过我们的应用程序,数据发生了变化的信号就是setState。不论什么时候setState被调用,virtual DOM都会被重新渲染,diff算法会跑起来,并且实际的DOM也会进行必要的更新。

作为旁注,当我们在下面的代码中介绍setState时,我们也会介绍一些清单中事件。一举两得!

因此在下面的例子中,我们将有一个输入框,无论什么时候进行输入,它都会自动更新我们的状态(state)和改变username的值。

class HelloUser extends React.Component {
    construtor(props) {
        super(props)
        
        this.state = {
            username: 'tylermcginnis'
        }
        
        this.handleChange = this.handleChange.bind(this)
    }
    handleChange (e) {
        this.setState({
            username: e.target.value
        })
    }
    render() {
        return (
            <div>
                Hello {this.state.username} <br />
                Change Name:
                <input
                    type="text"
                    value={this.state.username}
                    onChange={this.handleChange}
                />
            </div>
        )
    }
}

注意到我们介绍了一些东西。第一个是handleChange方法。每当用户在输入框中键入时,就会调用这个方法。当handleChange被调用时,它会调用setState来重新定义我们的用户名将其赋值为任何在输入框中输入的值(e.target.value)。记住,每次setState被调用时,React都会创建一个新的virtual DOM,执行diff算法,并且更新实际的DOM节点。

现在来看一下我们的render方法。我们添加了一个包含输入域的新行。它的类型显然是text。这个值将是我们的用户名(username)的值,它最初是在getInitialState方法中定义的,将在handleChange方法中更新。请注意,这里有一个你可能从未见过的新属性,onChangeonChange是一个React事件,它会在每次输入框中的值发生变化时调用你指定的任何方法,在这里我们指定的方法是handleChange

上述代码的处理过程大致是这样的。

用户在输入框中键入 -> handleChange调用 -> 组件的状态被设置为一个新值 -> React重新渲染虚拟DOM -> React执行diff算法计算出差异 -> 实际DOM节点被更新。

之后我们会讲到props,我们将看到一些更高级处理state的用例。

我们到这里了!如果你不能很好的解释上面列举条目中加粗的部分,你最好重读一遍。真正学习React的一个技巧是,不要让被动的阅读给你一种虚假的安全感,以为你知道发生了什么。你应该在编辑器中创建自己的组件,而不只是看我做了些什么。这是你真正开始学习如何使用React的唯一方法。这适用于本教程和后续教程。


从父组件中接收状态(props,propTypes,getDefaultProps)

我们已经讨论过几次props了,因为没有它们真的很难做很多事情。前面的定义指出,props是父组件传递给子组件的数据。这允许我们的React体系结构保持相当直接的状态。处理需要使用特定数据的顶级父组件中的状态,如果你的子组件也需要该数据,则将该数据作为props传递下去。

这里有一个使用props非常简单的例子:

class HelloUser extends React.Component {
 render() {
   return (
     <div> Hello, {this.props.name}</div>
   )
 }
}

ReactDOM.render(<HelloUser name="Tyler"/>, document.getElementById('root'));

注意,在第9行,我们有一个名为name的属性,其值为“Tyler”。现在在我们的组件中,我们可以使用{this.props.name}来获取“Tyler”。

让我们看一个更高级的例子。我们现在有两个组件。一个parent,一个child。父组件将跟踪状态并将该状态的一部分作为props传递给子进程。让我们首先看看父组件。

父组件

class FriendsContainer extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      name: 'Tyler McGinnis',
      friends: ['Jake Lingwall', 'Sarah Drasner', 'Merrick Christensen']
    }
  }
  render() {
    return (
      <div>
        <h3> Name: {this.state.name} </h3>
        <ShowList names={this.state.friends} />
      </div>
    )
  }
}

在这个组件中,没什么特别的:我们有一个初始状态,我们将初始状态的一部分传递给另一个组件。大多数新代码将来自这个子组件,所以让我们更仔细地研究一下。

子组件:

class ShowList extends React.Component {
  render() {
    return (
      <div>
        <h3> Friends </h3>
        <ul>
          {this.props.names.map((friend) => <li>{friend}</li>)}
        </ul>
      </div>
    )
  }
}

请记住,render方法返回的代码是实际DOM的表示形式。如果你不熟悉Array.prototype.map,这段代码看起来有点陌生。map所做的就是创建一个新数组,对数组中的每个项目调用回调函数,并将每个项目调用回调函数的结果填充到新数组中。例如,

const friends = ['Jake Lingwall', 'Sarah Drasner', 'Merrick Christensen'];
const listItems = friends.map((friend) => {
  return "<li> " + friend + "</li>";
});

console.log(listItems);
// ["<li> Jake Lingwall</li>", "<li> Sarah Drasner</li>", "<li> Merrick Christensen</li>"];

注意,我们创建了一个新数组,并将<li> </li>添加到原始数组中的每一项。

map的伟大之处在于它非常适合于React(JavaScript中的内置方法)。因此,在上面的子组件中,我们映射名称,将每个名称包装在一对<li>标记中,并将其保存到listItems变量中。然后,我们的render方法返回一个无序列表,其中包含我们所有的朋友。

在我们停止讨论props之前,让我们再看一个例子。重要的是要理解,无论数据位于何处,都是你希望操作这些数据的。这样可以简化对数据的推理。对于特定数据块的所有getter/setter方法都将始终位于定义该数据的相同组件中。如果你需要在数据所在位置之外操作一些数据,可以将getter/setter方法作为props传递到该组件。让我们看一个这样的例子。

class FriendsContainer extends React.Component {
    construtor(props) {
        super(props)
        
        this.state = {
            name: 'Tyler McGinnis',
            friends: [
                'Jake Lingwall',
                'Sarah Drasner',
                'Merrick Christensen'
            ],
        }
        
        this.addFriend = this.addFriend.bind(this)
    }
    
    addFriend(friend) {
        this.setState((state) => ({
            friends: state.friends.concat([friend])
        }))
    }
    
    render() {
        return (
            <div>
                <h3> Name: {this.state.name} </h3>
                <AddFriend addNew={this.addFriend} />
                <ShowList names={this.state.friends} />
            </div>
        )
    }
}

🚨.注意,在addFriend方法中,我们引入了一种调用setState的新方法。我们不是传递一个对象,而是传递一个函数,然后这个函数传递状态(state)。当你根据以前的状态设置组件的新状态时(正如我们对friends数组所做的那样),你希望给setState传递一个函数,该函数接收当前状态(state)并返回数据与新状态进行合并。

class AddFriend extends React.Component {
    construtor(props) {
        super(props)
        
        this.state = {
            newFriend: ''
        }
        
        this.updateNewFriend = this.updateNewFriend.bind(this)
        this.handleAddNew = this.handleAddNew.bind(this)
    }
    updateNewFriend(e) {
        this.setState({
            newFriend: e.target.value
        })
    }
    handleAddNew() {
        this.props.addNew(this.state.newFriend)
        this.setState({
            newFriend: ''
        })
    }
    render() {
        return (
            <div>
                <input
                    type="text"
                    value={this.state.newFriend}
                    onChange={this.updateNewFriend}
                />
                <button onClick={this.handleAddNew}> Add Friend </button>
            </div>
        )
    }
}
class ShowList extends React.Component {
  render() {
    return (
      <div>
        <h3> Friends </h3>
        <ul>
          {this.props.names.map((friend) => {
            return <li> {friend} </li>
          })}
        </ul>
      </div>
    )
  }
}

你会注意到上面的代码与前面的示例基本相同,只是现在我们能够向好友列表中添加新名称。注意我是如何创建一个新的AddFriend组件来管理我们将要添加的新朋友的。这是因为父组件(FriendContainer)并不关心你添加的新朋友,它只关心作为一个整体的所有朋友(朋友数组)。但是,因为我们坚持只操作关心它的组件的数据的规则,所以我们已经将addFriend方法作为一个props传递到addFriend组件中,并在handleAddNew方法被调用后与新朋友一起调用它。

在这一点上,我建议你在尝试使用上面的代码来重新创建相同的功能,你可能会被卡住3-4分钟。

在我们继续将props之前,我想再讲两个关于props的React特性。它们是propTypesdefaultProps。这里我不多讲,因为这两者都很直接。proptype允许你控制展示,或者传递给子组件的某些props的类型。使用propTypes,你可以指定特定的props是否是必需的,或者特定props的类型。

从React 15开始,PropTypes不再包含在React包中。你需要通过运行npm install prop-types来单独安装它。

defaultProps允许为某些props指定默认(或备份)值,以防这些props从未传递到组件中。

我使用propTypes修改了我们的组件,要求addFriend是一个函数,并将其传递给AddFriend组件。我还使用了defaultProps,如果没有向ShowList组件提供朋友数组,它将默认为空数组。

import React from 'react'
import PropTypes from 'prop-types'

class AddFriend extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      newFriend: ''
    }
  }
  updateNewFriend(e) {
    this.setState({
      newFriend: e.target.value
    })
  }
  handleAddNew() {
    this.props.addNew(this.state.newFriend)
    this.setState({
      newFriend: ''
    })
  }
  render() {
    return (
      <div>
        <input type="text" value={this.state.newFriend} onChange={this.updateNewFriend} />
        <button onClick={this.handleAddNew}> Add Friend </button>
      </div>
    )
  }
}

AddFriend.propTypes: {
  addNew: PropTypes.func.isRequired
}
class ShowList extends React.Component {
  render() {
    return (
      <div>
        <h3> Friends </h3>
        <ul>
          {this.props.names.map((friend) => {
            return <li> {friend} </li>
          })}
        </ul>
      </div>
    )
  }
}

ShowList.defaultProps = {
  names: []
}

好了,这是此教程的最后一部分。让我们看一下检查一下清单,看看还剩下什么。

Component LifeCycle
  - componentDidMount — Fired after the component mounted
  - componentWillUnmount — Fired before the component will unmount
  - getDerivedStateFromProps - Fired when the component mounts and
whenever the props change. Used to update the state of a
component when its props change

就快完了!

组件生命周期

你创建的每个组件都有自己的生命周期事件,这些事件是有用的。例如,如果我们想在第一次渲染时发出ajax请求并获取一些数据,我们应该在哪里做呢?或者,如果我们想在props改变的时候处理一些业务逻辑,我们又该怎么做呢?不同的生命周期事件是这两个问题的答案。让我们把它们分解。

class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      name: 'Tyler McGinnis'
    }
  }
  componentDidMount(){
    // 当DOM节点被挂载的时候执行一次
    // 有益于发送AJAX请求
  }
  static getDerivedStateFromProps(nextProps, prevState) {
    // 这个函数返回的对象将被整合到当前的state中
  }
  componentWillUnmount(){
    // 在组件被卸载之前立即执行
    // 有助于清理一些监听器(listeners)
  }
  render() {
    return (
      <div>
        Hello, {this.state.name}
      </div>
    )
  }
}

componentDidMount - 在初始渲染(render)之后调用一次。因为在调用此方法时组件已经被调用,所以如果有需要,你可以访问虚拟DOM,通过调用this.getDOMNode()来实现。这是一个生命周期事件,在这个事件中,你可以发出AJAX请求以获取一些数据。

componentWillUnmount - 在从DOM卸载组件之前,将立即调用此生命周期方法。这是你可以做必要清理的地方。

getDerivedStateFromProps - 有时你需要根据传递进来的props更新组件的状态(state)。这是生命周期方法,你可以这样做。它将传递propsstate,返回的对象将与当前state合并。

如果你能坚持到现在,做得很好。我希望本教程对你有所帮助,并且你现在至少对React感觉还算满意。

为了更深入地了解React的基本原理,看看我们的React基础课程。或者查看官方教程