一. 使用React编写todolist
1. 占位符Fragment;
先从下面图片开始了解本节的目录结构、代码、效果:
目录结构:
代码:
index.js
TodoList.js
效果:
上图中红色方框中的dom结构是用一个div包裹的,但是有时候我们在实现flex布局的时候该div包裹会影响我们的样式。React提供了一个占位符Fragment。我们引入该占位符,同时将最外层的div包裹替换成Fragment即可。
修改后的代码:
TodoList.js
效果:
二. React中的响应式设计思想和事件绑定
如果用原生js或jquery来做todolist的话,我们一般是获取输入框的value值,然后通过document.getElementById获取列表区域,然后将value值挂载到列表区域。
React与上面直接操作dom的形式不同。它是一个响应式的框架,通过操作数据,React会感知到数据的变化,自动地帮你去生成dom。所以写React项目的时候,我们只需要关注数据层的变化。
1. Constructor
在js中,一个对象或者类就有一个constructor构造函数。在React中,当我们去创建或者使用TodoList这个组件的时候,constructor这个构造函数时优于别的任何函数的,自动的最先被执行的一个函数。所以我们可以在该构造函数里定义数据。
2. 如何双向绑定
以todolist的input框的value值为例,我们将它和todolist状态中的inputValue进行绑定。
<input value={this.state.inputValue}/>
想在JSX语法中使用js的变量(如this.state.inputValue),我们需要用大括号将其括起来。
3. React事件绑定
以监听input框的change事件为例,原生js是onchange, 在React中应该写成onChange。
TodoList.js:
import React, { Component, Fragment } from 'react';
class TodoList extends Component {
constructor(props) {
//固定写法,意思是TodoList是继承自React的component这个组件,我们这要调用一次父类的构造函数
super(props);
//React的数据需要定义在状态里面,下面的state就是表示组件的状态
this.state = {
inputValue: 'hahah', //input框的value值
list: [] //数组中的每一项
}
}
render() {
return ( //jsx语法要求render函数返回的内容外层必须用一个标签包裹
<Fragment>
<div>
<input
value={this.state.inputValue}
onChange={this.handleInputChange}
/>
<button>提交</button>
</div>
<ul>
<li>学英语</li>
<li>学React</li>
</ul>
</Fragment>
)
}
handleInputChange(e) {
console.log(e.target.value); //控制台会打印出第一次修改后的value值,但是input框内容未改变
}
}
export default TodoList; //导出TodoList组件
按照常理,我们想input框的value跟着改变的话,加入如下代码即可:
handleInputChange(e) {
this.state.inputValue = e.target.value; //Cannot read property 'state' of undefined
}
实际上,上面的this并不是指向TodoList这个组件,打印this结果是undefined。我们可以通过bind来更改this的指向。
<input
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
handleInputChange(e) {
this.state.inputValue = e.target.value; //此时this指向的是该组件,但是还是不能改变输入框的值
}
//原因是React不能直接通过this.state来改变state里面的值,它提供了一个方法:this.setState({})
最终解决办法:
handleInputChange(e) {
this.setState({
inputValue: e.target.value
})
}
三. 实现TodoList新增删除功能
import React, { Component, Fragment } from 'react';
class TodoList extends Component {
constructor(props) {
//固定写法,意思是TodoList是继承自React的component这个组件,我们这要调用一次父类的构造函数
super(props);
//React的数据需要定义在状态里面,下面的state就是表示组件的状态
this.state = {
inputValue: '', //input框的value值
list: [] //数组中的每一项
}
}
render() {
return ( //jsx语法要求render函数返回的内容外层必须用一个标签包裹
<Fragment>
<div>
<input
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
<button onClick={this.handleBtnClick.bind(this)}>提交</button>
</div>
<ul>
{
this.state.list.map((item, index) => {
return (
<li
key={index}
onClick={this.handleItemDelete.bind(this, index)}
>{item}</li>
)
})
}
</ul>
</Fragment>
)
}
//输入框value发生改变
handleInputChange(e) {
this.setState({
inputValue: e.target.value
})
}
//点击提交将value加入列表项
handleBtnClick() {
this.setState({
//es6的展开运算符,将this.state.list该数组展开成参数列表的形式(如:[...[1, 2]] => [1, 2])
list: [...this.state.list, this.state.inputValue],
inputValue: ''
})
}
//点击列表项删除
handleItemDelete(index) {
//immutable, React不允许我们直接对state做任何修改
const list = this.state.list;
list.splice(index, 1);
this.setState({
list: list
})
}
}
export default TodoList; //导出TodoList组件
四、JSX语法细节补充
1、如何在JSX语法中编辑注释。
我们只需要将注释用花括号包裹即可。注意:单行注释应该重起一行。
2、在React中引入外部样式。
新建样式文件style.css
在TodoList.js中引入
给input框添加类名
结果显示样式发生了改变,但是控制台报错:
这是因为React为了避免将input中的class与类相混淆,不推荐我们使用class来表示元素的类名,推荐我们使用className来代替。
将上面的class修改成className,样式发生了改变且控制台不报错。
3、对输入框输出内容进行转义。
对于上图的结果,我们希望输出结果中的h1被解析转义出来。代码需进行如下修改:
最终结果如下所示:
4、点击标签左边文字使光标聚焦在输入框。
在HTML中label标签的作用是扩大输入框的点击区域。我们需要实现一个功能:点击输入内容,让光标聚焦在输入框。
添加如下代码实现:
效果如下:
但是控制台报错:
原因和上面一样,也是React为了避免label标签中的for属性和js的for语句混淆,不推荐使用for来关联input的id。使用htmlFor来替换for。
效果一样,同时控制台也不报错。
五. 拆分组件与组件之间的传值
1. 将TodoList这个组件拆分成两个组件
首先在src目录下创建一个TodoItem.js组件
输入内容如下:
在TodoList中引入该组件:
修改TodoList中的JSX:
注意:应该用一个元素包裹TodoItem与注释,否则会报错。
结果界面:
显然这不是我们想要的结果,这是因为我们将TodoItem里面内容写死了。要实现我们的功能,我们需要父组件向子组件传值
2. 父组件向子组件传值
通过属性的方式进行传值,子组件通过this.props接收。具体例子如下:
通过属性传值:
通过this.props接收:
结果界面:
3. 子组件操作父组件的数据
接下来实现点击每个列表项自动删除的功能:
首先子组件需要获取被点击那一项的index值:
接下来我们需要将该值传递给父组件的handleItemDelete方法,删除数组中对应index的那一项。
父组件:
子组件:
此时,当我们点击列表项的时候报错
原因是因为我们子组件中调用this.props.deleteItem方法实际上就是调用父组件的this.handleItemDelete方法,但是这里的this指向是子组件,我们需要强制改变this的指向,使它指向父组件。
最后功能成功实现了,也没有报错。
六. TodoList代码优化
1. TodoItem.js代码优化
利用ES6的解构赋值对代码优化:
优化前:
优化后:
2. TodoList.js代码优化
主要优化部分:
1. 在constructor部分改变this的指向
2.将JSX语法中的逻辑部分用一个方法来表示
3.使用新版React推荐的setState语法(不直接返回一个对象,而是返回一个函数,函数内再return一个对象)
4.解决使用循环带来的key值问题
优化前:
import React, { Component, Fragment } from 'react';
import './style.css';
import TodoItem from './TodoItem';
class TodoList extends Component {
constructor(props) {
//固定写法,意思是TodoList是继承自React的component这个组件,我们这要调用一次父类的构造函数
super(props);
//React的数据需要定义在状态里面,下面的state就是表示组件的状态
this.state = {
inputValue: '', //input框的value值
list: [] //数组中的每一项
}
}
render() {
return (
<Fragment>
<div>
<label htmlFor="insert">输入内容</label>
<input
id="insert"
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
className='input'
/>
<button onClick={this.handleBtnClick.bind(this)}>提交</button>
</div>
<ul>
{
this.state.list.map((item, index) => {
return (
<div>
<TodoItem
content={item}
index={index}
deleteItem={this.handleItemDelete.bind(this)}
/>
</div>
)
})
}
</ul>
</Fragment>
)
}
//输入框value发生改变
handleInputChange(e) {
this.setState({
inputValue: e.target.value
})
}
//点击提交将value加入列表项
handleBtnClick() {
this.setState({
//es6的展开运算符,将this.state.list该数组展开成参数列表的形式(如:[...[1, 2]] => [1, 2])
list: [...this.state.list, this.state.inputValue],
inputValue: ''
})
}
//点击列表项删除
handleItemDelete(index) {
//immutable, React不允许我们直接对state做任何修改
const list = this.state.list;
list.splice(index, 1);
this.setState({
list: list
})
}
}
export default TodoList; //导出TodoList组件
优化后:
import React, { Component, Fragment } from 'react';
import './style.css';
import TodoItem from './TodoItem';
class TodoList extends Component {
constructor(props) {
//固定写法,意思是TodoList是继承自React的component这个组件,我们这要调用一次父类的构造函数
super(props);
//React的数据需要定义在状态里面,下面的state就是表示组件的状态
this.state = {
inputValue: '', //input框的value值
list: [] //数组中的每一项
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
}
render() {
return (
<Fragment>
<div>
<label htmlFor="insert">输入内容</label>
<input
id="insert"
value={this.state.inputValue}
onChange={this.handleInputChange}
className='input'
/>
<button onClick={this.handleBtnClick}>提交</button>
</div>
<ul>
{this.getTodoItem()}
</ul>
</Fragment>
)
}
//输入框value发生改变
handleInputChange(e) {
const value = e.target.value;
this.setState(() => ({
inputValue: value
}));
}
//点击提交将value加入列表项
handleBtnClick() {
this.setState((prevState) => ({
//prevState指的是修改数据之前那次数据,该写法可以避免我们有时候不小心改变了state的状态
list: [...prevState.list, prevState.inputValue],
inputValue: ''
}));
}
//遍历list,显示每一个item
getTodoItem() {
return this.state.list.map((item, index) => {
return (
<TodoItem
content={item}
index={index}
key={index}
deleteItem={this.handleItemDelete}
/>
)
})
}
//点击列表项删除
handleItemDelete(index) {
this.setState((prevState) => {
const list = prevState.list;
list.splice(index, 1);
return {list};
})
}
}
export default TodoList; //导出TodoList组件
七. 围绕React衍生出的一些思考
1. 命令式开发、声明式开发
命令式开发: 我们之前使用的jquery开发都是直接操作dom,我们把这种方式称为命令式开发。就是我们开发的时候,需要告诉页面,你要如何去获取,接着又如何去挂载,这种命令式的方式。
声明式开发:React就是声明式的开发,比如我们要开发一个网页,之前命令式的开发需要一步一步地去指导如何去开发。而React不是这样的,它是面向数据来编程的,我们只需要把数据构建出就可以,React会自动根据数据去构建这个网页。数据就类似一个图纸,React根据图纸会自动地帮你去构建这栋房子。
2.可以与其它框架并存
在React项目的入口文件,我们将TodoList这个组件挂载到id等于root的元素下面,我们开发过程中,第三方框架在id等于root的元素之外使用是不会影响到React的,同样React也不会影响其他的框架。
3. 组件化
4. 单向数据流
父组件可以传数据给子组件,但是不允许子组件直接改变传递过来的数据。这是因为当该数据被多个组件使用的时候,我们子组件改变了该数据,别的组件使用该数据时也被改变了,而且在出现bug时不容易定位。
5.视图层的框架
因为React在开发大型项目的时候,组件树比较复杂,单靠React父子组件传值很麻烦,我们需要引用别的像Redux这样的数据层的框架来辅助我们开发。React只负责解决页面与数据渲染上的一些问题,至于组件之间如何传值,它并不负责。对于Todolist这种小型的项目,借助React内部的传值机制就可以,但是大中型项目,这时远远不够的。
6. 函数式编程
我们在React文件中可以看到,React开发都是一个函数一个函数地去编写。这带来了几个好处:
1. 维护起来容易。函数比较大地时候可以进行拆分,每个函数各司其职。
2. 方便自动化测试。我们只需要给函数一个输入值,看输出值是否正确,给前端自动化测试带来了很大地便捷性。