React快速暴力入门
React 作为如今三大框架之一,在进行了短期的学习后,整理了一些笔记,做一下分享: (新人一个,多多包涵)
一、React简介
1. 什么是React:
2. React的特点
- 声明式设计 −React的每个组件都是通过声明创建,使得页面逻辑更加清晰
2. 虚拟DOM −React每次渲染页面时会创建一个虚拟DOM,与现有的DOM进行对比,有差异的才进行替换重新渲染,提高了效率。 3. JSX − JSX 是 JavaScript 语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。
// 在 javascript中创建元素
const DOM = document.createElement("h1"); // 真实DOM
DOM.innerText = "这是h1标签";
// 在 jsx中创建元素
const VDOM = <h1>这是h1标签</h1> // 虚拟DOM
- 组件 − 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
// 不必现在就看懂,仅需要知道每个组件都是需要进行声明后才能使用
import React, {PureCompoent} from "react";
export default class Header extends PureCompoent{
render(){
return <header>这是头部组件</header>
}
}
// ------------------------------------------------
// 在需要使用 header组件时调用即可重复使用
import Header from "./Header";
<Header></Header>
- 单向响应的数据流 − React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。(这个在后面会了解到)
3. 安装与使用
- 使用cdn
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
- 通过下载导入 React包
<script src="../js/react.development.js"></script>
<script src="../js/react-dom.development.js"></script>
<script src="../js/babel.min.js"></script>
- 使用 react的脚手架
create-react-app
创建项目文件会自带react
npm i create-react-app -g
create-react-app 项目名称
4. 虚拟DOM
(1) 关于虚拟DOM
虚拟DOM的本质就是一个对象,真实DOM也是个对象,但虚拟DOM的属性更少,更加轻量。
// 虚拟DOM
const VDOM = <h1>Hello World</h1>; // 此处不需要引号,因为不是字符串
// 真实DOM
const TDOM = document.querySelector("#app");
console.log("虚拟DOM: ", VDOM); // Object
console.log("真实DOM: ", TDOM); // <div id="app"></div>
console.log("虚拟DOM类型: ", typeof VDOM); // object
console.log("真实DOM类型: ", typeof TDOM); // object
console.log(VDOM instanceof Object); // true
console.log(TDOM instanceof Object); // true
(2) 创建虚拟DOM
- 通过
React
的方法createElement()
方法创建虚拟DOM
// React.createElement(标签名称, 标签属性, 标签内容)
const VDOM1 = React.createElement("h1", {id: "title"}, "This is Title");
- 使用方法一的语法糖创建虚拟DOM
const VDOM2 = <h1 id="title">This is Title</h1>;
5. 关于JSX
jsx
语法与javascript
语法非常相似,只有一些需要注意的地方
// 虚拟DOM
const VDOM = ( // 使用括号框住 jsx标签表示层级会更加美观些。
<h1>
<span>Hello World</span>
</h1>
)
const myid = "HeLlO";
const content = "Happy New Year"
const students = [
{id: "001", name: "Tom", age: 18},
{id: "002", name: "Tim", age: 19},
{id: "003", name: "Jerry", age: 20},
];
const VDOM2 = (
<div>
<h2 className="title" id={myid.toLowerCase()}>
<span style={{color: 'pink'}}>{content}</span>
</h2>
<ul>{/* 使用 ES6的 map() 函数进行列表渲染(将数据批量渲染到页面上) */}
{
students.map(student=>{
return <li key={student.id}>{student.name}---{student.age}</li>
}
}
</ul>
<input type="text"/>
</div>
)
jsx语法规则:
- 定义虚拟DOM时不要写引号
- 标签中混入 js表达式时要用 {}
- 表达式会产生一个值,可以放在任何需要值的地方
- 语句时一串代码,用于处理逻辑用的
- 标签中类名指定不用 class,要用 className
- 标签中使用内联样式时,要用双括号写法(使用小驼峰写法写css样式)
- 添加事件属性时(如
onclick
),on后面的单词首字母要大写(如onClick
) - 虚拟DOM必须只有一个根标签
- 标签必须闭合
- 标签开头
- 标签开头为小写时,会被 jsx编译为 html标签,若 html没有对应同名元素则报错
- 标签开头为大写时,会被 jsx识别为组件,若没找到对应组件则
6. 渲染到页面上
使用 ReactDOM
的 render()
方法进行渲染
const VDOM = <h1 id="title">This is a Title</h1>; // 创建虚拟DOM
// ReactDOM.render( 组件(虚拟DOM), 要绑定到哪个元素上 );
ReactDOM.render(VDOM, document.querySelector("#root"));
二、组件的使用
在 React中,有两种组件的创建方式,分别为函数式组件和类式组件。其中 类式组件使用频率较高(React 16.8出现 Hook
后函数式组件也多了起来)
函数式组件
顾名思义,该组件是由函数来写的
function Demo(props){ // 定义一个组件,名为 Demo
return <h2>This is a component</h2>; // 返回值为组件的DOM内容
}
ReactDOM.render(<Demo/>, document.querySelector("#root"))
函数式组件定义:
- 函数名称必须大写
- 调用时以标签方式调用且开头大写
- 函数式组件的参数为
props
(后面会讲)
类式组件
该组件通过类来构建,但需要继承React自带的一个类Component
。
// 使用 ES6写法创建类式组件,并继承于 React.Component
class Demo extends React.Component{
// 添加render() 函数(必须),返回值为组件的虚拟DOM
render(){
console.log(this); // render() 函数的 this 指向组件的实例对象
return <h1>This is a Title!</h1>
}
}
ReactDOM.render(<Demo/>, document.querySelector("#root"));
类式组件的定义:
- 必须继承 React的内置类
Component
- 必须包含方法
render()
- 构造函数
constructor()
的参数为props
(后面会讲),如果需要使用constructor
则必须调用父类的构造函数super(props)
类式组件挂载时的执行情况:
- React解析组件标签,发现了
Demo
组件 - 发现为类式组件,随后
new
出该类的实例对象,通过实例调用原型对象上的render
方法 - 将
render
方法返回的虚拟DOM转为真实DOM,随后呈现到页面中
组件定义的注意事项:
- 类式组件的
render()
返回的组件标签与函数式组件返回的组件标签一定要有一个根标签 - 都必须以大写字母开头
组件的挂载与卸载
挂载已经看了很多个了,直接上代码:
// ReactDOM.render( 组件, 要绑定在哪个元素上 );
ReactDOM.render( <Demo/>, document.querySelector("#app") );
卸载的代码长点,我也直接放上来了:
// ReactDOM.unmountComponentAtNode( 要卸载哪个元素上的组件 );
ReactDOM.unmountComponentAtNode( document.querySelector("#app") );
其他小知识
- 包含表单元素的组件分为非受控租价与受控组件
- 受控组件:表单组件的输入组件随着输入并将内容存储到状态中(随时更新)
- 非受控组件:表单组件的输入组件的内容在有需求的时候才存储到状态中(即用即取)
三、组件的三大属性
组件的实质就是个对象,而对象自然有属性,在组件里最常用的三个属性分别是 state
、props
和 refs
1. state
state
即组件的状态,说的明白点就是该组件所存储的(所需要使用的)数据
类式组件中的使用:
class Weather extends React.Component{
constructor(props){
super(props);
// this.state = {weather: "Spring"} // 也可以在构造函数中定义 state
}
state = { // 定义 state
weather: "summer",
}
render(){
// 当前季节为:summer
return <h2>当前季节为:{ this.state.weather }</h2>
}
}
使用的时候通过 this.state
调用 state
里的值
类式组件定义 state
:
- 可以在构造函数中初始化
state
- 可以在类中添加属性
state
来初始化
函数式组件中的使用
- 在 React16.8 前,函数式组件并不能有自己的
state
(因为会重复初始化数据) - 到 React16.8 后,出现了
Hook
方法,使得函数式组件也可以使用state
特性,先了解即可,后面会教学。
function Demo(){
const [weather, setWeather] = React.useState("Winter");
return <h2>当前季节为:{weather}</h2> // 当前季节为:Winter
}
修改 state
在类式组件的函数中,你会发现直接修改 state
的值,如:
this.state.count = 1
如果你在页面中使用了 count
这个值,你会发现,这页面咋没变呢?
梳理一下页面渲染靠的是哪个函数呢?不是靠的 render()
函数吗?
如果你每次直接修改 state
后在调用 render()
函数的话,不会显得太麻烦了吗?
其实React也不建议 state
不允许直接修改,而是通过特定的渠道来修改,便是使用在类的原型对象上的方法 setState()
setState
this.setState(partialState, [callback]);
partialState
: 需要更新的状态的部分对象callback
: 更新完状态后的回调函数
setState
有两种写法:
写法1:
this.setState({
count: 1,
})
写法2:
// 传入一个函数,返回x需要修改成的对象,参数为当前的 state
this.setState(state => ({count: state.count+1});
使用那种写法,取决于修改的状态是否需要动用到当前的状态
forceUpdate
还有一种修改状态的方法,就是 forceUpdate
,意思是强制更新,即强制更新状态。
参数为更新状态完成后的回调函数
this.forceUpdate([callback]);
setState
更新与forceUpdate
更新都是一种合并操作,而不是替换操作
- 在执行
setState
操作后,React会自动帮我们调用一次render()
render()
的执行次数便是 1+n (1 为初始化时的自动调用,n 为状态更新的次数(即调用setState
或forceUpdate
的次数))- 尽量少用或不用
forceUpdate
2. props
与 state
不同,state
是组件自身的状态(数据),而 props
则是外部传入给自己的状态(数据)
props
在组件内修改,必须由谁传入的即由谁修改
类式组件中使用
class Person extends React.component{
constructor(props){ // 还记得构造函数的参数吗,也能够获取 props
super(props);
}
// 可以使用静态属性 propTyps 来限制props
static propTypes = {
// 在 16版本前,通过使用React自带的PropTypess属性传递值
// name: React.PropTypes.string.isRequired,
// 16版本后,PropTypes被单独移出 React作为单独的个体,需要另外导入
name: PropTypes.string.isRequired, // 字符串类型,且必须传值
sex: PropTypes.string, // 字符串类型
age: PropTypes.number, // 数字类型
speak: PropTypes.func, // 函数类型
}
// 可以使用静态属性 defaultProps 来设置某些 prop 的默认值
static defaultProps = {
sex: "男", // 设置 sex 的 prop 默认值为 “男”
}
render(){
const {name, sex, age} = this.props; // 从 props中获取值
return (
<ul>
<li>姓名:{name}}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
// ReactDOM.render( <Person name="Tom" sex="男" age="16"/>, document.querySelector("#root")); // 以类似属性的方式传递 props值
ReactDOM.render(<Person {...p} />, document.querySelector("#root"));
// 可以用扩展运算符来传递 props值
在使用的时候可以通过 this.props
来获取值
类式组件的 props
:
- 通过在组件标签上传递值,在组件中就可以获取到所传递的值
- 在构造函数的参数里可以获取到
props
- 可以分别设置
propTypes
和defaultProps
两个属性来分别操作props
的规范和默认值,两者都是直接添加在类式组件的原型对象上的(所以需要添加static
)
函数式组件中使用
还记得说函数式组件的参数是什么吗?便是 props
。
传入的方式与类式组件相同,都是在组件标签中传递值
function Person(props){ // 参数为 props
const {name, sex, age} = props; // 使用占位符获取 props的值
return (
<ul>
<li>姓名:{name}}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
Person.defaultProps = { // 设置 props默认值
sex: "男",
age: 18,
}
Person.propTypes = { // 设置 props限制
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
}
const p = {name:"Jerry", sex:"女", age:16}
ReactDOM.render(<Person {...p}/>,document.querySelector("#app"));
函数组件的 props
定义:
- 同样也是在组件标签中传递
props
的值 - 组件函数的参数为
props
- 对
props
的限制和设置默认值也同样实在原型对象上
3. refs
假设在你的组件里有着表单元素,然后你需要获取到其中的值,该怎么获取呢?有的人可能想给元素绑定 id
后用 document.querySelector
来查找元素,但这样就违背了 React的想法,又开始操作起了DOM元素,所有React提供了第三种特性 refs
在React的历史中,共有三种操作refs
的方法,分别为:
- 字符串形式
- 回调形式
createRef
形式
让我们一一来了解一下:
类式组件中使用 refs
(1) 字符串形式的 refs
class Demo extends React.Component{
showData(){
// 通过自己设定的 refs名称获取对应的元素
const {myInput} = this.refs; // 返回该元素
alert(myInput.value)
}
render(){
return (
<div>
{ /* 通过 ref属性绑定这个 input标签 */ }
<input type="text" ref="myInput" placeholder="search something" />
{/* 事件绑定会在下面讲 */}
<button onClick={this.showData}>Show Data</button>
</div>
)
}
}
(2) 回调形式的 refs
其实很简单,但在后来的版本中,官方觉得这样做有问题,于是推出了全新的版本:使用回调函数来操作 refs
class Demo extends React.Component{
showData(){
const {myInput} = this; // 返回该元素
alert(myInput.value)
}
render(){
return (
<div>
{/* 回调函数的参数为该元素本身,通过函数绑定在 this上 */}
<input type="text" ref={ e => this.myInput = e } placeholder="search something" />
<button onClick={this.showData}>Show Data</button>
</div>
)
}
}
(3) createRef形式(推荐)
后来的React发现,使用回调函数的形式还是会出现一些不可言喻的问题,所以,又一次推出了全新的的方法来使用 refs
,便是使用 React身上的方法 createRef()
class Demo extends React.Component{
// 使用 React.createRef() 创建一个 ref容器
myRef = React.createRef();
showData = ()=>{
// 在容器中获取 DOM元素
const {current} = this.myRef;
alter(current.value);
}
render(){
return (
<div>
{/* 将DOM元素绑定在容器中 */}
<input ref={this.myRef} placeholder="点击提示数据" type="text"/>
<button onClick={this.showData}>ShowData</button>
</div>
)
}
}
注意:一个 ref
容器,只能存储一个元素(专人专用),后加入的会把前一个顶出去
函数式组件中使用 refs
- 在React16.8前,与
state
相同,函数式组件也无法使用refs
特性 - 在React16.8后,出现了
Hook
函数,使得函数式组件也有了使用refs
特性的能力
以下代码现阶段无需看懂先,了解即可。
function Demo() {
const [count, setCount] = React.useState(0);
function add(){
setCount(count=> count+1);
}
return (
<div>
<h2>当前求和:{count}</h2>
<button onClick={add}>点击加一</button>
</div>
)
}
四、组件的事件绑定
1. 在函数式组件中进行事件绑定
可以在函数中定义另外一个函数(内置函数)然后进行调用即可
function Demo(props){
function showData(){
console.log(props);
alert("触发了事件");
}
return (
<div>
<button onClick={showData}>点击触发</button>
</div>
)
}
2. 在类式组件中进行事件绑定
在类式组件中可以通过类本身的方法来调用
class Demo extends React.Component{
state = { count: 0 } // 定义 state
showData(){
alert("触发了事件")
console.log(this.state.count);; 报错
}
render(){
return (
<div>
<button onClick={this.showData}></button>
</div>
)
// 成功弹出弹窗,但是输出报错
}
}
这里你会发现,虽然成功 alter
,但是输出 state
会报错,这是为什么呢?
平时在组件内使用 this.state
的时候,此时 this
的指向都是作用于类的原型对象,即类本身。
而在函数中,因为使用了 babel.js
的缘故,js
默认开启了严格模式,所以函数体的 this
为 undefined
,自然找不到在原型对象上的 state
。
此时有两种解决方法:
- 使用
bind
、apply
等方法改变this
指向 - 使用箭头函数,改变
this
指向(推荐)
class Demo extends React.Component{
constractor(props){
super(props);
// this.showData = this.showData.bind(this); // 方法1
}
state = { count: 0 } // 定义 state
showData = ()=>{ // 方法2
alert("触发了事件")
console.log(this.state.count);; 报错
}
render(){
return (
<div>
<button onClick={this.showData}></button>
</div>
)
// 成功弹出弹窗,但是输出报错
}
}
3. React的事件处理方法
- 通过
onXxx
属性指定事件处理函数(注意大小写-
React使用的是自定义(合成)事件,而不是原生的 DOM事件(为了更好的兼容性)
-
React的事件是通过事件委托方式处理的(委托给组件最外层的元素)(为了更加的高效)
-
可以通过事件的
event.target
获取发生的DOM元素对象,可以尽量减少refs
的使用 -
在绑定事件的时候不要加括号,这会被
jsx
识别为执行函数 -
在类式组件中绑定函数注意
this
的指向问题(推荐绑定的函数都使用箭头函数)
-
五、组件的生命周期
对 Vue
熟悉的应该对生命周期并不陌生。
组件在挂载到页面上的过程中,期间发生了许多的事情,而生命周期,可以看作是一个组件的执行流程,我们可以通过在某个流程节点中进行一些操作,而这就是 生命周期钩子(函数)
注:生命周期函数只适用于类式组件,并不适用于函数式组件,但是在React16.8之后的 Hook
,也可以让函数式组件实现类似于生命周期钩子的功能
1. React 16之前
以下是流程:
- 自身组件挂载
constructor
: 构造函数(初始化)componentWillMount
: 组件挂载前render
: 组件挂载中componentDidMount
: 组件挂载完成后
- 拥有父组件且父组件状态进行更新(
setState
):- 父组件
shouldComponentUpdate
: 父组件是否进行状态更新 - 父组件
componentWillUpdate
: 父组件状态更新前 - 父组件
render
: 父组件更新挂载中 - 子组件
componentWillReceiveProps
: 子组件将收到新的Props
- 子组件
shouldComponentUpdate
: 子组件是否进行状态更新 - 子组件
componentWillUpdate
: 子组件状态更新前 - 子组件
render
: 子组件更新挂载中 - 子组件
componentDidMount
: 子组件挂载完成 - 父组件
componentDidMount
: 父组件挂载完成
- 父组件
- 当组件要进行卸载时:
componentWillUnmount
: 组件卸载前
有几个需要注意的地方:
shouldComponentUpdate
的意思是是否更新状态,所以他应该有个布尔类型返回值,而且默认为true
(不写的时候),当你写了这个钩子时别忘了写个返回值,这个横眉周期一般用于进行更新值的一些判断。shouldComponentUpdate
有两个参数,分别是nextProps
:新的props
与nextState
: 新的state
,此时他们都还没更新到组件上。- 使用
setState
时会经过生命周期shouldComponentUpdate
,但是使用forceUpdate
时则不会,而是直接到componentWillUpdate
生命周期
2. React 16之后
新旧生命周期对比:
- 在新的生命周期中,舍弃了(即将舍弃)三个生命周期函数:
componentWillMount
componentWillReceiveProps
componentWillUpdate
- 新增了两个生命周期函数:
-
getDerivedStateFromProps
-
getSnapshotBeforeUpdate
-
以下是流程:
- 自身组件挂载:
constructor
: 构造函数(初始化)getDerivedStateFromProps
: 从props
中获取派生的state
render
: 组件挂载中componentDidMount
: 组件完成挂载
- 父组件更新时
- 父组件
getDerivedStateFromProps
: 从props
中获取派生的state
- 父组件
shouldComponentUpdate
: 判断是否进行状态更新 - 父组件
render
: 父组件挂载中 - 子组件
getDerivedStateFromProps
: 从props
中获取派生的state
- 子组件
shouldComponentUpdate
: 判断是否进行状态更新 - 子组件
render
: 子组件挂载中 - 子组件
getSnapshotBeforeUpdate
: 子组件获取状态更新前的快照 - 子组件
componentDidUpdate
: 子组件完成更新 - 父组件
getSnapshotBeforeUpdate
: 父组件获取状态更新前的快照 - 父组件
componentDidUpdate
: 父组件完成更新
- 父组件
- 组件卸载时
componentWillUnmount
: 组件卸载前
也有几个需要注意的地方:
getDerivedStateFromProps
需要定义个实例本身,所以是静态方法getDerivedStateFromProps
有两个参数,分别是当前的props
和state
- 若需要组件的状态任何时候都取决于
props
则可以使用getDerivedStateFromProps
,但是使用场景比较罕见,可以在其中定义state
getSnapshotBeforeUpdate
有两个参数,分别为状态更新前的props
和state
,并且有一个返回值(快照),一般用作本次更新情况的说明情况。componentDidUpdate
有三个参数,分别为状态更新前的props
和state
,以及先前getSnapshotBeforeUpdate
返回的快照。- 其他的与旧的生命周期没有什么差别
六、组件列表渲染、条件渲染与DOM的Diffing
算法
1. 组件列表渲染
在有时候我们需要批量的去创建一些DOM元素或组件,比如页面上的:新闻列表、推文列表、好友列表等等,你会发现在开发的过程中经常会使用到列表,但是自己一个个的去写DOM回十分繁琐。
我们可以通过数组存储数据,也可以使用数组来循环渲染数据。
举个例子:
class Demo extends React.Component{
state = {
arr: [a, b, c],
}
render(){
// 输出 abc
return (
<div>
<ul> {stus} </ul>
</div>
)
}
}
你会发现,React输出数组会把所有的元素直接循环输出出来,那么我们只要在每个元素左右添加上标签,那不就构成了列表渲染吗?
这里我们可以通过 jsx
语法配合ES6
语句来实现列表渲染。
直接上代码:
class Demo extends React.Component{
state = {
stus: [
{id: "001", name: "小明", age: "28"},
{id: "002", name: "小红", age: "26"},
],
}
render(){
return (
<div>
<ul>
{
this.state.stus.map(item =>{
return <li key={item.id}>{item.name}---{item.age}</li>
})
}
</ul>
</div>
)
}
}
2. key 的使用
有的人发现了写列表渲染的时候,我都会给每个标签加上一个 key
属性,这是为什么呢?
其实 key
的作用是给当前的标签添加一个唯一的标识,用于给React进行 diffing算法计算的时候使用
3. diffing 算法
当状态发生改变时,react会根据【新的状态】生成【新的虚拟DOM】
然后将新旧虚拟DOM进行 diff比较,比较规则如下:
- 旧虚拟DOM 中找到了与 新虚拟DOM 相同的 key
- 若虚拟DOM中的内容没变,则直接使用之前的真实DOM
- 若虚拟DOM中的内容变了,则生成新的真实DOM并进行替换
- 旧虚拟DOM 中未找到与 新虚拟DOM 相同的 key,则根据数据创建新的真实DOM,然后渲染到页面
用index作为key可能引发的问题
- 对数据进行 逆序添加、逆序删除 邓破坏顺序的操作 会产生没有必要的真实DOM更新,影响效率
- 对结构中包含输入类的DOM 会产生错误的 DOM更新,同时界面渲染有问题
- 若仅用于展示数据,那用 index作为 key则没有问题
七、React脚手架
恭喜,终于熬到了脚手架这里了,终于可以一键生成所有东西而不是自己一个个引用了。
什么是脚手架呢? 脚手架可以说是建房子时的构架,其他人已经帮你吧房子的架构搭好了,不需要自己动手。
而React就有自己的脚手架,开发团队通过 webpack
,babel
、npm
等方法,帮你搭建好了一个使用React开发软件的环境,不再需要自己去创建文件夹,导入React等重复的操作,更加利于我们编写 SPA
应用(单页面富应用)。
1. 安装React脚手架
在自己的命令行窗口中输入(需要现有 node
环境):
npm i create-react-app -g
全局安装 create-react-app
脚手架
2. 创建React应用
create-create-app 应用名称
- 应用名称不应该出现大写字母和特殊字符
- 使用英文命名而不是中文命名
3. 文件解析
项目文件中比较常用的文件就以下这些:
- node_modules ------ npm包的存放位置
- public ------ 用于存放静态文件
- src ------ 项目的代码存放位置
- components ------ 用于存放公用组件的文件夹
- page ------ 用于存放页面的文件夹
App.js
------ 根组件App.css
------ 根组件的样式index.js
------ 项目入口文件index.css
------ 项目的公用样式
.gitgnore
------ 编写git
的配置文件package.json
------ 项目配置文件README.md
------ 项目信息
还记得我们的讲组件化的图片吗
其中的 APP
就是根组件,通过我们编写其他的组件如 Header
、Aside
等组件,都加装在根组件上。
4. npm 指令
npm start // 使用 webpack-dev-server 启动服务查看应用
npm build // 打包生成生产文件
npm test // 进行软件测试(不常用)
npm eject // 将所有的配置文件暴漏出来(不常用且不建议用)
八、react-router的使用
1. 什么是 react-router
react-router
是为了React编写SPA
应用操作前端路由而诞生的。
(1) 前端路由(可能有点直白)
前端路由是通过HTML5
的新API History
来操作的,其原理就是url地址的地址发生改变,但是并不会触发重新加载,同时javascript
可以监听到改变。
有两种类型:
HashRouter
: 利用url地址栏的 # 后面的哈希值
BrowserRouter
: 利用浏览器的History API,地址栏中不包含 # ,显得更加美观
(2) SPA 应用
SPA
全称 Single Page web Application,顾名思义是只有一个页面的应用,通过javascript
进行实时的渲染来更新页面。
即通过当前的路由来判断页面应该加载什么组件,从而呈现不同的页面与效果。
2. 使用 react-router
(1) 安装与使用
在项目文件夹中打开命令行窗口进行 npm
下载
npm i react-router-dom -S
注意:我们下载的是 react-router-dom
而不是 react-router
两者区别:
react-router
:提供了router
的核心 API。如Router
、Route
、Switch
等,但没有提供有关dom操作进行路由跳转的API;react-router-dom
:提供了BrowserRouter
、Route
、Link
等api,可以通过dom操作触发事件控制路由。
(2) 常用组件
a. 路由跳转
在多页面应用中,通常都是使用 a
标签进行页面跳转
<a href="http://localhost:3000">跳转页面</a>
使用单页面富应用中使用react-router
则使用路由跳转组件
import {Link, NavLink} from "react-router";
<Link activeClassName="nav-active" className="nav" to="/about">About</Link>
<NavLink activeClassName="nav-active" className="nav" to="/home">Home</NavLink>
activeClassName
: 处于当前路由时,对应的组件会自动添加该类className
: 当前组件类名to
: 当前组件所对应的路由
Link
组件与 NavLink
组件都可以进行路由的跳转,区别在于:当前路由对应的NavLink
会自动添加class
: active
,而 Link
不会。
b. 注册路由
import {Route} from "react-router";
<Route path="/home" component={Home}></Route>
<Route exact path="/about" component={About}></Route>
path
: 所要监听的路由component
: 该路由要绑定的组件exact
: 可选,不写时为false
,是否选择严格匹配
当当前路由对应上了路由组件所绑定的路由时,则会展示所绑定的组件。
(a) 路由严格匹配与模糊匹配
路由不仅仅只有一级,有的时候是有多级嵌套的,比如以下这张:
模糊匹配和严格匹配,都是指当前的组件对当前路由的匹配模式:
- 模糊匹配: 如果当前路由与匹配的路由成相等或包含(注意层级)的情况,则启用该组件
http://localhost:3000/home/a/b/c
则为包含路由/home
http://localhost:3000/a/b/home/c
则为不包含路由/home
(层级不对)
- 严格匹配: 如果当前路由与匹配的路由相等的话,才启用该组件
http://localhost:3000/home
则为与路由/home
相等http://localhost:3000/home/a
则为与路由/home
不相等
c. 重定向路由
你明明设置好了路由 /home
,但是有的用户就喜欢对着干,在地址栏输入了 /nothing
,而你没有注册这个路由,那该怎么办呢?
这个时候你就可以东涌道重定向路由了,对于没有注册过的路由,都会被跳转到你指定的某个路由去,这就是重定向路由。
经常可以用作一些404,页面丢失等情况的路由跳转方式。
import {Redirect, Route} from "react-router";
<Route ....../>
<Route ....../>
<Redirect to="/home"/>
to
: 需要重定向到哪个路由?
Redirect
需放在所有Route
下面,当上面的 Route
都没有匹配到时,则路由将重定向到指定的路由。
d. Switch 路由
你想想,如果你的路由中,出现了 /home
与 /home/abc
和 /home/a/b/c
等这样的路由,当路由为 /home
时则会三个路由都同时渲染,但是你又只想要渲染其中的一条,这个时候我们就可以使用 Switch
组件。
使用 Switch
组件包裹住所有的 Route
和 Redirect
,当出现多个匹配的路由时,只会渲染第一个匹配的组件。
import {Switch, Route, Redirect} from "react-router";
<Switch>
<Route ..../>
<Route ..../>
<Redirect to="..."/>
</Switch>
e. 路由器
你想要使用路由跳转组件和路由组件,还差一个路由器组件,同时路由器组件必须包裹着这两个组件。
import {HashRouter, BrowserRouter} from "react-router";
一般为了使整个React应用都可以使用到路由组件,所以一般我们都是把路由器包裹在根组件上的。
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>,
document.querySelector("#root")
);
有两种路由器组件,分别是HashRouter
与 BrowserRouter
,分别对应者两种路由方式。
(3) 路由组件
路由组件与一般组件
- 写法不同
- 一般组件:
<Demo></Demo>
- 路由组件:
<Route path="/demo" component={Demo}/>
- 一般组件:
- 存放位置不同
- 一般组件:components文件夹
- 路由组件:page 文件夹
- 接受到的
props
不同- 一般组件:根据组件标签传递了上面,就收到了什么
- 路由标签:会收到三个固定的属性
{
"history": {
"length": 18,
"action": "PUSH",
"location": {
"pathname": "/home",
"search": "",
"hash": "",
"key": "tvfyve"
}
},
"location": {
"pathname": "/home",
"search": "",
"hash": "",
"key": "tvfyve"
},
"match": {
"path": "/home",
"url": "/home",
"isExact": true,
"params": {}
}
}
3.嵌套路由
假设我们有个路由组件为 Home
,在根组件中使用 Link
跳转到了该路由,当前路由为 /home
,可在组件 Home
中还有两个 Link
,分别导向路由 /home/message
与 /home/news
然后在组件中还有其他的路由组件。这就时嵌套路由的使用。
如下面的代码:
class Home extends Component{
render(){
return (
<div>
<Link to="/home/message">Message</Link>
<Link to="/home/news">News</Link>
<hr/>
<Route path="/home/message" component={Message} />
<Route path ="/home/news" component={News} />
</div>
)
}
}
4. 编程式路由
如果说,我们想要做用户点击按钮登陆后,如果他是老师就去老师页面,如果是学生就去学生页面,这个显然单靠 Ljnk
无法完成,我们可以通过 js
进行路由的跳转(也是 react-router
基于 History API
编写的)
class Message extends Component {
state = {
messageArr:[
{id:"01", title: "消息1"},
{id:"02", title: "消息2"},
{id:"03", title: "消息3"},
]
}
// 编程式路由导航
pushShow = (id, title)=>{
// push跳转 + 携带 params参数
this.props.history.push(`/home/message/detail/${id}/${title}`);
// push跳转 + 携带 search参数
// this.props.history.push(`/home/message/detail?id=${id}&title=${title}`);
// push跳转 + 携带 state参数
// this.props.history.push(`/home/message/detail`, {id,title});
}
replaceShow = (id, title)=>{
// replace跳转 + 携带 params参数
this.props.history.replace(`/home/message/detail/${id}/${title}`);
// replace跳转 + 携带 search参数
// this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`);
// replace跳转 + 携带 state参数
// this.props.history.replace(`/home/message/detail`, {id, title});
}
// 后退
goBack = ()=>{
this.props.history.goBack();
}
// 前进
goForward = ()=>{
this.props.history.goForward();
}
// 跳转指定位置
go = ()=>{
// 向前两步
this.props.history.go(2);
// 后退两步
this.props.history.go(-2);
}
render() {
const {messageArr} = this.state;
return (
<div>
<ul>
{
messageArr.map(item=>{
return (
<li key={item.id}>
<Link to={`/home/message/detail/${item.id}/${item.title}`}>{item.title}</Link>
<button onClick={() => this.pushShow(item.id, item.title)}>push查看</button>
<button onClick={() => this.replaceShow(item.id, item.title)}>replace查看</button>
</li>
)
})
}
</ul>
<hr/>
<Route path="/home/message/detail/:id/:title" component={Detail} />
<button onClick={this.goBack}>goBack</button>
<button onClick={this.goForward}>goForward</button>
<button onClick={this.go}>go</button>
</div>
)
}
}
总结一下上面的代码:
- 编程式路由都是通过
props
中的history
对象进行操作(都是该对象身上的方法,调用方式为:this.props.history.xxx
) - 常用方法:
-
push(route[, state])
: 跳转到指定路由(带有历史记录) -
replace(route[, state])
: 跳转到指定路由(不带有历史记录) -
goBack()
: 后退一个 -
goForward()
: 前进一个 -
go(num)
: 前往指定步数,当num
为正数时,为前进,当num
为负数时则为后退。
-
5. withRouter 组件
有的时候,我们想要在其他组件中也使用路由组件的功能,比如导航栏,应该属于公用组件,但是里面的导航链接的功能却是路由组件的功能,我们应该怎么解决呢?
在 react-router
中,提供了这么一种方法,可以让一般组件具有路由组件的功能,则就是 withRouter()
方法。
看看演示:
import {withRouter} from "react-router-dom";
class Header extends Component {
// withRouter后该组件也有了路由组件的功能
goBack = ()=>{
this.props.history.goBack();
}
go = ()=>{
this.props.history.go(2);
}
goForward = ()=>{
this.props.history.goForward();
}
render() {
return (
<div>
<h1>This is a React-router-dom Test!</h1>
<button onClick={this.goBack}>goBack</button>
<button onClick={this.goForward}>goForward</button>
<button onClick={this.go}>go</button>
</div>
)
}
}
// withRouter 用于给一般组件添加上路由组件特有的功能,返回一个新组件
export default withRouter(Header);
九、路由组件之间的参数传递
父子组件之间的内容传递,可以通过 props
来进行参数传递,但是路由组件却没有自己的标签,那么该如何进行参数的传递呢?
还记得路由组件特有的 props
吗,我们可以利用 History API
的一些特性来进行路由组件之间的参数传递,有三种方法。
params
search
state
1. 传递 params 参数
// 父组件
class Parent extends Component {
state = {
messageArr:[
{id:"01", title: "消息1"},
{id:"02", title: "消息2"},
{id:"03", title: "消息3"},
]
}
render() {
const {messageArr} = this.state;
return (
<div>
<ul>{
messageArr.map(item=>{
// 向路由组件传递 params参数
return <li key={item.id}><Link to={`/home/message/detail/${item.id}/${item.title}`}>{item.title}</Link></li>
})
}</ul>
<hr/>
{/* 声明接受 params参数,可以在 props中的 match属性的 params属性里获得 */}
<Route path="/home/message/detail/:id/:title" component={Child} />
</div>
)
}
}
// 子组件
class Child extends Component {
state = {
contentArr:[
{id:"01", content: "你好中国"},
{id:"02", content: "你好世界"},
{id:"03", content: "你好帅哥"},
]
}
render() {
console.log(this.props);
// 获取 params参数
const {id, title} = this.props.match.params
const findResult = this.state.contentArr.find(obj=>obj.id == id).content;
return (
<div>
<ul>
<li>ID: {id}</li>
<li>TITLE: {title}</li>
<li>CONTENT: {findResult}</li>
</ul>
</div>
)
}
}
在使用 params
传递参数时,你会清楚的看到参数以路由的形式展现了出来,例如:
http://localhost:3000/home/message/用户1/文章32
类式上面的用户1和文章32,这就是传递的参数。
以params
方式传递的参数,在路由中需要声明接受才可以使用。
<Route path="/home/message/detail/:id/:title" component={Child} />
后面的 :id
与 :title
就是可变路由,可以通过props
的match
接受接收这里的内容.
const {id, title} = this.props.match.params
2. 传递 search 参数
这个就是依赖 get
的请求方式。
http://localhost/home/message?id=1&title=abc
即 javascript
可以获取到 url
中 ? 后面的请求体。
所以我们可以吧上面的 map
中返回的标签修改以下
<li key={item.id}><Link to={`/home/message/detail?id=${item.id}&title=${item.title}`}>{item.title}</Link></li>
且这种方式不需要在路由中声明接收。
通过 props
中的 location
进行接收
const search = this.props.location.search;
// 获取到的格式时: ?id=xxx&title=xxx 所以还需要加工一下
const {id, title} = qs.parse(search.slice(1));
3. 传递 state 参数
通过 HistoryAPI
来进行数据传输:
<li key={item.id}><Link to={{pathname: `/home/message/detail`, state:{id:item.id, title:item.title}}}>{item.title}</Link></li>
不需要在路由链接中添加任何东西,也不需要路由进行声明接收。更加的美观。
在 props
中的 location
里获取 state
属性
const {id, title} = this.props.location.state;
4. 三者对比
路由组件传参其实用的较少,因为他们都有对某些东西的依赖:
params
: 依赖于路由链接,链接不美观search
: 依赖于路由链接,链接不美观state
: 依赖于历史记录 (HistoryAPI
),链接美观,但是当直接输入链接时则会报错(没有历史记录)
所以尽量使用其他的传参方式,如果非要使用的话,有限度比较为:
params
(最为常用)search
(较为常用)state
(比较少用)
十、redux 的使用
- 方法1:通过不断的
props
进行传参,但是这非常的费时费力 - 方法2:使用
Pubsub.js
等进行消息发布/订阅功能 - 方法3:使用
react-redux
进行数据集中式管理
即 redux
可以看作一个管家,负责帮忙存储公共的数据。
1. 安装 redux
npm i redux -S
2. 核心概念
redux
有着三个核心概念
action
:- 动作的对象(操作内容)
- 包含两个属性:
a.
type
: 表示属性,值为字符串,唯一,必要属性(要干嘛) b.data
: 数据属性,值为任意类型,可选属性(怎么干) - 例如:
{ type: "ADD_STUDENT", data: { name: "tom", age: 18 } }
reducer
:- 用于初始化状态和加工状态(对数据进行初始化和操作数据的)
- 加工时,根据旧的
state
和action
,产生新的state
的纯函数 - 有两个参数,一个为之前的状态(
prevstate
)与动作对象(action
)
store
:- 将
state
、action
、reducer
联系在一起的对象(大脑)
- 将
3. 基本使用
(1) 创建文件夹
在 src
文件夹中创建 redux
文件夹,用于存放 redux
的相关内容
- Compnoent ------ 存放组件相关的文件夹
- redux ------ 存放
redux
相关内容的文件夹- actions ------ 存放
action
相关内容的文件夹 - reducers ------ 存放
reducer
相关内容的文件夹 constant.js
------ 存放规范命名的文件store.js
------ 编写store
的文件
- actions ------ 存放
由我来一个个带你们解析。先写一个最简单的 redux
/**
* store.js
* 该文件专门用于暴漏一个 store对象,整个应用只有一个 store对象
*/
// 引入 createStore,专门用于创建 redux中最为核心的 store
import {createStore} from "redux";
// 引入为 Count组件服务的 reducer
import countReducer from "./count_reducer";
const store = createStore(countReducer)
// 暴露 store对象
export default store;
/**
* / reducer / count.js
* 1. 该文件是用于创建一个为 Count组件服务的 reducer,reducer的本质就是一个函数
*
* 2. reducer函数会收到两个参数,分别为:之前的状态(preState),动作对象(action)
*
* 3. 会自动调用一次 reducer(初始化)
*/
// 初始化的状态
const initState = 0;
export default function countReducer(preState = initState, action){
if(preState === undefined) preState = 0;
// 从 action对象中获取 type,data
const {type, data} = action;
// 根据 type觉得如何加工数据
switch (type) {
case 'increment': // data
return preState + data;
case 'decrement': // 如果是减
return preState - data;
default:
return preState;
}
}
// / Component / Count.js
import React, { Component } from 'react'
// 引入store
import store from "../../redux/store";
export default class Count extends Component {
// 加法
increment = ()=>{
const {value} = this.selectNumber;
// 发送动作对象给 store
store.dispatch({
type: "increment",
data: value*1
})
}
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select ref={c=>this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
</div>
)
}
}
以上就是最精简的 redux
,让我们分析一下流程:
- 编写
Count
组件,创建Count
组件对应的Reducer
文件 - 编写
Reducer
的代码并抛出::Reducer
是个纯函数- 通常使用
Switch
进行action
中的type
的判断 - 函数返回值为修改后的值
- 创建
store
并编写代码:- 使用方法
createStore()
方法来创建一个store
,参数是一个reducer
- 把编写好的
Count
组件的reducer
导入
- 使用方法
- 在组件中引入
store
并进行调用:- 在方法中通过使用
dspatch()
方法向store
传递action
dispatch
的参数时一个对象(即action
动作对象)
- 在方法中通过使用
(2) 使用流程
以上使创建的大致流程,而使用的大致流程是这样的:
(3) 使用异步redux
如果我们需要使用异步的 redux
的话,还需要借助另一款插件: redux-thunk
npm i redux-thunk -S
这是一个中间件,用于帮忙处理异步的 redux
,
异步的 action
的值为一个函数
在函数中进行普通的 dispatch()
操作
export const createIncrementAsyncAction = (data, time=500)=>{
// 返回一个 action
return ()=>{
setTimeout(()=>{
store.dispatch(createIncrementAction(data));
}, time);
}
}
同时我们要在 store
处设置让他支持执行中间件,通过 redux
的 applyMiddleware()
方法就可以加载中间件,他的参数就是中间件,然后 applyMiddleware()
将作为 createStore()
的第二个参数引入。
// store.js
// 引入 applyMiddleware,专门用于执行中间件
import {createStore, applyMiddleware} from "redux";
// 引入为 Count组件服务的 reducer
import countReducer from "./count_reducer";
// 引入 redux-thunk,用于支持异步 action
import thunk from "redux-thunk";
// 暴露 store对象
export default createStore(countReducer, applyMiddleware(thunk));
(4) 监听状态变化
你写着写着有没有发现,虽然 redux
里面的状态确实更新了,但是页面并没有变化啊?
还记得页面渲染使用的是哪个函数吗?render()
函数。可是在 redux
状态发生变化时,并不会帮助我们调用 render()
函数,所以我们需要手动实现实时渲染页面。
在这里我们使用到了 redux
的 store
上的 subscribe()
方法,用于监听 redux
上状态的变化,参数是一个函数,便于我们进行操作。
一般我们都写在根标签上(精简些,不用再每个使用 redux
的组件中都写一遍)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from "./redux/store"
ReactDOM.render(
<App />,
document.getElementById('root')
);
// 精简写法
store.subscribe(()=>{
ReactDOM.render(
<App />,
document.getElementById('root')
);
})
4. 注意事项
- 一般使用到
redux
的话,需要使用的组件肯定不止一个,所以创建 actions和reducers用来存储多个action
和reducer
- 同上,
store
肯定不止加载一个reducer
,所以我们使用redux
的combineReducers()
方法来整合所有的reducer
combineReducers()
方法的参数是一个对象,里面存放着所有的reducer
import {createStore, applyMiddleware, combineReducers} from "redux";
import countReducer from "./reducers/count";
import personReducer from "./reducers/person";
import thunk from "redux-thunk";
// 汇总所有的 reducer
const allReducer = combineReducers({
count: countReducer,
persons: personReducer,
});
export default createStore(allReducer, applyMiddleware(thunk));
- 因为
redux
在设计上还有许多的问题,例如:- 单个组件需要做:与
store
打交道,获取数据,监听数据变化,派发action
对象等,一个组件负责的事情太多了。 - 需要另外监听
redux
的状态变化来更新状态并渲染页面。 - 所以有人对
redux
进行了优化,推出了另一个库react-redux
(放心,没啥不同,就是多了点优化,后面会讲)
- 单个组件需要做:与
- 能不使用
redux
,就不要使用redux
(不管是redux
还是react-redux
) - 能不使用
redux
,就不要使用redux
(不管是redux
还是react-redux
) - 能不使用
redux
,就不要使用redux
(不管是redux
还是react-redux
)
十一、了解与使用 react-redux
前面也说了,react-redux
其实就是 redux
的升级版,对许多地方进行了优化,但在学习他之前,需要我们进行一些对 redux
的优化知识。
1. 使用容器组件和UI组件
其目的就是为了把组件身上太多的活进行拆分,分为UI组件(内组件)和容器组件(外组件),两个组件之间使用 props
进行通信,对 store
那边的请求状态,更改状态的活交给容器组件来干,而通过状态来编写页面,更新渲染等活,就交给 UI组件来干。
了解了这个后,就可以开始使用 react-redux
了
2. 安装 react-redux
npm i react-redux -S
这个就不多说了。
3. 创建文件夹
对于容器组件,我们都是使用 containers文件夹进行存储。
- containers ------ 用于存储容器组件的文件夹
- redux ------ 用于存储
react-redux
相关的文件夹
4. 创建容器组件
容器组件通过 react-redux
的 connect()
方法进行创建。
// 引入 Count的 UI组件
import Count from "../../components/Count";
// 引入 connect用于连接 UI组件与 redux
import { connect } from 'react-redux'
// 该函数的返回值作为状态传递给 UI组件
function mapStateToProps(state){
return {
count: state,
}
}
// 该函数的返回值作为操作状态的方法传递给 UI组件
function mapDispatchToProps(dispatch){
return {
add: data=>{
console.log(1234);
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Count);
使用 connect( )( )
创建并暴露一个 Count
组件的容器组件。
调用 connect
时有两个参数,且必须时函数。
mapStateToProps( state )
: 该函数的返回值会作为 状态 传递给 UI组件state
: 参数state
为react-redux
默认操作好的store.getState()
mapDispatchToProps( dispatch )
: 该函数的返回值会作为 操作状态的方法 传递给 UI组件,有语法糖写法,传入个对象即可(看下面代码)。dispatch
: 参数dispatch
为react-redux
默认给的store.dispatch
方法
这里只是为了讲解方便,使用时可以使用语法糖的。
注意的几个地方:
- 一般都是把 UI组件 与 容器组件 写在一个文件中,至于存在哪个文件夹中看公司需求(一般都是 containers)
- 容器组件里的
store
不是通过引入使用,而是作为props
传递给容器组件的标签的。<Count store={store} />
- 语法糖写法:
// 引入 Count的 UI组件
import Count from "../../components/Count";
// 引入 connect用于连接 UI组件与 redux
import { connect } from 'react-redux'
// 精简写法
export default connect(
state => ({count: state}),
{
add: data=> console.log(1234, data)
}
)(Count);
5. Provider 组件
如果你有许多个容器组件,那么每个容器组件都要传入 store
,那么是不是觉得太繁琐了呢?所以 react-redux
提供了 Provider
组件用于处理这个问题,只需要在根标签处使用并把 store
传递进去,他就可以自动判断哪些组件需要使用 store
并自动传递给它。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from "./redux/store"
// 优化3: 使用自带的 Provider自动判断哪些组件需要使用 store,从而自动导入
import { Provider } from "react-redux";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
6. 监听状态变化
有没有发现,我上面的代码已经没有再写 store.subscribe()
监听状态变化了,是因为我们创建容器组件的 connect
已经帮我们进行监听了。
7. 容器组件之间通信
其实这个很简单,还记的 connect
的第一个函数的第一个参数吗?传递给 UI组件的是 props
,他的参数是 state
,这个 state
是 store.getState()
。
可是此时你的 store.getState()
不再是一个单纯的值,而是所有 reducer
的对象,所以我们可以在里面获取到其他容器组件的值。
export default connect(
state => ({count: state.count, personLength: state.persons.length}),
{
increment: createIncrementAction,
decrement: createDecrementAction,
asyncIncrement: createIncrementAsyncAction,
}
)(Count);
8. 文件规范
创建了那么多东西,有的可以整合一下的,让我们来修整修整:
- containers ------ 用于存放容器组件的文件夹(UI组件与容器组件写在一起)
Count.js
------Count
的容器组件Person.js
------Person
的容器组件
- redux ------ 用于存放
react-redux
相关的文件夹-
actions ------ 用于存放所有
action
的文件夹count.js
------ 用于存储Count
组件的action
person.js
------ 用于存储Person
组件的action
-
reducers ------ 用于存放所有
reducer
的文件夹count.js
------ 用于存储Count
组件的reducer
person.js
------ 用于存储Person
组件的reducer
index.js
------ 用于存储汇总的reducer
的文件(combineReducers()
方法)
-
constant.js
------ 用于存储一些公用的命名的文件 -
store.js
------react-redux
的store
文件
-
9. 再说一遍,能不用就别用这东西!!
十二、React16.8 与一些扩展
1. lazy() 与 Suspense
之前的组件,都是一并加载的,这样会给服务器带来较大的负担,所以 react
推出了 lazy()
懒加载,进行组件的按需加载。
与之一起出来的是 Suspense
,他解决的是组件在加载过程中还没加载出来时的白屏,用于展示其他的内容。
import React, { Component, lazy, Suspense } from 'react'
import {Route, Link} from "react-router-dom"
// import Home from "./Home";
// import About from "./About";
import Loading from "./Loading";
// lazy() 方法的参数是一个函数,返回需要加载的组件
const Home = lazy(()=>import("./Home"))
const About = lazy(()=>import("./About"))
export default class index extends Component {
render() {
return (
<div>
<Link to="/home">Home</Link>
<Link to="/about">About</Link>
<hr/>
{/* Suspense 用于解决加载组件时的白屏,可以显示其他的内容,而其他内容不允许使用 lazy加载 */}
<Suspense fallback={<Loading/>}>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
</Suspense>
</div>
)
}
}
lazy
的参数是一个函数,使用import
导入一个组件并返回Suspense
的属性fallback
属性的属性值是 组件标签 而不是组件Suspense
所使用的组件不能使用lazy
进行懒加载。
2. Hook
React16.8可以说是给函数式组件一次春天,因为他有了Hook
,可以实现一些 state
、生命周期函数,refs
等特性。
让我们一个个来看:
(1) stateHook
可以让函数式组件实现使用 state
的特性:
export default function Demo() {
// useState返回一个数组,只有两个元素(只有两个元素)
// 元素1 为状态,元素2 为更新状态的方法
// 第一次调用时以及将count进行底层存储,所以 Demo重复渲染不会重置count数据
const [count, setCount] = React.useState(0); // 初始值赋为 0
const [name, setName] = React.useState("Tom");
function add(){
// 进行状态赋值
// setCount(count + 1); // 写法1,直接将原来的状态值覆盖
setCount(count=> count+1); // 写法2,参数为函数,接受原本的状态值,返回新的状态值,覆盖原来的状态
}
return (
<div>
<h3>名字:{name}</h3>
<h2>当前求和:{count}</h2>
<button onClick={add}>点击加一</button>
</div>
)
}
(2) EffectHook
可以让函数式组件实现类似生命周期钩子的特性:
export default function Demo() {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("Tom");
function add(){
setCount(count=> count+1);
}
function updName(){
setName(name=>"Jerry");
}
function unmount(){
ReactDOM.unmountComponentAtNode(document.querySelector("#root"));
}
// useEffect接收两个参数,第一个为函数体,第二个为检测的对象(数组),当检测的对象状态发生改变,就会触发函数
// 不填写第二参数时,检测所有元素,相当于 componentDidUpdate生命周期函数
React.useEffect(()=>{
// console.log("asdf")
let timer = setInterval(()=>{
setCount(count=>count+1);
},1000);
return ()=>{ // 在 useEffect中的函数体里返回的函数,相当于 componentWillUnmount生命周期函数
console.log("unmount")
clearInterval(timer);
; }
},[]) // 数组为空是谁也不检测,只执行一次函数,相当于生命周期函数的 componentDidMount
return (
<div>
<h2>当前求和:{count}</h2>
<button onClick={add}>点击加一</button>
<h2>当前名字:{name}</h2>
<button onClick={updName}>修改名字为Jerry</button>
<button onClick={unmount}>卸载组件</button>
</div>
)
}
可以把 useEffect Hook
看作三个函数的结合:
componentDidMount
componentDidUpdate
componentWillUnmount
(3) refHook
refHook
可以让函数式组件实现类似 ref
的特性
export default function Demo() {
const [count, setCount] = React.useState(0);
function add(){
setCount(count=> count+1);
}
function show(){
// 获取文本框内容
alert(myInput.current.value);
}
// 生成一个容器
const myInput = React.useRef();
return (
<div>
<h2>当前求和:{count}</h2>
{/* 绑定容器 */}
<input type="text" ref={myInput}/>
<button onClick={add}>点击加一</button>
<button onClick={show}>点击展示数据</button>
</div>
)
}
这个就没啥难度。
3. Fragment
在 react
渲染组件的时候,当你的组件越来越多时,你有没有发现你的 DOM层级越来越多,有些不美观,所以就出现了 Fragment
。
可以使用 Fragment
标签代替组件的根标签,在 React解析的时候会被处理掉,
从而让生成出来的代码的层级更加简洁。
export default class Demo extends Component {
render() {
return (
// 使用空标签可以达到一样的效果,但是空标签不允许包含任何的属性
<Fragment key={1}>
<input type="text"/>
<input type="text"/>
</Fragment>
)
}
}
4. Context
Context
是一种新的组件通信方式,常用于【祖组件】和【后代组件】之间通信。
// 1. 创建一个 Context容器对象
const UserNameContext = React.createContext();
// 1.1 拿到 Provider与 Consumer属性
const {Provider, Consumer} = UserNameContext;
export default class A extends Component {
state={username: "Tom"}
render() {
return (
<div className="a">
<h1>我是A组件</h1>
<p>我的用户名是:{this.state.username}</p>
{/* 2 使用组件,后代组件都能收到来自 value的值,就在 this上的 context属性上(需提前声明) */}
<Provider value={this.state.username}>
<B/>
</Provider>
</div>
)
}
}
class B extends Component {
// 3. 声明接受 Context
static contextType = UserNameContext; // 此方法只适用于 类组件
render() {
console.log(this.context); // Tom
return (
<div className="b">
<h2>我是B组件</h2>
<p>A的用户名是:{this.context}</p>
<C/>
</div>
)
}
}
function C() {
return (
<div>
<div className="c">
<h3>我是C组件</h3>
{/* 3.2 使用 Consumer组件进行声明接受(类组件和函数式组件都可以) */}
<Consumer>
{value=> ("A的用户名是:" + value)}
</Consumer>
</div>
</div>
)
}
5. PureComponent
在组件中,只要执行了 setState()
,即使没更新状态数据,组件也会重新 render()
。
只要组件重新 render()
,就会自动重新 render()
子组件,纵使子组件没有使用到父组件任何数据。
这两种情况都会导致重复渲染,使得效率低下。
效率高的做法:只有组件的state
或props
发生变化时菜重新render()
。
- 解决方法1:通过生命周期函数
shouldComponentUpdate()
进行数据判断在进行重新渲染 - 解决方法2:类组件通过继承
PureComponent
组件,自动进行数据判断(浅对比---判断地址值)(常用)
import React, { Component, PureComponent } from 'react'
export default class Parent extends PureComponent {
state = {carName: "奔驰"}
changeCar = ()=>{
this.setState(state=>({carName:"迈巴赫"}));
}
// shouldComponentUpdate有两个参数,分别是准备修改的 props和 state
// shouldComponentUp date(nextProps, nextState){
// console.log(nextProps, nextState); // 目标要修改的props和state
// console.log(this.props, this.state); // 还未修改原本的props和state
// return !this.state.carName === nextState.carName;
// }
render() {
console.log("parent render");
return (
<div className="parent">
<h1>Parent</h1>
<p>我的车是:{this.state.carName}</p>
<button onClick={this.changeCar}>点击换车</button>
<Child/>
</div>
)
}
}
class Child extends PureComponent {
// shouldComponentUpdate(nextProps){
// return !this.props.carName === nextProps.carName
// }
render() {
console.log("child render");
return (
<div className="child">
<h2>Child</h2>
{/* <p>父亲的车是:{this.props.carName}</p> */}
</div>
)
}
}
6. 父子组件
不多说,直接上代码:
import React, { Component, PureComponent } from 'react'
import "./index.css"
export default class Parent extends PureComponent {
state = {carName: "奔驰"}
changeCar = ()=>{
this.setState(state=>({carName:"迈巴赫"}));
}
render() {
console.log("parent render");
return (
<div className="parent">
<h1>Parent</h1>
<p>我的车是:{this.state.carName}</p>
{/* A组件 与 B组件 形成父子组件的第二种方法 */}
{/* <A>
<B/>
</A> */}
{/* 类似于 Vue的插槽 */}
<A render={(name)=><B name={name}/>}/>
</div>
)
}
}
class A extends PureComponent {
render() {
console.log("A render");
return (
<div className="a">
<h2>A</h2>
{/* A组件 与 B组件 形成父子组件的第一种方式 */}
{/* <B/> */}
{/* {this.props.children} */}
{this.props.render("Tom")}
</div>
)
}
}
class B extends PureComponent {
render() {
console.log("B render");
return (
<div className="b">
<h2>B</h2>
<p>{this.props.name}</p>
</div>
)
}
}
7. ErrorBoundary
当你的组件存在父子组件关系时,如果说你的子组件出现了错误,那么会导致父组件一并崩掉,那有没有什么办法,可以把错误控制在一个组件里,不让他扩散呢?
答案是有的,有两个函数:
getDerivedStateFromError(error)
componentDidCatch(error, info)
上代码:
import React, { Component, Fragment } from 'react'
import Child from "./Child";
// 错误边界即把组件的错误信息控制在一个组件中,不使他扩散而导致程序崩溃
export default class Person extends Component {
state = {
hasError: "", // 用于标识子组件是否产生错误
}
// 当子组件发生错误时会触发该生命周期函数,且参数为错误信息
// 只适用于生产环境,只能捕获后代组件生命周期产生的错误
static getDerivedStateFromError(error){
// 一般用于处理错误出现时返回给用户展示的东西
console.log("出错了");
console.log(error);
}
// 组件渲染过程中出错就会触发该生命周期函数
componentDidCatch(error, info){
// 一般用于统计错误,反馈给雾浮起,用于通知程序员进行bug修改
console.log("渲染组件出错");
console.log(error, info)
}
render() {
return (
<Fragment>
<h2>我是Parent组件</h2>
{
this.state.hasError ?
<h2>当前网络不大行,建议买高级网络套餐好吧</h2> :
<Child/>
}
</Fragment>
)
}
}
8. 组件通信方式总结
- 组件之间的关系:
- 父子组件
- 兄弟组件
- 祖孙组件(跨级组件)
- 几种通信方式:
props
- children props
- render props
- 消息订阅
pubsub
,event
- 集中式管理
redux
,dva
,react-redux
- conText 生产者, 消费者模式
- 较好的搭配方式:
- 父子组件:
props
- 兄弟组件: 消息订阅发布,集中式管理
- 祖孙组件:消息订阅发布,集中式管理,
conText
- 父子组件:
十三、最后
这些就是我学习React的一些学习笔记了,日后如果还有其他的内容的话应该会深究后写成单独的文章了。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!