React基础
此随笔同步在我的个人博客,欢迎访问~
什么是组件
简单来说,组件就是页面上的一部分。一个页面由多个组件构成。多个组件共同渲染出一个页面。
想让一个组件往页面上渲染内容,需要在这个组件中写一个render()函数
react渲染页面的流程:
index.html --> 执行index.js --> index.js中引入组件 --> 组件通过render()函数来定义需要渲染的内容 --> 组件在index.js中被引入,使用ReactDom.render()挂载到index.html中
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<title>Orla React</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
// index.js
import React from "react";
import ReactDOM from "react-dom";
// 引入组件
import App from "./App";
import Test from "./Test";
// ReactDom.render在渲染的时候只能渲染单标签
ReactDOM.render(
// JSX语法
// 想要渲染多个标签,使用JSX语法,将多个标签放入一个div中即可
<div>
<App />
<Test />
</div>,
// 将App组件的内容挂载到index.html页面id为root的节点上
document.getElementById("root")
);
// App.js 定义App组件
import React, { Component } from "react";
// 使用class定义了一个组件
// 要往页面上渲染的内容需要写在render()函数中
class App extends Component {
render() {
return <div>Hello World</div>;
}
}
// function App() {
// return <div>hello world</div>;
// }
export default App;
// Test.js 定义Test组件
import React, { Component } from "react";
// 使用class定义了一个组件
// 要往页面上渲染的内容需要写在render()函数中
class Test extends Component {
render() {
return <div>Test组件</div>;
}
}
export default Test;
页面效果:
JSX语法
React 使用 JSX 来替代常规的 JavaScript。JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。
JSX语法中,有两种类型的标签:
- 普通的html标签(div...)
- 组件标签(比如上面的<App />),首字母一定要大写
使用React编写ToDoList
下面的代码都是基于TodoList进行编写的。使用代码需要预先创建React项目。可以使用React官方提供的脚手架工具create-react-app快速创建一个React项目(需要nodejs环境)。命令如下:
$ npx create-react-app my-app
$ cd my-app
$ npm start
$ npm install
如上命令创建了一个名为my-app的项目。使用该项目的目录结构即可。
TodoList的功能:
-
输入文字,按下回车,添加对应内容至列表中
-
点击列表项能够删除该项
实现效果如下:
页面结构
首先在页面中实现一个静态的input框和ul列表。
需要注意的是,render一次只能渲染一个外部标签, 想要渲染多个标签,同时不想用div包裹起来导致dom会新增无用的div标签,可以使用React中提供的Fragment
标签。
// index.js
import React from "react";
import ReactDom from "react-dom";
// 引入组件
import TodoList from "./TodoList";
ReactDom.render(<TodoList></TodoList>, document.getElementById("root"));
// TodoList.js, TodoList组件,组件开头字母大写
// 引入Fragment组件
import React, { Component, Fragment } from "react";
class TodoList extends Component {
render() {
// render一次只能渲染一个外部标签,想渲染多个,需要将标签放在一个div中。
// 但是这样的做法会导致dom中多出来一个div
// React16中提供了占位符组件Fragment
// 将所有组件放在Fragment中,不会在dom中添加其他的元素
return (
<Fragment>
<input />
<ul>
<li>Learn React</li>
<li>Learn Component</li>
</ul>
</Fragment>
);
}
}
export default TodoList;
React中数据驱动的设计思想和事件绑定★
React是一个数据驱动的框架,所有功能都不直接操作DOM,而是直接操作数据。
TodoList中的数据分为两部分:input中的数据和ul列表中的数据。需要在代码中把这两种数据定义出来。
在组件中定义数据,要在render()函数上面写constructor()函数。constructor中接收参数props,并且要调用父类的构造函数,将props传递给父类的构造函数。所以要固定写super函数。
// 定义组件中的数据
constructor(props) {
// 接收参数props,传递给基类(Component)的构造函数
super(props);
}
前面提到,TodoList中的数据分为两部分:input中的数据和ul列表中的数据。我们要在constructor函数中,把这两种数据定义出来。
React中,定义组建的数据要放在this.state
中,以对象的形式进行定义。
constructor(props) {
// 接收参数props,传递给基类(Component)的构造函数
super(props);
// React中,定义组件的数据要放在this.state中
// inputValue定义input输入框中的数据
// list定义ul列表中的数据
this.state = {
inputValue: "hello World",
list: [],
};
}
定义好数据后,将数据放到要渲染的标签中即可。使用花括号将其包裹起来。
render() {
return (
<Fragment>
{/* 使用花括号将数据包裹起来 */}
<input value={this.state.inputValue} />
<ul>
<li>Learn React</li>
<li>Learn Component</li>
</ul>
</Fragment>
);
}
页面显示效果如下:
但是这样做会导致一个问题。input框中的value是固定的,在页面中不能修改输入框中的内容。想要能够动态修改框中的内容,需要在input上绑定一个onChange
函数(注意和原生js中onchange不同,此处C要大写)
// 此处这样直接绑定handleInputChange会导致问题,后续会介绍
render() {
return (
<Fragment>
<input
value={this.state.inputValue}
onChange={this.handleInputChange}
/>
<ul>
<li>Learn React</li>
<li>Learn Component</li>
</ul>
</Fragment>
);
}
在组件对应的类中添加handleInputChange
函数
handleInputChange(e) {
console.log(e.target.value);
}
这里的e.target
指的是这个事件绑定的input元素,e.target.value
指的就是input输入框中的内容
到这里,看起来好像直接修改this.state中inputValue的值就可以了:
handleInputChange(e) {
this.state.inputValue = e.target.value;
}
不幸的是,输入框中的内容依然不能修改……
React中如果想要修改组件中定义的数据,需要使用setState()
方法
handleInputChange(e) {
// 数据不能直接改变
// this.state.inputValue = e.target.value;
// 要使用setState方法来改变组件中的数据
this.setState({
inputValue: e.target.value,
});
}
现在setState()
也用了,总该没问题了吧。保存!刷新页面!修改输入框中的内容!
又报错了_
呀?他说不能获取undefined的setState
……我这个setState可是绑定在this上的……那我看看这个this到底是什么东西。打印一下!
handleInputChange(e) {
console.log(this); // undefined
this.setState({
inputValue: e.target.value,
});
}
这个this居然是undefined……
看一下我们是如何绑定this的
render() {
return (
<Fragment>
<input
value={this.state.inputValue}
onChange={this.handleInputChange}
/>
<ul>
<li>Learn React</li>
<li>Learn Component</li>
</ul>
</Fragment>
);
}
onChange={this.handleInputChange}
中的this是render函数自身的this,指向组件实例。
此时this.handleInputChange
只是对于handleInputChange
的一个引用,没有加()进行调用,所以在onChange事件触发时才会调用handleInputChange()方法。
此时调用这个方法的应该是全局的window对象,this应该指向window,但是ES6中的class是严格模式,所以这时的this指向的就是undefined。
想要将this绑定到当前的组件实例上,需要使用bind来改变this的指向。于是修改代码如下:
render() {
return (
<Fragment>
{/* 使用花括号将数据包裹起来 */}
<input
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
<ul>
<li>Learn React</li>
<li>Learn Component</li>
</ul>
</Fragment>
);
}
}
onChange={this.handleInputChange.bind(this)}
使this指向了当前的组件实例,再修改当前组件中的数据就可以了。
此时我们就可以随意修改输入框中的数据了~
实现TodoList新增删除功能
处理完了输入框的输入问题,现在来解决列表新增删除的问题。
新增
之前我们在组件的state中定义了两种数据:input输入框的输入数据和<ul>列表的数据。现在默认输入框内容为空,list中存放原本写在<li>标签中的数据。页面上展示的列表应该由list中的内容来决定。
constructor(props) {
super(props);
this.state = {
inputValue: "",
list: ["Learn React", "Learn Component"],
};
}
现在我们希望根据list中的内容循环显示出<li>标签,把它们渲染到页面上。
使用ES6中的map()
方法对数组进行循环。map()
方法接收一个函数作为参数,这个函数可以接收几个值:value(数组每一项的值),index(每一项的索引值)。使用map进行循环,每一次循环都需要return出一个结果。
对于循环的每一个子项都应该有一个key值,这个key值对于每一个循环都应该是唯一的。添加key值会使react的性能更高。
render() {
return (
<Fragment>
{/* 使用花括号将数据包裹起来 */}
<input
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
<ul>
{this.state.list.map((value, index) => {
return <li key={index}>{value}</li>;
})}
</ul>
</Fragment>
);
}
}
此时list数组中的内容便能循环渲染到页面上了。
我们现在希望能够动态地添加列表项。当我们在输入框中按下回车时,将输入框中的内容添加到列表项中。为input标签添加onKeyUp事件,当按下enter键时,修改list数组中的数据即可。
handleKeyUp(e) {
// 回车键的键盘码是13,按下回车代表输入完毕,添加列表项
// 由于函数handleInputChange,this.state.inputValue的值会随着输入框中内容的改变动态改变
// 获取this.state.inputValue就是获取输入框当前内容
// 添加后自动清空输入框
if (e.keyCode === 13) {
const list = [...this.state.list, this.state.inputValue];
this.setState({
list: list,
inputValue: "",
});
}
}
完成!效果如下:
删除
现在我们希望点击某一列表项,能够删除该项。思路很简单,给li标签添加onClick事件,绑定删除的函数即可。
在删除的时候,如何能够知道要删除的是哪一项?我们之前遍历数组的时候使用了map()
,map()
方法中,对于数组每一项都有一个唯一的索引index
,根据index
值找到要删除的那一项,然后使用splice()
方法即可~
render() {
return (
<Fragment>
<input
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
onKeyUp={this.handleKeyUp.bind(this)}
/>
<ul>
{this.state.list.map((value, index) => {
return (
<li key={index} onClick={this.handleItemClick.bind(this, index)}>
{value}
</li>
);
})}
</ul>
</Fragment>
);
}
handleItemClick(index) {
const list = [...this.state.list];
list.splice(index, 1);
this.setState({
list,
});
}
更多JSX的语法细节
bind(this)的优化
在上面的代码中,每次监听事件都要调用bind(this)
,这样导致页面中到处都是bind()
。而且每次使用bind(this)
,都会重新生成一个新的函数,也就是说,每次触发事件,都会生成一个新的函数,这样会导致性能降低。
解决这个问题,可以将this的绑定放在constructor中
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.state = {
inputValue: "",
list: ["Learn React", "Learn Component"],
};
}
这样,监听事件的时候就不需要再调用bind(this)
了。
render() {
return (
<Fragment>
<input
value={this.state.inputValue}
onChange={this.handleInputChange}
onKeyUp={this.handleKeyUp}
/>
<ul>
{this.state.list.map((value, index) => {
return (
<li key={index} onClick={this.handleItemClick.bind(this, index)}>
{value}
</li>
);
})}
</ul>
</Fragment>
);
}
上面对onChange和onKeyUp进行了优化。对于onClick,因为每次需要传一个参数index进去,所以没有办法进行改写。
JSX的简化
之前我们直接在JSX中对数组进行循环,这样显得有些臃肿,我们可以把这部分提取出来,封装到一个函数中,然后直接在JSX中调用这个函数即可。
// 封装为函数
getListItems() {
return this.state.list.map((value, index) => {
return (
<li key={index} onClick={this.handleItemClick.bind(this, index)}>
{value}
</li>
);
});
}
完整TodoList代码
注意:使用代码需要提前创建React项目。
public/index.html:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<title>Todo List</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
src/index.js:
// index.js
import React from "react";
import ReactDom from "react-dom";
// 引入组件
import TodoList from "./TodoList";
ReactDom.render(<TodoList></TodoList>, document.getElementById("root"));
src/TodoList.js:
// TodoList组件,组件开头字母大写
// 引入Fragment组件
import React, { Component, Fragment } from "react";
class TodoList extends Component {
// 定义组件中的数据
constructor(props) {
// 接收参数props,传递给基类(Component)的构造函数
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
// React中,定义组件的数据要放在this.state中
// inputValue定义input输入框中的数据
// list定义ul列表中的数据
this.state = {
inputValue: "",
list: [],
};
}
handleInputChange(e) {
// 这个函数的this默认是undefined
// 想要让this指向这个组件,需要在render函数中调用此方法时使用bind(this)
// console.log(e.target.value);
// 数据不能直接改变
// this.state.inputValue = e.target.value;
// 要使用setState方法来改变组件中的数据
// console.log(this);
this.setState({
inputValue: e.target.value,
});
}
handleKeyUp(e) {
// 回车键的键盘码是13,按下回车代表输入完毕,添加列表项
// 由于函数handleInputChange,this.state.inputValue的值会随着输入框中内容的改变动态改变
// 获取this.state.inputValue就是获取输入框当前内容
// 添加后自动清空输入框
if (e.keyCode === 13 && e.target.value.trim() !== "") {
const list = [...this.state.list, this.state.inputValue.trim()];
this.setState({
list: list,
inputValue: "",
});
}
}
handleItemClick(index) {
const list = [...this.state.list];
list.splice(index, 1);
this.setState({
list,
});
}
getListItems() {
return this.state.list.map((value, index) => {
return (
<li key={index} onClick={this.handleItemClick.bind(this, index)}>
{value}
</li>
);
});
}
render() {
// render一次只能渲染一个外部标签,想渲染多个,需要将标签放在一个div中。
// 但是这样的做法会导致dom中多出来一个div
// React16中提供了占位符组件Fragment
// 将所有组件放在Fragment中,不会在dom中添加其他的元素
return (
<Fragment>
{/* 使用花括号将数据包裹起来 */}
<input
value={this.state.inputValue}
onChange={this.handleInputChange}
onKeyUp={this.handleKeyUp}
/>
<ul>{this.getListItems()}</ul>
</Fragment>
);
}
}
export default TodoList;