1. 前言
1.1 学习背景
笔者技术栈为 Vue,闲来无事,重温下胖哥的React 基础
课程,记录自己学习 react 的过程,希望能帮助到和我一样的初学者,此处附上胖哥课程的链接: React16 免费视频教程(共 28 集)
1.2 React 简介
当前全球最火的前端框架,由 Facebook 推出并进行维护,社区强大,国内的一二线互联网公司大部分都在使用 React 进行开发,还衍生了ReactNative
和React VR
这些比较好用的框架。
2. React 开发环境搭建(React 17)
2.1 Node.js 安装
进入 Node 中文网:http://nodejs.cn/ ,安装稳定版本,安装完毕后使用win+R
打开cmd
,然后输入代码
node -v
如果出现正确的版本号说明 node 安装成功,再检查 npm 的状态
npm -v
如果正确出现版本号,也说明 npm 是没问题的
2.2 React 脚手架安装
作为初学者为了减少踩坑,经验使用官方推荐的脚手架工具create-react-app
进行安装
npm install -g create-react-app
2.3 创建一个 React 项目
脚手架安装后,在D盘
创建一个ReactDemo
文件夹,然后进入文件夹,创建 React 项目
D: //进入D盘
mkdir ReactDemo //创建ReactDemo文件夹
create-react-app myreact //用脚手架创建React项目
cd myreact //等创建完成后,进入项目目录
npm start //预览项目,如果能正常打开,说明项目创建成功
3. 脚手架生成项目目录介绍
//目录表
├─public
│ favicon.ico
│ index.html
│ logo192.png
│ logo512.png
│ manifest.json
│ robots.txt
│
└─src
App.css
App.js
App.test.js
index.css
index.js
logo.svg
reportWebVitals.js
setupTests.js
│ .gitignore
│ package-lock.json
│ package.json
│ README.md
3.1 项目根目录文件
- public:公共文件,里面存放公用模板及图标。
- src:存放主要项目代码以及一些项目静态资源,封装的 api 等等。
-
gitignore:这是 git 的选择性上传配置文件,一般脚手架已经默认配置了一些不需要上传的文件,比如
node_modules
、build
等等。 -
package-lock.json:这个文件用一句话来解释,就是锁定安装时的版本号,并且需要上传到 git,以保证其他人再
npm install
时大家的依赖能保证一致。 -
package.json:这个文件是
webpack
配置和项目包管理文件,平时 npm 安装的一些包的版本就会记录在里面,其中dependencies
里面的包是项目上线后还会继续使用到的,devDependencies
里面的包则是开发环境使用到,上线了就不会使用。 -
README.md:这个文件主要是用来记录项目里的内容,主要使用
Markdown
语法来编写
3.2 public 文件夹
- favicon.ico :这个是项目在浏览器中的标签页图标,一般在浏览器标签页的左上角显示。
-
index.html:首页的模板文件,我们编写的 react 代码就是内嵌在
body
下面的rootdiv
里面。 - mainifest.json:移动端配置文件,暂时不会用到。
3.3 src 文件夹
- index.js:项目的入口文件
-
app.js:脚手架为我们配置的第一个模块,就是
npm run start
后打开网页看到的第一个页面 - serviceWorker.js:这个是用于写移动端开发的,PWA 必须用到这个文件,有了这个文件,就相当于有了离线浏览的功能。
3.4 编写代码前处理
public
文件夹只保留favicon.ico
以及index.html
文件,别的删除,index.html
里面的这行代码删除
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
src
文件夹只保留App.js
和index.js
,里面代码处理完如下
//App.js
function App() {
return <div>初始化</div>;
}
export default App;
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
然后打开页面看只看到白色背景里面文字初始化就代表我们的项目初始化完毕。
4. HelloWorld
4.1 app.js 编写
import React, { Component } from 'react';
class App extends Component {
render() {
return <div>Hellow World</div>;
}
}
export default App;
这里的步骤首先要做的是引入 React 核心库里面的component
import React, { Component } from 'react';
这里写的是 ES6 的解构赋值方法,改为 ES5 的话就是
import React from 'react';
const Component = React.Component;
4.2 jsx 语法
jsx 就是 Javascript 和 XML 结合的一种格式。React 发明了 JSX,可以方便的利用 HTML 语法来创建虚拟 DOM,当遇到<
,jsx 就当作 HTML 解析,遇到{
就当 JavaScript 解析.
//jsx语法对比普通js语法
//jsx
<ul className="my-list">
<li>JSPang.com</li>
<li>I love React</li>
</ul>;
//普通js语法
var child1 = React.createElement('li', null, 'JSPang.com');
var child2 = React.createElement('li', null, 'I love React');
var root = React.createElement('ul', { className: 'my-list' }, child1, child2);
从上面对比可以看出,在 js 里面使用 jsx 语法与平时在 html 里面写并没有什么差异,而在 js 里面用普通 js 语法写 html 结构代码繁杂,jsx 的出现大大减少了我们的代码量
jsx 语法还有一个好处,就是变量可以用{}
括起来在页面里进行渲染,比如
class App extends Component {
render() {
const num = 10;
return (
<ul className="my-list">
<li>{num}</li>
//{}内还可以使用三元运算符
<li>{false ? '小红' : '小蓝'}</li>
</ul>
);
}
}
4.3 VSCode 配置
在 js 里面使用 jsx 语法会出现无法识别并快速生成标签的问题,如果你也是使用 VSCode 的话,可以在设置里面添加上这行代码
"emmet.includeLanguages": {
//jsx代码提示
"javascript": "javascriptreact"
},
5. React 实例-小姐姐服务菜单
5.1 新建小姐姐组件
在这里还是引用胖哥的案例,在 src 目录下面,新建Xiaojiejie
文件夹,里面新建index.js
文件,然后写一个基本结构
import React, { Component } from 'react';
class Xiaojiejie extends Component {
render() {
return (
<div>
<div>
<input type="text" />
<button>增加服务</button>
</div>
<ul>
<li>头部按摩</li>
<li>精油推背</li>
</ul>
</div>
);
}
}
export default Xiaojiejie;
搭建完基本结构后,我们去 app.js 引入小姐姐组件
import React, { Component, Fragment } from 'react';
//import引入小姐姐组件,注意组件命名及使用需大写
import Xiaojiejie from './Xiaojiejie';
class App extends Component {
render() {
return (
<Fragment>
<div>Hello World</div>
{/* 使用小姐姐组件 */}
<Xiaojiejie />
</Fragment>
);
}
}
export default App;
引入小姐姐组件后,我们就去打开页面看看结构有没有展示出来,展示出来就代表这一步成功了。
5.2 组件外层包裹原则&Fragment 标签&jsx 内注释
-
组件外层包裹原则:这是一个很重要的原则,比如上面的代码,我们去掉最外层的
<Fragment>
,就会报错,因为 React 要求必须在一个组件的最外层进行包裹。//这样写会报错,最外层缺少标签包裹,return内最外层只能存在一个标签 class App extends Component { render() { return ( <div>Hello World</div> {/* 使用小姐姐组件 */} <Xiaojiejie /> ); } }
-
Fragment 标签:大多时候我们可以通过最外层加
div
来解决组件外层包裹原则问题的报错,但是有些时候,比如flex
布局,如果你外层包裹了一个 div,里面就不起作用了。这时候就需要用到Fragment
标签,它的作用是能包裹标签且不会在页面中展示出这层包裹。//在使用前需要先引入 import React, { Component, Fragment } from 'react';
jsx 内注释:普通的 js 注释只需要
//
即可,但是在 jsx 中,则需要使用{/**/}
包裹住注释,也可使用编辑器的ctrl+/
快捷注释
6. 给小姐姐服务添加功能
6.1 响应式数据绑定
因为React
使用的是虚拟 DOM,不建议直接操作 DOM 元素,所以尽量使用数据驱动的方式去做,React 会根据数据变化,去进行 DOM 更新。
首先需要给input
绑定值,并实现数据响应式,我们先定义数据
class Xiaojiejie extends Component {
//使用构造函数
constructor() {
super() //调用父类的构造函数,固定写法
this.state = {
/** 新增服务内容 */
serviceValue: '',
/** 服务列表 */
serviceList: []
}
}
...
}
定义好数据之后,把数据绑定在 input 上
//使用{}存放变量
<input value={this.state.serviceValue} type="text" />
做完这一步之后,我们可以改变constructor
里serviceValue
的值,页面中的 input 框已经正常展示出了数据,这时候就会发现一个问题:input 框怎么输入都没有发生变化。
这是因为我们强制绑定了value
的值导致的,想要改变值我们需要去绑定响应事件
,动态改变serviceValue
的值
//dom代码
<input value={this.state.serviceValue} onChange={this.serviceChange} type='text' />
//创建serviceChange事件
class Xiaojiejie extends Component {
constructor() {
...
}
render() {
return (
...
);
}
//创建serviceChange事件,与render同级
serviceChange = (e) => {
console.log(e.target.value);
this.state.inputValue=e.target.value
}
}
创建好事件并绑定了之后,我们去浏览器试着在 input 框里输入值,会发现调试台报错了,原因是 state 里的数据不能直接去改变,而是要通过setState()
方法去改变,下面是正确写法
//dom代码
<input value={this.state.serviceValue} onChange={this.serviceChange} type='text' />
//创建serviceChange事件
class Xiaojiejie extends Component {
constructor() {
...
}
render() {
return (
...
);
}
//创建serviceChange事件,与render同级
serviceChange = (e) => {
console.log(e.target.value);
this.setState({
serviceValue:e.target.value
})
}
}
现在就可以在控制台看到正确的结果了,再说说 React 创建事件及绑定的 3 种写法
6.2 创建事件及绑定的三种方式
//第一种写法,注意绑定事件时要通过bind去改变this的指向,不然会报错
<input value={this.state.serviceValue} onChange={this.serviceChange.bind(this)} type='text' />
serviceChange(e){
this.setState({
serviceValue:e.target.value
})
}
//第二种写法,我比较喜欢用的
<input value={this.state.serviceValue} onChange={this.serviceChange} type='text' />
//通过创建方法的时候使用箭头函数,this关键字将指向箭头函数定义位置下的this。
serviceChange = (e) => {
this.setState({
serviceValue:e.target.value
})
}
//第三种写法,在constructor里面就改变this指向
constructor(props){
super(props)
this.serviceChange=this.serviceChange.bind(this)
}
render() {
return (
<input value={this.state.serviceValue} onChange={this.serviceChange} type='text' />
);
}
serviceChange(){
console.log(this.props.index)
}
6.3 增加服务功能
在前面进行了数据响应式绑定之后,就可以开始实现添加服务功能了,首先我们要先将<ul>
里面的内容改写为数据渲染的结构
<ul>
{this.state.serviceList.map((item, index) => {
//在map遍历的时候,这里注意需要添加key值,key为唯一标识,缺少key值react会报错
return <li key={index}>{item}</li>;
})}
</ul>
给按钮绑定点击事件
<button onClick={this.addService}>增加服务</button>
然后创建添加服务事件
addService = (e) => {
this.setState({
serviceList: [...this.state.serviceList, this.state.serviceValue],
});
};
需要注意的是,setState()
里面不能使用数组方法push()
,因为push()
没有返回值,使用了到导致相应 key 值变为 number
//错误写法
addService = (e) => {
this.setState({
serviceList: this.state.serviceList.push(this.state.serviceValue),
});
};
//可以用concat代替
addService = (e) => {
this.setState({
serviceList: this.state.serviceList.concat(this.state.serviceValue),
});
};
6.4 删除指定服务
首先我们需要给<li>
绑定点击事件,需要在点击的时候获取到当前索引
<ul>
{this.state.serviceList.map((item, index) => {
return (
<li key={index} onClick={this.deleteService.bind(this, index)}>
{item}
</li>
);
})}
</ul>
上面需要注意的地方是事件传参的时候,需要写成Fn.bind(this, xx)
的方式,如果直接写成Fn(xx)
就会变成直接调用了
然后我们创建这个删除事件
//删除服务
deleteService = (index) => {
//因为react禁止直接操作state,所以我们新建变量来进行操作
let list = this.state.serviceList;
list.splice(index, 1);
this.setState({
serviceList: list,
});
};
到这里这个小项目就算完成了。
7. 组件拆分
虽然上面小项目完成了,但是在实际工作中,我们肯定是要进行组件拆分的,这样有利于我们后期的维护。
首先列出完整代码,标注需要拆分的地方
import React, { Component } from 'react';
class Xiaojiejie extends Component {
constructor() {
super(); //调用父类的构造函数,固定写法
this.state = {
/** 新增服务内容 */
serviceValue: '',
/** 服务列表 */
serviceList: ['头部按摩', '精油推背'],
};
}
render() {
return (
<div>
<div>
<input
value={this.state.serviceValue}
onChange={this.serviceChange}
type="text"
/>
<button onClick={this.addService}>增加服务</button>
</div>
{/* 拆分 */}
<ul>
{this.state.serviceList.map((item, index) => {
return (
<li key={index} onClick={this.deleteService.bind(this, index)}>
{item}
</li>
);
})}
</ul>
</div>
);
}
//响应式数据绑定
serviceChange = (e) => {
this.setState({
serviceValue: e.target.value,
});
};
//增加服务
addService = () => {
this.setState({
serviceList: this.state.serviceList.concat(this.state.serviceValue),
});
};
//删除服务
deleteService = (index) => {
let list = this.state.serviceList;
list.splice(index, 1);
this.setState({
serviceList: list,
});
};
}
export default Xiaojiejie;
7.1 新建服务列表组件
在Xiaojiejie
文件夹下面,新建一个XiaojiejieItem.js
,然后把基础结构写好
import React, { Component } from 'react';
class XiaojiejieItem extends Component {
render() {
return <li>服务列表模块</li>;
}
}
export default XiaojiejieItem;
7.2 父子组件的传值
我们的需求是功能模块化,把服务列表组件抽离出来后,我们需要引入子组件,然后给子组件传递服务列表参数,使子组件展示出来
父传子
父组件写法
//先引入子组件
import XiaojiejieItem from './XiaojiejieItem';
//使用子组件,将需要传递的参数传递给子组件,与vue类似:自定义属性名 = 值 格式,子组件取代之前的li标签,同样需要设置key值
<ul>
{this.state.serviceList.map((item, index) => {
return <XiaojiejieItem key={index} content={item} />;
})}
</ul>;
子组件写法
//通过this.props.xx的形式去接收
class XiaojiejieItem extends Component {
render() {
return <li>{this.props.content}</li>;
}
}
这时候页面就能正常展示出来了,但是我们发现还缺少一个删除服务
的功能,这个功能需要子组件传递索引给父组件,在调用父组件的方法去删除
子传父
父组件写法
//把索引跟删除服务的方法传递给子组件
<ul>
{this.state.serviceList.map((item, index) => {
return (
<XiaojiejieItem
key={index}
content={item}
index={index}
deleteService={this.deleteService}
/>
);
})}
</ul>
子组件写法
//给li绑定点击事件
class XiaojiejieItem extends Component {
render() {
return <li onClick={this.handleServiceList}>{this.props.content}</li>;
}
//传递索引调用父组件方法
handleServiceList = () => {
this.props.deleteService(this.props.index);
};
}
8. PropTypes 的简单应用
8.1 作用
验证传入数据的有效性,当向 props 传入无效数据时,JavaScript 控制台会抛出警告。为了性能考虑,只在开发环境验证 propTypes
。
8.2 简单使用
在XiaojiejieItem.js
里面先引入
import PropTypes from 'prop-types';
然后去设置不同参数的类型,注意是写在 class 外面
//这里的.propTypes是小写开头
XiaojiejieItem.propTypes = {
content: PropTypes.string,
deleteService: PropTypes.func,
index: PropTypes.number,
//设置非必传参数,添加关键词isRequired,这样设置就算不传这个值也不会报错
name: PropTypes.string.isRequired,
};
设置完参数的类型后,如果父组件传递进来的参数不符合我们定义的类型,就会在控制台报错
9.React 生命周期
9.1 四个大阶段
React 生命周期可分为四个大阶段:
-
Initialization
:初始化阶段 -
Mounting
:挂载阶段 -
Updation
:组件更新阶段 -
Unmounting
:销毁阶段
9.2 各生命周期及其用法
1. 挂载卸载过程
-
componentWillMount()
:在组件渲染前调用,一般用的比较少,它更多的是在服务端渲染时使用。它代表的过程是组件已经经历了 constructor()初始化数据后,但是还未渲染 DOM 时,React 17版本即将遗弃。 -
componentDidMount()
:组件第一次渲染完成,此时 dom 节点已经生成,可以在这里调用 ajax 请求,返回数据 setState 后组件会重新渲染。 -
componentWillUnmount()
:在组件从 DOM 中移除之前立刻被调用,一般用来销毁setTimeout, setInterval
以及removeEventListener
。
2. 更新过程
-
getDerivedStateFromProps(nextProps, prevState)
:会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回null
则不更新任何内容。 -
shouldComponentUpdate(nextProps,nextState)
:通过返回布尔值控制该组件是否更新,主要用于性能优化
,因为react父组件的重新渲染会导致所有子组件都跟着重新渲染
,因此需要在子组件的该生命周期中做判断。 -
getSnapshotBeforeUpdate(prevProps, prevState)
:在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息。此生命周期的任何返回值将作为参数传递给componentDidUpdate()
。 -
componentDidUpdate(prevProps,prevState)
:组件更新完毕后,react 只会在第一次初始化成功会进入 componentDidmount,之后每次重新渲染后都会进入这个生命周期。 -
render()
:render 函数会插入 jsx 生成的 dom 结构,react 会生成一份虚拟 dom 树,在每一次组件更新时,在此 react 会通过其 diff 算法比较更新前后的新旧 DOM 树,比较以后,找到最小的有差异的 DOM 节点,并重新渲染。 -
getDerivedStateFromError()
:此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state