rendering elements 渲染元素

一个 element 表示我们想要显示在屏幕上的内容:

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

不同于浏览器 DOM 中的 elements,React elements 是简单的 objects 且可以很方便的创建,React DOM 会严格的刷新 DOM 并匹配对应的 React elements。

容易混淆的概念是 component 和 element,区别是 component 是用来创建 element 的。在后续章节会介绍。

在 DOM 中渲染元素

我们的 html 页面中定义了一个 div 容器:

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

我们将其称作 root DOM 节点,因为它所有的内容都是被 React DOM 管理的。

通常情况下使用 React 创建的程序只有一个 root DOM 节点。如果你是将 React 整合到现有网站中,你可以有任意个独立的 root DOM 节点。

将 React elements 渲染到 root DOM 节点,需要通过调用 ReactDOM.render(),并将 React element 和 root DOM 节点作为传入参数:

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

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

此时页面会显示 hello world。

刷新渲染的元素

React element 是 immutable 不可改变的,当创建了一个 element 后不可以修改其 children 或 attributes,一个 element 就好像一个视频的一帧,它表示了某一时间点的 UI。

从我们目前学到的知识,唯一刷新 UI 的方法就是重新创建新的 elements 然后调用 ReactDOM.render(),通过设置 setInterval 来定时刷新:

const tick = () => {
    const element = <h1>{new Date().toLocaleTimeString()}</h1>;

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

这样就会每秒钟创建一个新的 element 并通过 ReactDOM.render() 渲染到界面。

通常情况下大多数 React app 只会调用 ReactDOM.render() 一次。下一章节会介绍如何将封装到 component 中。

React 只会更新必要的内容

React DOM 会比较其当前和上一个状态的,然后只对有了变化的部分进行更新来达到最终期望的状态。

我们打开上面示例的运行页面,通过chrome 的开发工具查看 elements 情况,可以看到只有时间元素每秒在刷新:
![2021-02-24T07:44:17.png][
即使我们每秒钟都新建并渲染 element,但是只有时间文本 node 是一直通过 React DOM 在刷新的。通过以上的实验,思考我们的 UI 在某个时间点应该是什么样的,而不是只想这着去修改它。

components 和 props

components 将 UI 元素分割为独立的,可复用的片段,每个片段都是单独存在的。这一章节介绍 component 的概念,更多细节参考:React.Component

components 类似于 JavaScript 的 functions,它可以接受抽象的输入数据(props),然后返回 React elements 用来在界面上显示。

Function 和 Class Components

最简单的定义 component 方式就是定义一个 JavaScript function:

const Welcome = (props) => {
    return <h1> hello, {props.name}</h1>
}

上面的 function 是一个有效的 React component,因为它接受一个单参数 props object 作为传入数据并返回一个 React element。我们称这种 component 为 function component。

也可以使用 ES6 的 class 定义 component:

class Welcome extends React.Component {
    render() {
        return <h1>hello, {this.props.name}</h1>
    }
}

以上两种定义方式是一致的。

需要注意的是 components 名称必须是以大写字母开头,因为 React 会见以小写字母开头的 components 作为 DOM tags 标签,如:<div /> 表示一个 html div 标签。

rendering a component

上面的介绍中,我们只遇到了 DOM tags 标签类型的 React elements,例如:

const element = <div />;

elements 也可以表示用户自定义的 components:

const element = <Welcome name='marco' />

当 React 检测到使用了用户自定义的 components 它会将此 JSX 内的 attributes 或 children 作为一个 object 传入 component,这个 object 叫做 props

下面的示例会输出 hello, marco:

const Welcome = (props) => {
    return <h1> hello, {props.name}</h1>;
}

const element = <Welcome name='marco' />;
ReactDOM.render(
    element,
    document.getElementById('root')
);

以上示例过程如下:

  • 首先调用 ReactDOM.render() 渲染 <Welcome name="Sara" /> 元素.
  • React 调用 Welcome component 使用 {name: 'Sara'} 作为 props.
  • Welcome component 返回一个 <h1>Hello, Sara</h1> 元素.
  • React DOM 高效的更新 DOM 来匹配 <h1>Hello, Sara</h1> 结果.

构建 component

component 可以在其输出中引入关联其他 components。这可以让我们在一个 component 内抽象出一个多层的结构。一个 button,一个 form,一个 dialog 或者一个 screen,在 React app 中他们都统称为 components。

例如我们可以创建一个 App component 来渲染多个 Welcome component:

const Welcome = (props) => {
    return <h1> hello, {props.name}</h1>;
}

const App = () => {
    return (
        <div>
            <Welcome name='marco' />
            <Welcome name='tim' />
            <Welcome name='jone' />
        </div>
    )
}
ReactDOM.render(
    <App />,
    document.getElementById('root')
);

以上示例中,没有定义 App 的 props,因为不需要给其传入数据,也是可以的。

一般情况下,新建的 React app 只有一个顶层的 App component。

拆解 component

不要害怕将一个 component 拆解为多个小 components。例如下面这个 Comment component:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

它的 props 包含一个 author object,一个 text,一个 data,描述了一个社交网站上一个 commit 的内容。

修改这个 component 有点困难,因为它有很多的嵌套,同时也难以复用它的内部组件。下面我们尝试拆解这个 component。

首先我们拆解出 Avatar

const Avatar = (props) => {
    return (
        <img className="Avatar"
            src={props.user.avatarUrl}
            alt={props.user.name}
        />
    );
}

Avatar 并不需要知道它被用于 commit 中,因此我们修改其 prop 名称为一个更加通用的:user。推荐从 component 本身为出发点命名 props,而不是考虑什么地方使用它。

现在我们可以简化 Commit component:

function Comment(props) {
    return (
        <div className="Comment">
            <div className="UserInfo">
                <Avatar user={props.author} />
                <div className="UserInfo-name">
                    {props.author.name}
                </div>
            </div>
            <div className="Comment-text">
                {props.text}
            </div>
            <div className="Comment-date">
                {formatDate(props.date)}
            </div>
        </div>
    );
}

我们将 props.author 作为 user 数据传入 Avatar component 中。

下面我们拆解 UserInfo,其中包含一个 Avatar component:

const UserInfo = (props) => {
    return (
        <div className="UserInfo">
            <Avatar user={props.user} />
            <div className="UserInfo-name">
                {props.user.name}
            </div>
        </div>
    );
}

然后进一步简化 Commit:

function Comment(props) {
    return (
        <div className="Comment">
            <UserInfo user={props.user} />
            <div className="Comment-text">
                {props.text}
            </div>
            <div className="Comment-date">
                {formatDate(props.date)}
            </div>
        </div>
    );
}

拆解 component 在开始看起来使工作量变大了,但是在稍微复杂写的 app 中我们就能够利用这些可复用的 components。一条基本准则是:如果 UI 中的某一部分被多次使用,如 button,panel,Avatar等,或者其自身结构比较复杂,如:App, FeedStory, Comment 等,将他们拆解为独立 components 是一个好的选项。

props 是只读的

当把一个 component 定义为 function 或 class 时,需要注意的是不可以修改 props 的值。

考虑下面的 function:

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

以上的 function 被称作 pure 纯粹的,以为它没有尝试修改输入数据。

作为对比,下面的就是 impure 不纯粹的 function,因为会尝试修改它的输入数据:

function sum(a, b) {
  a = b;
}

React 程序有一条限制条件:所有的 React components 都需要是 pure function 来对待 props 数据

当然应用程序的 UI 是随时间动态变化的。下一节我们会介绍 state 的概念。通过 state 可以使 React components 在运行期间修改它们的输出 elements 来响应用户动作,网络响应等。同时不违反上面的那条规则。

state 和 lifecycle

这一节介绍 React components 中 state 和 lifecycle 的概念。

在前一章的示例中,我们通过一个 tick function 在指定时间间隔通过创建新 element 并渲染的方式刷新 UI:

const tick = () => {
    const element = (
        <div>
            <h1>hello world</h1>
            <h2>{new Date().toLocaleTimeString()}</h2>
        </div>
    );

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

下面我们介绍通过创建一个封装好的 Clock component,设置定时器并更新其自身。

首先我们根据上面的示例创建 Clock component:

const Clock = (props) => {
    <div>
        <h1>hello world</h1>
        <h2>{props.date.toLocaleTimeString()}</h2>
    </div>
}

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

tick 调用 Clock component 并定义 date prop 的数据供 Clock 使用。但是上面的实现缺乏一个基本需求,那就是 Clock 应该在其自身中定义定时器并每秒刷新数据的。

我们想要在渲染时达到如下效果调用 Clock:

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

为了实现上述功能,需要为 Clock 添加 statestate 类似于 props 但是它是由 component 私有且完全控制的。

首先我们需要将 component 转换为 class 模式,转换过程如下:

  • 首先创建一个 ES6 class,且继承自 React.Component
  • 添加一个 render() function,将原 component function 的返回元素放入其返回值中
  • render() 中用 this.props 代替 props
class Clock extends React.Component {
    render() {
        return (
            <div>
                <h1>hello world</h1>
                <h2>{this.props.date.toLocaleTimeString()}</h2>
            </div>
        );
    }
}

当 update 更新发生时会自动调用 render function。但当我们将 <Clock /> 放入 DOM 后,将只会有一个 Clock object 实例被使用,这就让我们可以使用 statelifecycle 等功能。

添加 state

下面我们将 date 数据直接放入 CLock component 中。

首先将 render 中的 this.props.date 修改为 this.state.date

class Clock extends React.Component {
    render() {
        return (
            <div>
                <h1>hello world</h1>
                <h2>{this.state.date.toLocaleTimeString()}</h2>
            </div>
        );
    }
}

然后添加 class constructor 构造器给 this.state 赋初值:

class Clock extends React.Component {
    constructor(props) {
        super(props);
        this.state = {date: new Date()}
    }
    render() {
        return (
            <div>
                <h1>hello world</h1>
                <h2>{this.state.date.toLocaleTimeString()}</h2>
            </div>
        );
    }
}

注意 class component 总是应该使用 constructor 且初始化参数为 props。child class 有 constructor 时需要调用 super 来初始化 parent class,具体语法参考我的 JavaScript 教程:https://blog.niekun.net/archives/2011.html

然后删除渲染到 DOM 中 Clockdate prop,以及我们设置的 setInterval 定时器:

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

修改完成后的完整代码如下:

class Clock extends React.Component {
    constructor(props) {
        super(props);
        this.state = {date: new Date()}
    }
    render() {
        return (
            <div>
                <h1>hello world</h1>
                <h2>{this.state.date.toLocaleTimeString()}</h2>
            </div>
        );
    }
}

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

下面我们实现 Clock 设置自己的定时器并每秒更新。

添加 lifecycle method

对于包含很多 components 的程序,但某个 component 不再需要是需要及时释放其占用的资源。

我们需要 Clock 第一次在 DOM 中渲染时设置一个 timer 定时器,在 React 中叫做 mounting 载入。同时我们需要当 Clock 在 DOM 中被删除时清除这个定时器,在 React 中叫做 unmounting 卸载。

我们可以在 components 载入或载出时通过定义特殊的 method 来运行特定指令:

  componentDidMount() {
  }

  componentWillUnmount() {
  }

这些 methods 叫做 lifecycle methods

componentDidMount method 会在 component 第一次输出到 DOM 后被自动调用,我们可以将定时器定义在这里:

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

这样当 Clock 渲染到 UI 后会自动启动这个定时器。注意使用 this 定义的参数可以在 class 中任意地方被调用。

注意 setInterval 中定义的响应动作需要写在 callback 内 () => {} 中,不要直接写:setInterval(this.tick, 1000)。因为如果要在 callback 调用 method 需要在 constructor 中做如下定义:

this.tick = this.tick.bind(this);

componentWillUnmount method 会在 component 将要被删除时自动调用,我们将定时器在这里取消:

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

接下来我们定义每秒都会自动运行的 tick method,通过 this.setState() 来更新本地 state 中的设置:

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

最终的完整代码如下:

const React = require('react')
const ReactDOM = require('react-dom')

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>{this.state.date.toLocaleTimeString()}</h2>
            </div>
        );
    }
}

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

现在整个处理流程如下:

  • ReactDOM.render() 传入 <Clock /> 后,React 调用 Clock 构造器初始化 state 为一个包含 date 的 object
  • 然后 React 调用 Clock 的 render() 查询到需要显示的 UI 元素,然后更新 DOM 以匹配 Clock 的输出
  • 当 Clock 的输出嵌入到 DOM 后会调用 componentDidMount,Clock 告诉浏览器设置一个定时器每秒调用 tick()
  • 每秒钟调用一次 tick(),这里面 Clock component 通过 setState() 配置了其 UI 更新任务,通过 setState() React 就知道了 state 发生了变化并再次调用 render() 监测需要显示的内容,此时的 this.state.date 和上一次的发生了变化,React 就会更新 DOM 到最新的状态。
  • 当 Clock 从 DOM 中删除后,React 会调用 componentWillUnmount 并结束定时器

正确使用 state

关于 state 的使用需要如下的几点要求。

第一点,不要直接修改 state

下面的语法不会触发重新 render 渲染 component:

this.state.comment = 'Hello';

正确的语法为使用 setState()

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

唯一可以对 state 赋值的是在 constructor 构造器中。

第二点,state 更新是异步的

React 为了性能可能会在一次 component update 更新中捆绑多个 setState() 调用,由于 this.propsthis.state 可能会被异步更新,所以不要依赖他们的数据来计算后续的 state

如下示例可能会错误的更新 counter:

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

为了实现是这个需求,使用 setState 的另一种格式:传入一个 function,第一个参数为当前 state,第二个参数为 props 然后内部计算 state 更新:

this.setState(function (state, props) {
    return {
        counter: state.counter + props.increment
    };
});

第三点,state 更新会合并

当调用 setState() 后,会合并设置的 object 到当前 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
            });
        });
    }

合并过程是自动完成的,所以通过 setState 修改 comments 只会更新 comments 而不会改变 posts。

数据向下传递

一个 component 的 child 或 parent 都不会知道当前 component 是包含 state 还是不包含,且不关心是通过 function 还是 class 方式构建的 component。所以 state 被认为是封装的不能够被外界所访问。

component 的 state 可以作为 props 向它的 child component 传递:

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

如上所示 FormattedDate 可以接受 date prop,它并不知道数据来自 parent 的 state 还是 props:

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

通常这叫做 top-downunidirectional 数据流。任何 state 都被某个特定 component 所有,state 的数据只能在其 component 的 child 中传递出去。

我们通过建立 App component 并构建三个 Clock component 来展示 component 之间是互相独立的:

function App() {
    return (
        <div>
            <Clock />
            <Clock />
            <Clock />
        </div>
    );
}

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

每个 Clock 都有个各自的定时器并独立更新。

在 React 中,components 定义为 stateful 还是 stateless 的取决于其在运行中可能的变化,可以在 stateful 的 component 中使用 stateless 的 component,反过来亦可。

标签: none

添加新评论