React 入门教程之四 -- rendering, components 和 state
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 添加 state。state 类似于 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 实例被使用,这就让我们可以使用 state 或 lifecycle 等功能。
添加 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 中 Clock 的 date 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.props 和 this.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-down 或 unidirectional 数据流。任何 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,反过来亦可。
标签:无