无状态函数组件
Stateless Functional Component (SFC)
之前介绍的所谓组件,或叫容器,是基于 JS 的类。
传统组件支持 Props 和 State,也需要 Render 方法。
由于 Render 方法资源成本较高,没有 State 的可用SFC。
SFC 是基于方法的组件,不支持 State 但支持 Props。
Props 从外部导入数据,SFC 不主动求变,不需要 Render。
介绍 SFC 和默认 Props 设置,新建src/stateless.js
:
// 最外层容器组件,包含其他组件,使用 SFC 创建
const Person = (props) => {
return (
<div>
<h1>Stateless Functional Component</h1>
<Student name="Student" />
<Teacher name="Teacher" age={props.age}/>
</div>
)
}
// 设置默认 Props 值,没有指定 Props 时将加载
Person.defaultProps = {
age: 30
}
// 同样使用 SFC 创建的子组件,注意其代码简洁度
const Student = (props) => {
return (
<div>
<h2>Student</h2>
<p>Name: {props.name}</p>
{props.age && <p>Age: {props.age}</p>}
</div>
)
}
// 传统组件,注意 render 方法和 props 引用方式
class Teacher extends React.Component {
render () {
return (
<div>
<h2>Teacher</h2>
<p>Name: {this.props.name}</p>
<p>Age: {this.props.age}</p>
</div>
)
}
}
// 如果 Person 的 age 没有设置,则会用默认的 Props
ReactDOM.render(
<Person age={25}/>,
document.getElementById('app')
);
由于 SFC 相对简介,可以提高程序运行效率。
默认 Props 设置可以使程序更健壮和有弹性。
React开发者工具
Chrome 或 Firefox 都有专门的 React 开发工具。
搜索 React Developer Tools 就可以找到。
安装后,可以在开发者工具中看到 React 页。
其中提供类似 Elements 页的 React 对象信息。
一个彩蛋是$r
变量,可在 Console 中查看。
在 React 开发者工具页选中一个 React 对象。
开发者工具会将其自动将其保存到$r
变量。
在 Console 中输入$r
可查看该对象的细节。
再次简化代码
查看之前 TODO LIST 程序中清空列表的方法:
onRemoveTasks() {
this.setState(() => {
return {
tasks: []
};
});
}
仅仅为了清空一个状态值不需要如上许多行:
onRemoveTasks() {
this.setState(() => ({tasks: []}));
}
做了以上简化后,代码简洁了许多。
注意,用箭头函数返回对象要包裹括号。
否则,标识对象的花括号会被当作函数体标识。
localStorage 和 JSON
JavaScript 提供基于 localStorage 模块的本地存储服务。
注意,该功能将所有数据转换为字符串类型保存。
在开发者工具的控制台可以执行以下命令:
> localStorage.setItem('age',20);
< undefined
> localStorage.getItem('age');
< "20"
> localStorage.removeItem('age');
< undefined
> localStorage.getItem('age');
< null
注意赋值为整形的 age 变量返回的是字符串类型数值。
对于对象类,JavaScript 提供基于 JSON 模块的转换功能。
JSON (JavaScript Object Notation) JavaScript 对象标记类型。
JSON.stringify() 方法将字符串表示的对象转为 JSON 对象。
JSON.parese() 方法将 JSON 对象转换为 JavaScript 对象。
> JSON.stringify({ age: 26 });
< "{"age":26}"
> const json = JSON.stringify({ age: 26});
< undefined
> json
< "{"age":26}"
> JSON.parse(json);
< {age: 26}age: 26__proto__: Object
> JSON.parse(json).age
< 26
注意返回的对象值为整型,而不再是字符串类型。
理解 JSON.stringify 和 JSON.parse 相配合的用法。
Todo App 更新
使用以上介绍的方法,更新src/todo.js
如下:
// 无状态函数组件 Stateless Functionl Component
// 对于没有 state 的组件,可以简化其代码
class TodoApp extends React.Component {
// 构造函数可引入外部导入的 props 值
constructor(props) {
// 继承父类的 props
super(props);
this.onRemoveTasks = this.onRemoveTasks.bind(this);
// 绑定新建的删除单个任务的函数到 this
this.onRemoveTask = this.onRemoveTask.bind(this);
this.onHandlePick = this.onHandlePick.bind(this);
this.onAddTask = this.onAddTask.bind(this);
// 使用外部导入的 props 定义初始 state
this.state = {
tasks: props.tasks
}
}
// 只有基于类的组件才支持生命周期函数
// 组件加载后执行的生命周期函数
componentDidMount() {
// 用 try 和 catch 防止非法数据造成程序崩溃
try {
// 从本地存储获取字符串方式保存的 tasks 对象
const json = localStorage.getItem('tasks');
// 将字符串方式保存的对象转换为 JS 对象
const tasks = JSON.parse(json);
// 如果任务列表不为空,则更新到状态
if (tasks) {
// 状态名与状态值变量名相同可简化定义方法
this.setState(() => ({ tasks }));
}
} catch (e) {
console.log(e)
}
}
componentDidUpdate(props, state) {
// 只有组件更新成功添加了任务,才保存任务列表到本地存储
if (state.tasks.length !== this.state.tasks.length) {
// 将程序中保存的 JS 对象转换为 JSON 对象本地保存
const json = JSON.stringify(this.state.tasks);
localStorage.setItem('tasks', json);
}
}
// 在组件卸载前运行的生命周期函数
componentWillUnmount() {
console.log('will unmount');
}
onRemoveTasks() {
this.setState(() => ({ tasks: [] }));
}
// 添加删除单个任务的函数
onRemoveTask(target) {
this.setState((state) => ({
// 使用 filter 方法返回任务列表中除指定任务之外的任务
tasks: state.tasks.filter((task) => task !== target)
}));
}
onHandlePick() {
// Math.floor 方法取整
const randomNum = Math.floor(
// 以下方法计算随机 index 值
Math.random() * this.state.tasks.length);
const task = this.state.tasks[randomNum];
alert(`Let's do ${task}`);
}
// 该方法在输入错误时返回错误信息,正确时更新任务列表
onAddTask(task) {
if (!task) {
return 'Enter Valid Value!'
// 如果指定 task 值可以查到有效 index 则证明输入重复
} else if (this.state.tasks.indexOf(task) > -1) {
return 'Task already exists!'
} else {
this.setState((state) =>
// concat 方法添加数组元素并返回一个新数组
({ tasks: state.tasks.concat([task]) }));
}
}
render() {
const title = 'TODO LIST';
const subtitle = 'Make a wise plan!';
return (
<div>
<Header
title={title}
subtitle={subtitle}
/>
<Action
hasOptions={this.state.tasks.length > 0}
onHandlePick={this.onHandlePick}
/>
<Tasks
tasks={this.state.tasks}
onRemoveTasks={this.onRemoveTasks}
// 将删除单个任务的方法传入任务列表的 props
onRemoveTask={this.onRemoveTask}
/>
<AddTask
onAddTask={this.onAddTask}
/>
</div>
);
}
};
// 为程序外层容器设置默认的任务列表状态值
TodoApp.defaultProps = {
tasks: []
}
// 优化为无状态函数组件,省略了 render 方法
const Header = (props) => {
return (
<div>
<h1>{props.title}</h1>
<h2>{props.subtitle}</h2>
</div>
);
}
// 优化为无状态函数组件,省略了 render 方法
const Action = (props) => {
return (
<div>
<button
onClick={props.onHandlePick}
disabled={!props.hasOptions}
>
Choose a task for me!
</button>
</div>
);
}
// 任务列表外层容器,包含任务数量提示,清除列表按钮和任务列表
const Tasks = (props) => {
return (
<div>
<p>
{
props.tasks.length ?
`You have ${props.tasks.length} tasks:` :
'You do not have tasks!'
}
</p>
<button onClick={props.onRemoveTasks}>
Remove All
</button>
<ol>
{
props.tasks.map((task) =>
<Task
key={Math.random()}
task={task}
// 将删除单个任务方法传入单任务 props
onRemoveTask={props.onRemoveTask}
/>
)
}
</ol>
</div>
);
}
// 优化为无状态函数组件,省略了 render 方法
const Task = (props) => {
return (
<div>
<li>{props.task}
<button
onClick={(e) =>
props.onRemoveTask(props.task)}
>
remove
</button>
</li>
</div>
);
}
// 添加任务的容器,包含错误信息和提交新任务的表单
class AddTask extends React.Component {
constructor(props) {
super(props);
// 注意这里对本地定义的添加任务方法做绑定处理
this.onAddTask = this.onAddTask.bind(this);
// 在 state 中定义一个默认为空的错误信息值
this.state = {
error: undefined
}
}
onAddTask(e) {
e.preventDefault();
// 清理输入任务数据中可能存在的前后空格字符
const task = e.target.elements.task.value.trim();
// 处理添加任务的操作,返回可能存在的错误信息
const error = this.props.onAddTask(task);
// 将返回的可能错误信息放入状态值
this.setState(() => ({ error }));
// 在无错误,即添加任务成功时,清空表单输入框
if (!error) {
e.target.elements.task.value = '';
}
}
render() {
return (
// 在错误信息存在时输出错误提示信息
<div>
{this.state.error && <p>{this.state.error}</p>}
<form onSubmit={this.onAddTask}>
<input type="text" name="task" />
<button>Add Task</button>
</form>
</div>
);
}
}
ReactDOM.render(<TodoApp />, document.getElementById('app'));
以上,所有更新和重点部分已经做了行内注释。
Counter App 练习
作为对本节内容的练习,更新 Counter 程序如下:
class Counter extends React.Component {
constructor(props) {
super(props);
this.onAddOne = this.onAddOne.bind(this);
this.onMinusOne = this.onMinusOne.bind(this);
this.onReset = this.onReset.bind(this);
this.state = {
count: 0
}
}
componentDidMount() {
// 从本地存储获取计数器值并转换为整型
const text = localStorage.getItem('count');
const value = parseInt(text, 10);
// 如果本地存储数据非法,转换结果会为 NaN
if (!isNaN(value)) {
// 确认转换结果为有效整数后写入 State
this.setState(() => ({count: value}));
}
}
componentDidUpdate(props, state) {
// 判断只有在计数器发生变化时触发本地存储作业
if (state.count !== this.state.count) {
// 这里主要防止由无效的 reset 操作触发的资源浪费
localStorage.setItem('count', this.state.count);
}
}
onAddOne() {
this.setState((state) => ({count: state.count + 1}))
}
onMinusOne() {
this.setState((state) => ({count: state.count - 1}));
}
onReset() {
this.setState(() => ({count: 0}));
}
render() {
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.onAddOne}>+1</button>
<button onClick={this.onMinusOne}>-1</button>
<button onClick={this.onReset}>reset</button>
</div>
);
}
}
ReactDOM.render(
<Counter/>,
document.getElementById('app'));
以上主要应用了生命周期函数和本地存储方法。