React App – SFC

无状态函数组件
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'));

以上主要应用了生命周期函数和本地存储方法。

Leave a comment