Skip to content

第一个组件

实现一个时钟的例子

首先我们需要

function Tick () {
    const clock = <div>
        <h1>Hello, world!</h1>
        <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>

    ReactDOM.render(
        element,
        document.getElementById('root')
    );
}

Tick ()

为了更新时间,我们需要使用定时器,不停的重复渲染他:
setInterval(Tick,1000)

我们把它封装一个组件:

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()}  />,
    document.getElementById('root')
  );
}
setInterval(tick, 1000)

但是这样我们使用这一次时钟,就需要设定一次定时器。我们希望的是,编写一次代码,后续使用的时候都自动更新(上述代码中组件是Clock,Tick是为了定时器而包装的渲染函数)。

为了实现这个目的,我需要把定时器"绑定"在组件上,当组件渲染了,这个定时器就开始工作,不停更新显示的时间。

将函数组件转换成 class 组件

在函数组件中,我们只能接受一个prop,并且只有渲染的时候能改变它的值。但是在class组件中,我们有更多的方法和私有属性,来改变数据。 将整个函数组件的函数体,迁移到我们继承自React.Component的子类的render方法中:

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock date={new Date()}  />,
  document.getElementById('root')
);

我们还需加一个定时器,为此我们需要使用到React的声明周期,当 Clock 组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器。这在 React 中被称为"挂载(componentDidMount)"。

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

class Clock extends React.Component {
    render () {
        return (
            <div>
                <h1>Hello, world!</h1>
                <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
    componentDidMount () {
        console.log('x')
        this.timerID = setInterval(
            () => this.tick(),
            1000
        );
    }

    componentWillUnmount () {
        clearInterval(this.timerID)
    }

    tick () {
        this.props.date = new Date();
    }

}

ReactDOM.render(
    <Clock date={new Date()}  />,
    document.getElementById('root')
);

但是这样我们的视图并不会随着我们改变props.date而改变,而且作为传入的值,我们在组件中进行更改更是不推荐的。为此我们需要使用的state属性,通过state储存数据,并且通过setState改变数据的时候,我们组件的视图也会对应更新。

class Clock extends React.Component {
    constructor(props) {
        super(props)
        this.state = { date: props.date ? props.date : new Date() };
    }
    render () {
        return (
            <div>
                <h1>Hello, world!</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
    componentDidMount () {
        this.timerID = setInterval(
            () => this.tick(),
            1000
        );
    }

    componentWillUnmount () {
        clearInterval(this.timerID)
    }

    tick () {
        this.setState({
            date: new Date()
        });
    }

}

ReactDOM.render(
    <Clock date={new Date()}  />,
    document.getElementById('root')
);

让我们来快速概括一下发生了什么和这些方法的调用顺序:

被传给 ReactDOM.render()的时候,React 会调用 Clock 组件的构造函数。因为 Clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state。我们会在之后更新 state。 之后 React 会调用组件的 render() 方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配 Clock 渲染的输出。 当 Clock 的输出被插入到 DOM 中后,React 就会调用 ComponentDidMount() 生命周期方法。在这个方法中,Clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法。 浏览器每秒都会调用一次 tick() 方法。 在这方法之中,Clock 组件会通过调用 setState() 来计划进行一次 UI 更新。得益于 setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。 一旦 Clock 组件从 DOM 中被移除,React 就会调用 componentWillUnmount() 生命周期方法,这样计时器就停止了。

正确的使用 State

关于setState()你应该了解三件事:

  1. 不要直接修改state 例如,此代码不会重新渲染组件:

    //wrong
    this.state.comment = 'hello'

    而是应该使用setState():

    // correct
    this.setState({comment:'hello'})

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

  2. State的更新可能是异步的 出于性能考虑,React可能会把多个setState()调用合并成一个调用。 因为this.propsthis.state可能会异步更新,所以你不要依赖他们的值来更新下一个状态。 例如,此代码可能无法更新计数器:

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

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

        this.setState(function(state,props){
            return {
                counter:state.counter + state.props.increment
            }
        })
  3. State的更新会被合并 当你调用setState()的时候,React会把你提供的对象合并到当前的state。 例如,你的state包含几个独立的变量:

        constructor(props) {
            super(props);
            this.state = {
            posts: [],
            comments: []
            };
        }

    然后你可以分别调用 setState() 来单独地更新它们:

      componentDidMount() {
            fetchPosts().then(response => {
            this.setState({
                posts: response.posts
            });
            });
    
            fetchComments().then(response => {
            this.setState({
                comments: response.comments
            });
            });
        }

    这里的合并是浅合并,所以 this.setState({comments}) 完整保留了 this.state.posts, 但是完全替换了 this.state.comments。

数据是向下流动的

不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且他们也并不关心它是函数组件还是class组件。 这就是为什么称state为局部的或者是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。 组件可以选择把它的state作为props向下传递到它的子组件中。

    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>

这对于自定义组件同样适用:

    <FormattedDate date={this.state.date}  />

FormattedDate 组件会在其 props 中接收参数 date,但是组件本身无法知道它是来自于 Clock 的 state,或是 Clock 的 props,还是手动输入的:

    function FormattedDate(props) {
    return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
    }

这通常会被叫做"自上而下"或是"单向"的数据流。任何的state总是所属于特定的组件,而且从该state派生的任何数据或UI只能影响树中"低于"他们的组件。 如果你把一个以组件构成的树想象成一个props的数据瀑布的话,那么每一个组件的state就想是在任意一点上给瀑布增加的额外水源,但是它只能向下流动。

在 React 应用中,组件是有状态组件还是无状态组件属于组件实现的细节,它可能会随着时间的推移而改变。你可以在有状态的组件中使用无状态的组件,反之亦然。