前面的话
这是一篇算React入门级的文章,初次接触React,有问题请指出~~~。
安装方式
这里介绍搭建一个 react 项目的两种方式: CDN链接 和 官方推荐的 create-react-app 脚手架
CDN 链接
创建一个 index.html 文件,将 React、React DOM、Babel 这三个 CDN 链接引入到 head 标签中。并创建一个 div ,id为 root,作为根元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello React</title>
<!-- React --- React顶级API -->
<script src="https://unpkg.com/react@^16/umd/react.production.min.js"></script>
<!-- React DOM --- 添加特定与DOM的方法 -->
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<!-- Babel ---js编译器,可使用es6+ -->
<script src="https://unpkg.com/[email protected]/babel.js"></script>
</head>
<body>
<div id="root"></div>
<!-- text/babel 脚本类型是babel所必须的 -->
<script type="text/babel">
// react code
</script>
</body>
</html>
引入这三个 CDN 链接的目的:
- React – 顶级API
- React DOM —添加特定DOM的方法
- Babel — 种JavaScript编译器, 可使用ES6+
如果需要安装指定的版本,将@后面的版本号改为指定版本。
⚠️注意:script 脚本的类型 text/babel
是必须的。
我们都知道,不管是 Vue 还是 React 都是组件化,在 React 中有两种创建组件的方法,Class
组件和 Function
组件, 这里我们创建一个 Class 组件。
创建一个 Class 组件很简单,只要创建一个 Class
类,并让他继承 React.Component
, 在render()
方法中 return
一个 JSX
模版,用于呈现 DOM节点。
⚠️注意:render() 方法是类组件唯一需要的方法。
创建组件App :
// 创建组件App
class App extends React.Component {
// render方法必须,用于呈现DOM节点
render() {
return (
// jsx
<h1>hello word</h1>
)
}
}
在 return
中, 可以看到返回很像 HTML 的内容,但其不是 HTML ,而是 JSX ,这里先不过多介绍。
最后,我们使用 ReactDOM.render()
方法将 App组件渲染到 root
根元素 div 中:
ReactDOM.render(<App/>, document.getElementById('root'));
index.html 的完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello React</title>
<!-- React --- React顶级API -->
<script src="https://unpkg.com/react@^16/umd/react.production.min.js"></script>
<!-- React DOM --- 添加特定与DOM的方法 -->
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<!-- Babel ---js编译器,可使用es6+ -->
<script src="https://unpkg.com/[email protected]/babel.js"></script>
</head>
<body>
<div id="root"></div>
<!-- text/babel 脚本类型是babel所必须的 -->
<script type="text/babel">
// 创建组件App
class App extends React.Component {
// render方法必须,用于呈现DOM节点
render() {
return (
// jsx
<h1>hello word</h1>
)
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
</script>
</body>
</html>
在浏览器中打开 index.html 文件:
这样一个简单的 React 应用就搭建好了。
create-react-app
第一种搭建 react 项目的方法只适合书写简单的react 代码,使用 create-react-app
脚手架可以帮我们初始化一个项目 demo。
npx create-react-app react-start
项目结构:public/index.html 是页面模板,src/index.js 则是入口JS文件
安装完成后,进入项目文件并启动:
cd react-start
npm start
运行后,会弹出新窗口:
下面修改一下代码,修改App.js 文件为上面创建 App 组件的代码:
import React, { Component } from 'react';
// import './App.css';
class App extends Component {
render() {
return <h1>Hello, React</h1>;
}
}
export default App;
什么是JSX?
上面的代码中已经接触了 JSX,看起来很像 HTML ,但是并不是。它是JSX,代表 JavaScript XML 。
接着上面的代码,修改App.js :
const name = 'React';
const element = (
<div tabIndex="0" className="box">
<h1>Hello, {name}</h1>
<div className="name">
<span>xiaoqi</span>
</div>
</div>
);
class App extends Component {
render() {
return element;
}
}
element 就是一个 JSX,可以看到它的特点:
- 可以使用 { } 内置 js 表达式
- JSX 标签可以包含很多子标签
- JSX 中可以指定特定的属性(采用大驼峰),比如上面的
tabIndex
、className
看一下渲染之后的效果:
元素渲染
元素是构成 React 应用的最小砖块。
创建一个元素 element :
const element = (
<div className="box">
<h1>React 入门</h1>
<span> 元素是构成 React 应用的最小砖块。</span>
</div>
);
将这个元素渲染到 DOM 根结点中,需要使用 ReactDOM.render()
方法:
ReactDOM.render(element, document.getElementById('root'));
因 index.js 文件是项目的 js 入口文件,所以我们修改 index.js 文件为上面的代码:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// import App from './App';
// import * as serviceWorker from './serviceWorker';
const element = (
<div className="box">
<h1>React 入门</h1>
<span> 元素是构成 React 应用的最小砖块。</span>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
运行结果:
组件
组件分为两种: Class 组件和 Function 组件。
大多数的 React 应用都由很多小组件组成,将所有的小组件都添加到App主组件中,最后将 App主组件渲染到入口 js 文件 index.js 中。
我们将修改 App.js
和 index.js
文件:
App.js:
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div className="app">
<h1> Hello, React</h1>
</div>
);
}
}
export default App;
index.js:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './index.css'
ReactDOM.render(<App />, document.getElementById('root'))
Class 组件
前面提过,创建一个 Class 组件,只需要创建一个 class 类,并且让它继承React.Component
, 在 render()
方法中 return
出 JSX
模版。
接下来,我们在 src 文件夹中 ,创建一个 Table.js文件:
// Table组件
import React, { Component } from 'react';
class Table extends Component {
render() {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Job</th>
</tr>
</thead>
<tbody>
<tr>
<td>Charlie</td>
<td>Janitor</td>
</tr>
<tr>
<td>Mac</td>
<td>Bouncer</td>
</tr>
<tr>
<td>Dee</td>
<td>Aspiring actress</td>
</tr>
</table>
);
}
}
export default Table;
在 App.js中添加 Table组件:
class App extends Component {
render() {
return (
<div className="app">
<h1> Hello, React</h1>
<Table />
</div>
);
}
}
运行结果:
Function 组件
上面的 Table 组件, 我们可以拆分为两个子组件:TableBody
组件和TableHeader
组件, 这两个组件我们使用 Function 组件来创建:
修改 Table.js : TableHeader
和 TableBody
组件都在Table.js文件中,并被 Table
组件使用。
// TableHeader组件
const TableHeader = () => {
return (
<thead>
<tr>
<th>Name</th>
<th>Job</th>
</tr>
</thead>
);
};
// TableBody 组件
const TableBody = () => {
return (
<tbody>
<tr>
<td>Charlie</td>
<td>Janitor</td>
</tr>
<tr>
<td>Mac</td>
<td>Bouncer</td>
</tr>
<tr>
<td>Dee</td>
<td>Aspiring actress</td>
</tr>
</tbody>
);
};
class Table extends Component {
render() {
return (
<table>
<TableHeader />
<TableBody />
</table>
);
}
}
小结
比较一下 Function
组件与Class
组件:
Function 组件:
const functionComponent = () => {
return <div> Example</div>
}
Class 组件:
class ClassComponent entends React.Component {
render() {
return <div> Example</div>
}
}
组件通信: Props
Vue中的父传子使用的是 props,React 中的父传子也是props,下面看一下在React中props是如何使用的。
如果表格的数据很多,那么上面的TableBody
组件会显的很臃肿。我们将< tbody>中要输出的数据提取出来,通过props的方式来进行传递。
修改App.js文件:
-
在App组件中声明数据
characters
, 表示< tbody>中要输出的数据。 -
在子组件
Table
上添加属性名称和数据class App extends Component { render() { // 提取数据 const characters = [ { name: 'Charlie', job: 'Janitor', }, { name: 'Mac', job: 'Bouncer', }, { name: 'Dee', job: 'Aspring actress', }, ]; return ( <div className="app"> <h1> Hello, React</h1> <Table characterData={characters} /> </div> ); } }
修改 Table.js :
-
子组件中的使用:在 class 组件中通过
this.props.属性名
来获取父组件中传递的数据:class Table extends Component { render() { const { characterData } = this.props; return ( <table> <TableHeader /> <TableBody characterData={characterData} /> </table> ); } }
这里的
const { characterData } = this.props
相当于const characterData = this.props.characterData
,这里只是es6的写法。 -
继续将数据传递给
Table
组件的子组件TableBody
:TableBody
是一个 function组件,其使用 props 的方法直接是:将props作为参数名传递给function组件,使用props.属性名
即可获取父组件的数据。// TableBody 组件 const TableBody = (props) => { const rows = props.characterData.map((item, index) => { return ( <tr key={index}> <td>{item.name}</td> <td>{item.job}</td> </tr> ); }); return <tbody>{rows}</tbody>; };
上面的例子中,给每个表行都添加了一个
key
。React 中创建列表时,使用都要使用key
,来识别列表的每一项项,这是必要的。
⚠️注意:props
具有只读性,无论是函数声明还是class声明的组件,都不能修改自身的props
State状态
props 是单向传递,并且是只读的。如果想改变数据的状态,可以使用state,与props 不同的是state可变,但是 state 是class组件私有的对象,修改state 不能直接修改,要使用this.setState()
方法来改变。
现在我们实现一个功能:在表的每行添加一个删除按钮,点击之后,可删除该行信息。
修改 App.js
- 创建一个
state
对象,包含存储的数据 characters - 由于要删除数据,即要改变 state 对象,我们添加一个
removeCharacter()
方法,在该方法内实现 state 数据的变化,通过数组的索引过滤,然后返回新的数组。 - 最后将该函数传递给子组件
Table
class App extends Component {
state = {
characters: [
{
name: 'Charlie',
job: 'Janitor',
},
{
name: 'Mac',
job: 'Bouncer',
},
{
name: 'Dee',
job: 'Aspring actress',
},
],
};
// 箭头函数解决 class中this不绑定的问题
removeCharacter = (i) => {
this.setState({
characters: this.state.characters.filter((item, index) => {
return i !== index;
}),
});
};
render() {
// 提取数据
return (
<div className="app">
<h1> Hello, React</h1>
<Table
characterData={this.state.characters}
removeCharacter={this.removeCharacter}
/>
</div>
);
}
}
将函数removeCharactor
传递给Table
组件之后,还需要传递给TableBody
组件,并且还要在 TableBody
组件中给每行添加一个按钮,点击之后执行此函数。
修改 Table.js:
// Table组件
import React, { Component } from 'react';
// TableHeader组件
const TableHeader = () => {
return (
<thead>
<tr>
<th>Name</th>
<th>Job</th>
</tr>
</thead>
);
};
// TableBody 组件
const TableBody = (props) => {
const rows = props.characterData.map((item, index) => {
return (
<tr key={index}>
<td>{item.name}</td>
<td>{item.job}</td>
<td>
<button onClick={() => props.removeCharacter(index)}>Dalete</button>
</td>
</tr>
);
});
return <tbody>{rows}</tbody>;
};
class Table extends Component {
render() {
const { characterData, removeCharacter } = this.props;
return (
<table>
<TableHeader />
<TableBody
characterData={characterData}
removeCharacter={removeCharacter}
/>
</table>
);
}
}
该onClick函数必须通过返回该removeCharacter()方法的函数,否则它将尝试自动运行。
运行结果:
⚠️注意: State 的更新可能是异步的, React 可能会把多个setState() 调用合并成一个调用。
props 与 state的区别
- state 是管理数据,控制状态,可变(通过this.setState() )
- props 是外部传入的数据参数,不可变
- 没有 state 的叫无状态组件,有 state 的叫有状态组件
- 多用 props,少用 state。
事件处理
React 中事件处理和 DOM 元素的很相似,但是在语法上有些不同:
- React 事件的命名采用小驼峰(camelCase)
- 使用 JSX 语法时需要传入一个函数作为事件处理函数
例子:
<button onClick={activateLasers}>
Activate Lasers
</button>
前面,我们已经实现了将数据存储在state状态中,并且可以从状态中删除任何数据。但如果我们想添加新的数据怎么办呢?
接下来,通过下面的表单例子,来体验 React 中的事件处理。我们添加一个 Form.js 组件, 每次更改表单中字段时会更新状态,并且在我们提交时,所有数据会传递给 App组件的 state,进而更新整个 Table。
创建新文件 Form.js :
-
我们将表单的初始状态设置为带有空属性的对像,并且将初始状态赋给
this.state
:import React, { Component } from 'react' class Form extends Component { initialState = { name: '', job: '', } state = this.initialState }
-
表单内容大致如下:
render() { const { name, job } = this.state; // 初始化时为空 return ( <form> <label htmlFor="name">name</label> <input type="text" name="name" id="name" value={name} onChange={this.handleChange} /> <label htmlFor="job">job</label> <input type="text" name="job" id="job" value={job} onChange={this.handleChange} /> </form> ); }
-
每次输入时都更改
state
里面的数据,所以在onChange
事件发生时,将handleChangr()
方法作为回调传入handleChange = (event) => { const { name, value } = event.target; this.setState({ [name]: value, }); };
⚠️注意:
在class 组件中,class 的方法默认不会绑定 this
,所以要谨慎 JSX 回调函数中的 this 。例如上面的例子中,如果忘记绑定 this
,直接将this.handleChange
传入 onChange
,这时调用这个函数时, this
将会是是undefined
。
绑定this的方法
-
第一种:class 的方法使用箭头函数, 如上例。
handleChange = (event) => { const { name, value } = event.target; this.setState({ [name]: value, }); };
-
第二种: 在构造函数中使用
this.handleChange.bind(this)
进行绑定。constructor() { super(); this.handleChange = this.handleChange.bind(this); } handleChange(event) { const { name, value } = event.target; this.setState({ [name]: value, }); }
-
第三种:其实也相当于第二种,只不过不使用构造函数
handleChange(event) { const { name, value } = event.target; this.setState({ [name]: value, }); // 直接在传入时进行绑定 onChange={this.handleChange.bind(this)}
前面实现了表单的数据更新,最后一步是提交表单的数据并更新App组件的 state
状态。
修改App.js:
创建一个名为handleSubmit
的函数,用来更新 state
的数据(添加表单提交的数据):
handleSubmit = (character) => {
// 添加数据
this.setState({
characters: [...this.state.characters, character],
});
};
将其传给 Form 组件:
render() {
// 提取数据
return (
<div className="app">
<h1> Hello, React</h1>
<Table
characterData={this.state.characters}
removeCharacter={this.removeCharacter}
/>
<Form handleSubmit={this.handleSubmit} />
</div>
);
}
接下来,在 Form.js 中创建名为 submitForm()
,该方法将调用 父组件传递的方法 handleSubmit
。将Form组件的 state
作为参数传入:
修改 Form,js:
// 提交表单
submitForm = () => {
// 添加数据
this.props.handleSubmit(this.state);
// 添加之后清空state
this.setState(this.initialState);
};
最后,我们为表单添加一个提交按钮用来提交表单:
<input type="button" value="submit" onClick={this.submitForm} />
⚠️注意:这里的submitFrom
方法也得绑定this
。
运行项目,具体的功能就实现了:
条件渲染
React 中的条件渲染和javaScript 中的一样,可以使用 运算符 if 或者条件运算。
下面实现一个登录/登出按钮,根据不同的状态现实不同的效果。为了方便,还是在上面的项目实现这效果。
添加一个Login.js文件:
-
创建两个组件:
LoginButton
和LogoutButton
分别代表登录和注销。// 登录 const LoginButton = (props) => { return <button onClick={props.click}>Login</button>; }; // 注销 const LogoutButton = (props) => { return <button onClick={props.click}>Logout</button>; };
-
创建一个有状态的组件
LoginControl
,用来根据当前的状态来渲染上面的两个组件。
class LoginControl extends Component {
// 初始状态
state = {
isLoggedIn: false,
};
handleLoginClick = () => {
this.setState({
isLoggedIn: true,
});
};
handleLogoutClick = () => {
this.setState({
isLoggedIn: false,
});
};
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
// 通过if 语句来进行条件渲染
if (isLoggedIn) {
button = <LoginButton click={this.handleLogoutClick} />;
} else {
button = <LogoutButton click={this.handleLoginClick} />;
}
return (
<div>
{button}
</div>
);
}
}
- 为了更好的看到效果,我们每次切换后,加上对应的描述。创建两个组件,代表切换后对应的描述:
// 登录
const LoginGreeting = () => {
return <span>Welcome </span>;
};
// 注销
const LogoutGreeting = () => {
return <span>Please sign up</span>;
};
- 创建一个组件
Geeting
,来渲染上面的描述组件.这里使用三目运算符来实现
const Greeting = (props) => {
const { isLoggedIn } = props;
return <div>{isLoggedIn ? <LoginGreeting /> : <LogoutGreeting />}</div>;
};
- 最后,在
LoginControl
组件中加入这个Greeting
组件,同时在App 组件中加入LoginControl
组件。
// ...
return (
<div>
{button}
<Greeting isLoggedIn={isLoggedIn} />
</div>
// ...
在App
加入LoginControl
组件:
// ...
render() {
// 提取数据
return (
<div className="app">
<h1> Hello, React</h1>
<Table
characterData={this.state.characters}
removeCharacter={this.removeCharacter}
/>
<Form handleSubmit={this.handleSubmit} />
<LoginControl />
</div>
);
}
// ...
运行项目(样式有点丑~):
列表 & key
前面的例子 TableBody
组件中 ,已经接触了列表:
const TableBody = (props) => {
const rows = props.characterData.map((item, index) => {
return (
<tr key={index}>
<td>{item.name}</td>
<td>{item.job}</td>
<td>
<button onClick={() => props.removeCharacter(index)}>Dalete</button>
</td>
</tr>
);
});
return <tbody>{rows}</tbody>;
};
这个组件中,rows
为 tr
的数组,最后将rows
插入到 < tbody>
元素中。
这里可以看到每一个 tr
都有一个特殊的属性key
,这个key
是必要的(这与diff算法有关)。
key
帮助 React
识别哪些元素改变了,比如删除或者添加。所以应当给数组中的每一个元素给予一个确定的标识。一个元素的key
最好是这个元素在列表中拥有的独一无二的字符串。通常使用数据的id
来作为元素的 key
。如果元素没有确定的 id
时,可以使用元素索引 index
作为 key
。
⚠️注意:
- 元素的
key
只有放在就近的数组上下文中才有意义。如上例中的map 方法中的元素需要设置 key 属性 key
值只是在兄弟节点间独一无二,而不是全局的唯一。(也就是说,生成两个不同的数组时,可以使用相同的key
)
生命周期
挂载
当组件实例被创建并插入 DOM中,其生命周期调用顺序如下:
- constructor: 构造函数初始化,最先被执行,初始化
state
等 - getDerivedStateFromProps :这是一个静态方法,需要在前面添加
static
属性 - render: 渲染函数,返回渲染的内容,当页面更新时也会触发
- componentDidMount: 组件挂载之后,这个时候组件已经挂载完毕了
更新
- getDerivedStateFromProps : 组件即将被更新,这里参数分别对应前后被修改的内容,通过返回一个布尔值告知是否要更新视图。
- render: 当视图更新,那么render 也会更新
- getSnapshotBeforeUpdate:
getSnapshotBeforeUpdate
在render
之后componentDidUpdate
之前输出,类似于中间件用来做一些捕获操作 - componentDidUpdate:
getSnapshotBeforeUpdate
,有三个参数prevProps
,prevState
,snapshot
,表示之前的props
,之前的state
,和snapshot
。snapshot
是getSnapshotBeforeUpdate
返回的值
卸载
- componentWillUnmount: 组件卸载,可以清除一些定时器,取消网络请求。
修改 App.js 代码:
class App extends Component {
constructor() {
super();
console.log('1: constructor初始化');
}
state = {
characters: [
{
name: 'Charlie',
job: 'Janitor',
},
{
name: 'Mac',
job: 'Bouncer',
},
{
name: 'Dee',
job: 'Aspring actress',
},
],
};
// 移除项目
removeCharacter = (i) => {
this.setState({
characters: this.state.characters.filter((item, index) => {
return i !== index;
}),
});
};
handleSubmit = (character) => {
// 添加数据
this.setState({
characters: [...this.state.characters, character],
});
};
static getDerivedStateFromProps(nextProps, prevState) {
console.log('getDerivedStateFromProps: 组件将要被更新');
console.log(prevState);
return true;
}
componentDidMount() {
console.log('componentDidMount 组件挂载完毕');
}
getSnapshotBeforeUpdate(preProps, prevState) {
console.log(preProps, prevState);
return 'top: 200';
}
componentDidUpdate(preProps, prevState, snapshot) {
console.log('组件更新完毕');
console.log(preProps, prevState, snapshot);
}
render() {
// 提取数据
return (
<div className="app">
<h1> Hello, React</h1>
<Table
characterData={this.state.characters}
removeCharacter={this.removeCharacter}
/>
<Form handleSubmit={this.handleSubmit} />
<LoginControl />
</div>
);
}
}
运行结果:
总结
学习React很多都是以Vue的思路先入个门,很多东西都是类似的。第一篇入门文章就到这里,后面继续~~。