学习来源 B站尚硅谷的课程
react
概述
用于构建用户界面的js库
react库的组成
npm info react
create-react-app --version
create-react-app -V
0-1. JSX语法 jsx语法是 一种react描述 组件html结构的开发语法 主要要求如下:
定义DOM元素时候,不能加引号
标签内混入JS表达式要用 { }
样式类名不能使用class关键字,而是使用className
不管是函数组件还是类组件,返回jsx结构只能有一个根标签
标签不管是双标签闭合,还是自闭合,必须闭合
组件标签 必须使用大写字母开头
小写字母开头标签,会自动查找html对应标签,如果没有就会报错
区别与传统js,事件使用驼峰命名,onclick => onClick
prop-types 通过静态引入
propTypes 通过包安装
import PropTypes from 'prop-types';
//接收props组件内
export default class MyCmp extends Component {
static propTypes = {
xxx: PropTypes.func.isRequired // 传递至MyCmp组件上的属性xxx 类型为函数,且必填
}
static defaultProps = {
xxx: 123 //属性缺省值配置
}
}
函数式组件 定义函数返回一个结构,然后将这个结果的jsx,返回的虚拟DOM转化为真实DOM,渲染至容器DOM节点上
function Cmp1(){
return 我是一个组件
}
ReactDOM.render( ,document.getElementById('root'));
3-1. 函数式组件使用props
function Person(props){
console.log(props)
return hello world 类型{typeof props.myname} - {props.myname}
}
ReactDOM.render(
,
document.getElementById('root')
);
HOOKS
react 16.8 推出的语法新特性
常用的hook有:
react.useState -- 赋予函数式组件操作state的能力
react.useEffect -- 赋予函数式组件使用生命周期 ( componentDidMount, componentDidUpdate, componentWillUnmount )
react.useRef -- 赋予函数式组件操作元素节点的能力
3-2. 通过react.useState的hook,使得函数式组件拥有操作state的能力
import React from 'react'
import ReactDOM from 'react-dom'
function Funcmp(props){
//格式为数组,结构后,第一项为state,第二项为方法
// 修改一般数据
let [ num,changeNum ] = React.useState({ num:0 })
let [sName,changeName] = React.useState('zhangsan');
//方式一,对象式修改数据方法
function addFn(){
changeNum(num+1);
};
//修改对象数据
let [sNum,changeNum] = React.useState({num1:1,num2:1});
function changeNumFn1(){
//调用操作state的方法
changeNum({...sNum,num1:sNum.num1+1});
//方式二,函数式来修改数据方法
//函数的参数就是 state数据
changeNum((data)=>{
// console.log(data); //{num1: 1, num2: 1}
return {...data,num2:data.num2*2}
});
};
function changeNumFn2(){
//调用操作state的方法
changeNum({...sNum,num2:sNum.num2*2});
};
return (
当前的数值 { num } , { sNum.num1 }--{ sNum.num2 }
点击我改变数值1
点击我改变数值2
)
};
3-3. 通过react.useEffect的hook,使得函数式组件拥有操作生命周期的能力 语法结构参数不同,可能出现的生命周期
react.useEffect(()=>{
console.log(1111);
return (()=>{
//此处为componentWillUnmount
})
},[ ])
如果react.useEffect的第二个参数为空数组,则函数内1111只会触发一次,此处为componentDidMount
如果react.useEffect的第二个参数没有,则函数内1111会触发1+N次,此处为componentDidMount+componentDidUpdate
如果react.useEffect的第二个参数为数组内有一项或以上,则针对这些项,为componentDidMount+componentDidUpdate 针对这些项目以外的项,为componentDidMount
react.useEffect第一个参数的返回的函数,为componentWillUnmount
3-4. 通过React.useRef 赋予函数式组件,通过ref获取DOM节点的能力
import React from 'react'
export default function Funcmp3() {
//设置state数据
let [inputVal,changeMes] = React.useState('');
let inputRef = React.useRef();
//通过ref获取DOM元素的value值
function showMes(){
console.log(inputRef.current.value);
}
//表单改变state
function changeMesFn(event){
changeMes(event.target.value)
}
return (
点击获取输入框内容
)
}
类组件
// 类组件继承了React组件上的属性与方法,其中三大属性为 state props refs
class MyCmp extends React.Component {
render (){
return ( 我是类组件 )
}
}
ReactDOM.render( ,document.getElementById('root'))
4-2. 关于类组件的一些注意事项
组件中的render方法内的,this指向为组件实例
组件自定义方法中this为undefined,如何解决 方法1: 通过在constructor内,通过bind修改this的指向 方法2: 在constructor外,通过变量和箭头函数的方式,修改this指向
状态数据,不能直接修改或者更新, 如:this.state.xxx = xxx, 必须要使用 setState方法,如果修改数据为对象要创建新对象(object、array),详见 下面第7条
类组件 三大属性 state
什么情况下使用state, 类组件如果需要维护一个自身的状态,可以使用,函数组件没有这个属性
复杂的类组件可以使用, 简单的类组件不需要一个state
state定义的位置 方式1:可以在constructor内定义,如:
class MyCmp extends React.Component {
constructor(props){
super(props)
this.state = { somedata: 100 }
}
render (){
let { somedata } = this.state;
return ( 我是类组件 数据为 { somedata } )
}
}
方式2:在contructor外定义,如:
class MyCmp extends React.Component {
//contructor 不是必写
state = { somedata: 100 }
render (){
let { somedata } = this.state;
return ( 我是类组件 数据为 { somedata } )
}
}
修改state,不能直接赋值,而是需要调用setState方法
通过事件,如onClick调用一个事件处理函数,来调用setState方法
关于事件处理函数放置的位置也存在两种 方式一,放在constructor内,值得注意的是需要修改this指向
constructor(props){
super(props) //写constructor必须写super(props)
this.eventHandle = this.eventHandle.bind(this);
}
eventHandle(){
dosomething...
}
方式二,放在constructor外面 写成变量赋值箭头函数的形式,this会因为箭头函数自动指向实例对象
constructor(props){
super(props)
...
}
eventHandle = () => {
dosomething...
}
关于setState的注意事项 **React遵循函数式编程规范。在函数式编程中,不推荐直接修改原始数据。 ** 如果要改变或更改数据,则必须复制数据副本来更改。 所以,函数式编程中总是生成原始数据的转换副本,而不是直接更改原始数据。
纯函数的特点: 第一,同一个输入对应同一个输出 第二,函数内部不能使用不可预测的操作,如 网络请求、输入输出设备 第三,函数内部不能使用 Date.now() 与 Math.random()
什么情况下,需要在组件内使用state? 某一个组件自己使用的,则state放在这个组件下 某一些组件用一个数据,则state放在它们共同的父级 (官方称之为 状态提升
)
要修改的state对象下,如果键名对应键值为一个字符串,可以直接通过setState修改 如:
class xxx extends React.Component {
state = { isActive: false }
...
mouseEnterFn = ()=> {
this.setState({
'isActive': true
})
}
}
要修改的state对象下,如果键名对应键值为一个对象(object,array),那么不能直接修改原来的数据,而要生成一个新的对象
原理: react底层进行了一个浅拷贝,对比前后修改的对象是否是同一个对象(至于对象内的值是否改变无关紧要), 只关心引用地址值
如 数组的修改:
class xxx extends React.Component {
state = {
list:[]
}
changeList = ()=> {
let dataArr = this.state.list;
let NewList = [];
//方式一,通过concat返回一个新数组,而不能直接用push等方法直接在dataArr上操作
NewList = dataArr.concat([xxxx])
//方式二,通过展开运算符来返回新数组,并按照意愿调整新增数据的位置(插入第一个,或者插入最后一个)
NewList = [xxxx , ...dataArr]
NewList = [...dataArr , xxxx]
//方式三,通过数组map方法返回新数组,一般用于处理数据或者筛选数据
NewList = dataArr.map((item)=>{ return item*2; })
NewList = dataArr.map((item)=>{ if(item%2==0){ return item; } })
//方式四,通过filter来返回新数组
//注意:filter() 方法创建一个新的数组; filter() 不会对空数组进行检测; filter() 不会改变原始数组;
NewList = dataArr.filter((item)=>{ return item%2==0; })
//方式五,forEach遍历原来数组,根据条件,将筛选项插入新数组内
dataArr.forEach((item)=>{ if(item%2==0){ NewList.push(item); } })
this.setState({
list: NewList
})
}
}
要修改的state对象下,如果键名对应键值为一个对象(object,array),那么不能直接修改原来的数据,而要生成一个新的对象 如 对象的修改:
class xxx extends React.Component {
state = {
obj: { a:1, b:2 }
}
changeList = ()=> {
let dataObj = this.state.obj;
//方式一,通过assign返回一个新的对象( 属于浅拷贝 ),并修改对象内的数据
dataObj = Object.assign({ },dataObj,{a:10,b:20})
//方式二,展开运算符拷贝和修改数据
dataObj = { ...dataObj, a:10 }
this.setState({
obj: NewObj
})
}
}
setState是异步的,设置state是同步操作,但是渲染是异步操作 通过setState的第二个参数可以获取最新的state数值,这个函数会在页面render后触发
import React, { Component } from 'react'
class Count extends Component {
state = {
num:0
}
addNum = () =>{
var oldNum = this.state.num;
this.setState({
num: oldNum+1
},function(){
console.log(this.state.num); //1
})
console.log(this.state.num); //0
}
}
setState不仅仅可以接收对象,也可以接收函数 setState函数形式的格式: this.setState(( oldState )=>{ return xxxx }) 此外这种写法也支持第二个参数,即render后的callback函数 对象形式写法是函数形式写法的语法糖
//-- 对象形式
this.setState({
num: this.state.num+1
});
//-- 函数形式 (最终函数return出来的依然是一个对象)
this.setState((oldstate)=>{
return { num: oldstate.num+1 };
},function(){
console.log(this.state.num);
})
写法推荐的建议: 如果不需要依赖之前的值,优先写对象形式 如果依赖之前的值,可以写函数形式,这样之前的值就不用再保存到一个变量内了,直接从函数参数里拿即可
类组件 三大属性 props
props - 通过组件标签挂载的属性,使得父组件可以将数据或者方法传递至子组件中 子组件通过 this.props 来获取父级组件的属性以及方法
组件之间通信的方式之一
函数类型组件也是可以获取到props的
function Person(props){
console.log(props)
return hello world 类型{typeof props.myname} - {props.myname}
}
ReactDOM.render(
,
document.getElementById('root')
);
react-router-dom,操作路由 其中 this.props.match.params -- params 风格理由配置 this.props.history.search -- query 风格 路由配置 this.props.history.state -- state 风格 理由配置 都是挂载在props属性上面
类组件 三大属性 refs 用来获取元素节点 一共有四种方式来获取元素节点
方式一: 字符串类型获取 (存在效率问题,未来可能会被移除,官方不建议使用)
class xxx extends React.Component {
doSearch = ()=> {
let _searchInput = this.refs.searchInput;
console.log(_searchInput.value);
}
render (){
return
}
}
方式二: ref等于函数,在标签内写行内函数,函数参数为当前DOM节点,通过箭头函数,将this指向组件实例
class xxx extends React.Component {
doSearch = ()=> {
let _searchInput = this.searchInput;
console.log(_searchInput.value);
}
render (){
return { this.searchInput = currentNode}} placeholder="输入搜索内容" />
}
}
方式三: ref等于函数,不将函数写在行内,而是在组件内定义,函数的参数为操作的DOM节点
class xxx extends React.Component {
doSearch = (currentNode)=> {
this.searchInput = currentNode;
console.log(this.searchInput.value);
}
render (){
return
}
}
※ 相较于行内方式,更新数据会被调用两次,第一次用于重置ref标签数据为null,第二次为正常获取DOM节点 ※ 官方的描述,这个属于正常现象,写内联箭头函数,或者是调用类组件内的函数,都属于正常现象 ※ 标签内写箭头函数还是一种主流方式
方式四: ※ 通过类上挂载属性,通过React.createRef聊创建容器,用来储存ref的元素节点 ※ 容器为元素一一对应,不能混用 ※ 流程:创建ref数据容器,元素标签上ref绑定这个容器,
class xxx extends React.Component {
searchInputRef = React.createRef()
doSearch = ()=> {
console.log(this.searchInputRef.current.value);
}
render (){
return
}
}
绑定事件的一些注意点
更好的兼容性:事件是react合成的自定义事件,为了更加好的浏览器兼容性,例如: onclick -> onClick
更加高效:react通过事件委托的方式,将绑定事件元素,提升到父级容器
不能过渡使用ref,如果可能,能不用就不用 例如: 获取表单的数据,不要使用ref获取表单元素节点,而是通过事件的event对象,获取表单内容 受控组件 -> 非受控组件
class xxx extends React.Component {
doSearch = (event)=> {
console.log(event.target.value);
}
render (){
return
}
}
关于事件处理函数传参的问题 原理: ※ 通过函数柯里化,或者说是高阶函数,一个函数执行后返回另一个函数,来实现参数传递 react函数如果带参数,会立即执行,这个不是react要的,它需要一个函数, 因此借助高阶函数来实现函数传参
※ 常见的高阶函数有:promise、setTimeout、array.map等
方式一,标签内部自执行一个匿名函数,并将参数event,传递至下一个函数
class xxx extends React.Component {
doSearch = (event,data)=> {
console.log(event.target.value);
console.log(data); //somedata....
}
render (){
return { this.doSearch(event,'somedata....') }} placeholder="输入搜索内容" />
}
}
方式二,正常调用函数,在函数内部,通过箭头函数自执行,返回对应的event
class xxx extends React.Component {
doSearch = (data)=> {
return (event)=>{
console.log(event.target.value);
console.log(data); //somedata....
}
}
render (){
return
}
}
受控组件与非受控组件 简而言之就是输入框,没有绑定onChange或onInput、onKeyUp等类似控制 以复选框为例
//如果没有绑定事件onChange存在,那么它将不能改变
//react会建议使用defaultChecked 代替 checked,但它仅会在页面渲染的第一次受到数据影响
//Warning: You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.
改写为
生命周期
一些概念: 挂载组件(mount),卸载组件(unmount)
生命周期调用次数: componentDidMount - 组件挂载,调用1次 componentWillUnmount - 组件更新,调用N次
一个组件开启定时器与关闭定时器的时机
constructor(props){
super(props)
this.state = { ... }
}
componentWillMount(){
//组件将要挂载....
}
render(){
return (
...
)
}
componentDidMount(){
this.iTmer = setInterval(()=>{ ... },1000);
}
componentWillUnmount(){
clearInterval(this.iTimer);
}
shouldComponentUpdate(){
//需要返回布尔值,组件是否允许更新
return true
}
componentWillUpdate(){
}
componentDidUpdate(preProps,preState){
//preProps 更新之前的props
//preState 更新之前的state
}
13-1. react新旧版本下,生命周期的差异
componentWillMount -> 使用需要加上 UNSAFE_ 前缀
componentWillReceiveProps -> 使用需要加上 UNSAFE_ 前缀
componentWillUpdate -> 使用需要加上 UNSAFE_ 前缀
getDerivedStateFromProps
-> 静态方法 前面需要加上 static
-> 通过组件标签挂载属性,在这个生命周期参数可以获取,返回值可以左右组件内的state
-> 必须有返回值,可以返回null
-> 会导致代码冗余、会使组件难以维护
-> 若state值都取决于props,可以使用这个生命周期
getSnapshotBeforeUpdate
-> 可以带两个参数 prevProps, prevState
-> 必须有返回值,可以为null; snapshot值可以为任意,会传递至 componentDidUpdate
-> 会将至传递给 componentDidUpdate的第三个参数
-> 使用场景极其罕见
虚拟DOM与DIFF ※ key的意义? react数据更新 新的虚拟DOM与旧的虚拟DOM,通过DIFF算法进行比对,针对变化的地方进行更新,提高DOM复用 有key的时候,diff会优先,对相同结构树下同key结构进行比对 如果没有key,判断是新元素,会重新创建DOM元素,降低性能与复用率
※ 使用索引作为key有什么后果?
如果单纯的展示,且新插入数据为正序排序(新插入数据在后面),没有影响排序的行为,则没有问题
如果新数据插入在列表最前面,会影响所有列表项,所有的元素都会重新创建,且可能造成布局错乱
※ 常规做法:
使用唯一id作为key,而不是索引
使用三方包,如 nanoid,来生成唯一的key
npm install nanoid -S
import {nanoid} from 'nanoid'
//返回为函数,调用即可
console.log(nanoid());
组件之间通信手段 ※ 方式一:props传递函数 父组件通过向子组件上挂载方法,子组件通过props来获取方法,并调用函数,将传递数据作为参数传递至父组件
父组件:
fatherFn = (sonData)=> {
console.log('子组件数据:', sonData); //123
}
render() {
return (
{/* 顶部 一般组件 */}
)
}
子组件:
render(){
let {fatherFn} = this.props;
return (
{/* 注意传参要包裹一个自执行匿名箭头函数,否则返回的不是函数,因为react会默认调用带()的函数 */}
{ fatherFn('123') }}>通信
)
}
※ 方式二:第三方包 'pubsub-js' ,通过发布与订阅来实现任意位置组件之间通信 安装 npm install pubsub-js -S
组件订阅(位置:生命周期的组件挂载)
componentDidMount(){
//订阅信息
this.subscribeObj1 = PubSub.subscribe('mydata', function(msg, data){
console.log( msg, data );
// msg - 'mydata' , data - '发布的一段信息...'
});
}
组件解绑订阅(位置:生命周期的组件卸载)
componentWillUnmount(){
//取消订阅
PubSub.unsubscribe(this.subscribeObj1);
}
组件发布 位置
//发布一个信息
pubFn = ()=>{
PubSub.publish('mydata', '发布的一段信息...');
}
render(){
return (
{/* 发布一个信息 */}
发布信息
)
}
※ 方式三:redux、react-redux 管理数据共享 redux 第三方数据状态管理 react-redux 官方出品,与第三方包redux 搭配使用 见 (下面 ↓) redux与react-redux部分
※ 方式四:通过React.createContext来创建上下文 这个目前学习感觉像一种单向的数据通信,方便祖辈传递信息和同步信息至子孙组件
第一步,创建一个Context文件夹用于创建上下文的文件。
import React from 'react'
//这个是一个上下文的对象,名称为'MyContext'
export const MyContext = React.createContext();
export const { Provider,Consumer } = MyContext;
第二步,在祖先组件中使用Provider包裹需要祖先数据的组件
import { Provider } from './components/Context'
传递数据至Head组件
特别注意:
Provider上的value属性名称,是固定的不能修改,否则报错 Did you misspell it or forget to pass it?
第三步,方法1 - 在后代组件中获取传递的数据
import { Consumer } from './components/Context'
export default function Funcmp3() {
return(
{
(value999999)=>{
return `姓名:${value999999.name} - ${value999999.age}`
}
}
)
}
注:
在Consumer内的参数可以自行定义,不用担心报错,如上‘value999999’
使用Consumer既可以在函数式组件,也可以在类组件内使用
第三步,方法2 - 仅子组件为类组件时可以使用
//引入上下文对象
import { MyContext} from '../Context'
export default class Footer extends Component {
//表名该组件需要通过上下文接收数据
static contextType = MyContext
render(){
return (
姓名:{this.context.name} - {this.context.age}
)
}
}
注:
类组件内 添加静态属性,标明需要接收上下文数据 static contextType = MyContext
在实例的context上获取传递的数据
react-cli react 快速开发脚手架,包含 语法检查、jsx编译、devserver热更新等..
安装脚手架 npm install create-react-app -g
创建项目 create-react-app 项目名称
启动项目 npm start
模块化引用样式,防止样式污染
//index.css -> index.module.css --- 固定的格式变化
import css1 from './index.module.css'
export default class MyCmp extends Component {
render(){
return (
标题
)
}
}
关于调试项目,如何使用代理,调用远程服务器接口 ※ 方式一: 在package.json中添加配置
"proxy": "http://localhost:5000" // 一个远程服务器的地址,与你的本地服务器存在跨域
※ 方式二: 在src文件夹内,创建一个新文件 setupProxy.js
//内置包,无需安装
const proxy = require('http-proxy-middleware');
module.exports = function(app){
app.use(
proxy('/api',{ //规则关键词,如果存在接口 /api/list
target:'http://localhost:5000', //请求后台服务器接口的地址
changeOrigin: true, //是否伪装地址,如果不伪装则为当前静态页面的地址,否则为 上面请求服务器的地址
pathRewrite: {'^/api':''} //请求服务器接口会替换为 http://localhost:5000/list
})
)
}
上面代理配置完了,前端页面请求数据
getDetalFn = (id)=> {
//请求数据
let that = this;
//请求地址不能写远程地址,要写本地服务器地址
//真实服务器接收到数据的request.path为 http://localhost:5000/detail/xxx ,将规则标识api替换为空
axios.get(`http://localhost:3000/api/detail/${id}`)
.then(function (response) {
let _cont = response.data;
that.setState({
'cont':_cont
})
})
.catch(function (error) {
// handle error
console.log(error);
})
}
相对于之家在package.json内配置代理,可以配置多个代理,规定代理的触发关键词
react-router
安装 npm install react-router-dom -S
概念
单页面应用 (SPA - single page web application)
这个页面切换hash或者切换history(H5),来调用不同的组件,渲染更新页面上的不同区域
数据通过ajax请求获取,不刷新页面
路由的理解
路由的分类
后端路由 router.get(path,function(req,res){ ... })
前端路由 react中,切换path显示component,
路由内的组件
- 基础的路由按钮
- 可以有激活状态的路由按钮,默认有一个active 类名,可以通过activeClassName配置
- 路由配置,映射理由与组件的关系
- 匹配第一个相符的路由映射,对于后面的不再匹配筛选
- 重定向,放在路由配置的最下面,作为逻辑兜底
- history风格路由
- hash路由
4-2. BrowserRouter与HashRouter的区别:
底层原理: BrowserRouter 是H5的history API,不兼容IE9及以下 ( 提出业务需求时候,明确低版本浏览器不支持 ) HashRouter 是使用URL的哈希值 (#后的部分,不会发送至服务器)
path表现形式不一样 BrowserRouter 是 localhost:3000/test HashRouter 是 localhost:3000/#/test
浏览器刷新 BrowserRouter 没有任何影响,因为state存在history内 HashRouter 刷新导致state参数丢失
HashRouter 可以解决引用静态文件路径错误问题
建议: 优先考虑 BrowserRouter (不考虑浏览器兼容的情况下) 如果使用 HashRouter 会造成state方式配置路由错误,因为没有底层history记录state数据,刷新为undefined,可以使用params或search风格路由
配置路由
import {NavLink,Link,Route,Switch} from 'react-router-dom';
import About from './compontents/About'
import News from './compontents/News'
关于
新闻
{/* About */}
{/* News */}
{/* 默认路径,兜底 */}
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import App from './App';
ReactDOM.render(
,
document.getElementById('root')
);
路由嵌套
{/* 二级路由 */}
热点新闻
今日新闻
{/* 呈现区域 */}
路由的细节
home页面
home页面
结论: to可以包含path,且必须开头匹配,如果不匹配就失败了,这个方式属于模糊匹配
路由传参
※ 方式一 search方式 调用:
配置:
//--编程式导航
this.props.history.push({ path:'/news/today',query:{id:123} });
获取:
this.props.location.search
注:需要借助于querystring来解析地址,删除首字母?,并使用querystring.parse()来将字符串转化为对象
※ 方式二 params方式 调用:
配置:
//--编程式导航
this.props.history.push( '/news/today/123' );
获取:
this.props.match.params
※ 方式三 state方式 调用:
配置:
//--编程式导航
this.props.history.push({ path:'/news/today',state:{id:123} });
获取:
this.props.location.state
注:刷新也可以保留参数,但是如果清空浏览器缓存,state会消失,需要考虑调用undefined下属性的情况
7-1. 路由组件与一般组件
路由组件不是自己通过标签写入页面,而是通过路由配置,让路由自己去调用
一般组件需要自己去引用组件,添加到页面,无法调用this.props.history等
一般组件转化为路由组件,通过withRouter包裹即可
import {withRouter} from 'react-router-dom'
class Head extends Component {
...
}
export default withRouter(Head);
使用params进行路由传参时候,会遇到一个情况,就是无限调用render 情景: 列表菜单与列表详情在同一个页面情况下, 根据params传递参数ID,componentDidMount来请求服务器,获取数据,然后进行setState操作, 如果点击列表菜单,再次进行操作,无限陷入render情况 解决: componentDidUpdate生命周期,进行前后params的id比对,不同情况下,再次进行数据请求
componentDidUpdate(prevProps, provState, snapshot){
let {id} = this.props.match.params;
if(prevProps.match.params.id!==this.props.match.params.id){
this.getDetalFn(id);
};
}
原因: componentDidMount调用函数中setState触发了更新componentDidUpdate,更新componentDidUpdate调用setState调用函数中setState又触发了componentDidUpdate,进而componentDidUpdate无限自己调用自己
封装一个NavLink,将常用的属性封装组件内,放置反复写
import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
import './index.css'
{/* 注意 {...this.props} 放在默认属性的右侧,这样自定义属性与默认属性同时存在,自定义会覆盖默认*/}
export default class MyNavLink extends Component {
render() {
return (
)
}
}
新闻 //使用默认的属性
热点新闻 //使用部分自定义属性
注意: MyNavLink组件在定义时候,标签体内容在this.props.children内( 标签体内容为特殊的属性 ),所以一并与其他属性一起解构即可
redux
安装 npm install redux -S
概述:
状态管理的JS库,非react官方
可以配合前端三大框架使用,但基本与react配合使用
集中管理react应用中多个组件共享 的状态
使用场景:
某个组件状态与其他组件一起用
一个组件需要改变另外一个组件的状态
使用原则,能不用就不用,非用不可才用
目录结构建议 同components级目录下,创建redux文件夹,里面放置
store.js
action 文件夹
reducer 文件夹
type类型的常量.js (可有可无,方便多文件内修改action的type,放置输入错误的情况)
原理图
Action Creators 创建函数返回一个对象,对象的格式为
// +data操作的action
export const addAction = (data)=>{
return {
type: 'add',
data: data*1
}
}
// -data操作的action
export const redAction = (data)=>{
return {
type: 'redu',
data: data*1
}
}
//初始值
let initState = 0;
export default function reduceFn(preState=initState,action){
switch(action.type){
case 'add':
return preState+action.data;
case 'redu':
return preState-action.data;
default:
return preState;
}
};
import {createStore} from 'redux'
import countReducer from './count_reducer'
export default createStore(countReducer)
//-- 派发一个+10的操作
store.dispatch(addAction(10));
//-- 派发一个减20的操作
store.dispatch(redAction(20));
Store 内的数据变化时候,强制调用render更新视图 ( setState一个空对象 )
store.subscribe(function(){
that.setState({}); //强制触发render视图(当store内的数据变化时候)
})
也可以对整个App来做监听状态变化的刷新(index.js)
ReactDOM.render( ,document.getElementById('root'));
//监听状态变化后,刷新App
store.subscribe(()=>{
ReactDOM.render( ,document.getElementById('root'));
});
store.getState(xxx); // store获取数据
redux库的基本构成
Action Creator - 接收组件请求,创建一个动作,将需要做的事儿,dispatch给Store
Store - 将Action Creator的要求发送(之前的状态以及ActionCreator传递的事儿)给 Reducers
Reducers - 处理数据,返回新的数据(初始化数据(之前的状态为undefined),与更新数据)
React Component - 通过getState从Store请求获取新数据,以及发送请求给 Action Creator
React Component 相当于客户
Action Creator 相当于服务员 初始化时候 dispatch(action) action的{type:@@init@@} data不传递,只传递type
Store 相当于餐厅经理
Reducers 相当于厨师
客户 -> 服务员 -> 经理 -> 厨师 -> 经理 -> 客户
关于异步action如果不像放在组件内,而想放在交给action中 由于store必须dispatch一个普通对象,而非函数,所以需要借助一个中间件。
需要借助于第三方库 redux-thunk
安装 npm install redux-thunk
步骤1,action创建函数不返回对象,返回一个函数,函数内容部调用同步action
步骤2,store中通过applyMiddleware包裹thunk(三方包)来作为createStore的第二个参数
步骤3,在组件事件处理函数中,直接用store派发action, 如:store.dispatch(addActionAsync(数据,延迟时间))
store.js ( 通过applyMiddleware载入中间件 )
import {createStore,applyMiddleware} from 'redux'
import countReducer from './count_reducer'
import thunk from 'redux-thunk'
export default createStore(countReducer,applyMiddleware(thunk))
/*
* 根据不同逻辑,返回一个action对象 ,或者一个异步函数(需要借助于三方库 redux-thunk)
* 对象 { type:'xxx', data:'xxxx' }
* 函数 function(){ return action调用 }
*/
import {ADD,RED} from './const_vars'
import store from './store'
export const addAction = (data)=>{
return {
type: ADD,
data: data*1
}
}
export const redAction = (data)=>{
return {
type: RED,
data: data*1
}
}
export const addActionAsync = (data,time)=>{
return ()=>{
setTimeout(()=>{
store.dispatch(addAction(data));
},time)
}
}
import React, { Component } from 'react'
import store from '../../redux/store'
import {addAction,redAction,addActionAsync} from '../../redux/count_action'
export default class Count extends Component {
// 事件处理函数
dealFn = (typeName)=> {
let _val = this.selectEle.value;
if(typeName===ADD){
store.dispatch(addAction(_val));
}else if(typeName===RED){
store.dispatch(redAction(_val));
}else if(typeName===ADDIFEVEN){
let nowVal = store.getState();
if(nowVal%2!==0){
store.dispatch(addAction(_val));
}
}else if(typeName===ADDASYNC){
//一个借助于redux-thunk的异步action
store.dispatch(addActionAsync(_val,1000))
}
}
render() {
return (
计算后的值:{store.getState()}
{ this.selectEle = c }}>
1
2
3
{/* 简化 */}
{ this.dealFn('add') }}>加
{ this.dealFn('red') }}>减
{ this.dealFn('addIfEven') }}>奇数加
{ this.dealFn('addAsync') }}>异步加
)
}
}
react-redux
特点
react-redux官方提供,目的是减少对store频繁直接调用,如 store.getState、store.dispatch、store.subscribe,等一些列操作都放在container容器上,通过容器来做这一些列操作
从react-redux中导入connect,将UI组件,state、dispatch方法导入到container容器
将store创立的仓库,挂载到容器标签上
UI组件内的数据通过属性获取(mapStateToProps),修改状态的方法(mapDispatchToProps)同样通过属性获取
store与reducer的使用,同使用redux没有什么差别,主要工作在UI组件内不停的调用属性
原理图:
总结点:
使用流程 第一步,安装react-redux, 同component文件夹,创建一个容器文件夹,创建一个容器文件 如:Count.js
//引入Connect
import { connect } from 'react-redux'
//引入UI
import CountUI from '../components/Count'
//引入Action
import { addAction,redAction,addActionAsync } from '../redux/count_action'
//mapStateToProps 返回对象
function mapStateToProps(state){
return {count:state}
}
//mapDispatchToProps 返回对象,对象内是方法
function mapDispatchToProps(dispatch){
return {
ADDFn: (val)=>{ dispatch(addAction(val)) },
REDFn: (val)=>{ dispatch(redAction(val)) },
ADDASYNC: (val,time)=>{ dispatch(addActionAsync(val,time)) }
}
}
export default connect(mapStateToProps,mapDispatchToProps)(CountUI);
第二步,在UI组件内使用,通过属性props传递过来的属性 state 与 dispatch
import React, { Component } from 'react'
import {ADD,RED,ADDIFEVEN,ADDASYNC} from '../../redux/const_vars'
export default class Count extends Component {
dealFn = (typeName)=> {
let _val = this.selectEle.value;
if(typeName===ADD){
//通过属性获取修改状态的方法
this.props.ADDFn(_val)
}else if(typeName===RED){
this.props.REDFn(_val)
}else if(typeName===ADDIFEVEN){
let nowVal = this.props.count;
if(nowVal%2!==0){
this.props.ADDFn(_val)
}
}else if(typeName===ADDASYNC){
this.props.ADDASYNC(_val,1000);
}
};
render() {
return (
{/*通过属性获取状态*/}
计算后的值:{this.props.count}
{ this.selectEle = c }}>
1
2
3
{/* 简化 */}
{ this.dealFn('add') }}>加
{ this.dealFn('red') }}>减
{ this.dealFn('addIfEven') }}>奇数加
{ this.dealFn('addAsync') }}>异步加
)
}
}
//引入Connect
import { connect } from 'react-redux'
//引入UI
import CountUI from '../components/Count'
//引入Action
import { addAction,redAction,addActionAsync } from '../redux/count_action'
/*
* 状态对应一个对象 ( count的命名,可以根据reducer内的处理来语义化命名 )
* 修改状态方法也对应一个对象 ( 返回对应创建action的方法即可,内部的逻辑由react-redux来处理 )
*/
export default connect(
state => ({count: state}),
{
ADDFn: addAction,
REDFn: redAction,
ADDASYNC: addActionAsync
}
)(CountUI);
优化容器的store挂载 将所有容器组件上挂载的store,都统一挂载到react-redux的Provider组件上,用Provider包裹App组件,以实现一次性完成对所有容器组件的store挂载
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './redux/store'
import { Provider } from 'react-redux'
ReactDOM.render(
,
document.getElementById('root')
);
import React, { Component } from 'react'
//引入Connect
import { connect } from 'react-redux'
//引入Action
import { addAction } from '../redux/count_action'
class CountUI extends Component {
// 简化
dealFn = (typeName)=> {
let _val = this.selectEle.value;
if(typeName==='add'){
this.props.ADDFn(_val)
}
}
render() {
return (
计算后的值:{this.props.count}
{ this.selectEle = c }}>
1
2
3
{/* 简化 */}
{ this.dealFn('add') }}>加
)
}
};
// -- 简写方式
export default connect(
state => ({count: state}),
{
ADDFn: addAction
}
)(CountUI);
多个store数据 使用combineReducers可以将多个reducer整理一起,用一个key:value的形式保存
import {createStore,applyMiddleware,combineReducers } from 'redux'
import thunk from 'redux-thunk'
import countReducer from './reducers/count'
import personReducer from './reducers/person'
/*reducers列表*/
let allReducer = combineReducers({
num: countReducer,
person: personReducer
})
export default createStore(allReducer,applyMiddleware(thunk))
在容器组件内,同过key值来获取每个reducer的状态值 如:
// -- 简写方式
export default connect(
state => ({
num: state.num,
person: state.person
}),
{ ...改变状态的action的key、value对儿 }
)(CountUI);
优化以及细节工作
组件按需加载 ( Lazy,Suspense都来自与react核心库,无需二次安装 )
import React, { Component,Lazy,Suspense } from 'react'
import { Switch,Route } from 'react-router-dom'
//按需引入组件
const About = Lazy(()=>{ import('@/component/About') });
//配置路由
LOADING....}>
由于React的jsx语法限制,不管是类组件还是函数式组件,返回的结构只能有一个根节点,而且这个节点会被渲染到页面中,无形中添加了很多无意义的div标签 解决方法 使用Fragment组件来替代一般html标签
import React,{Fragment} from 'react'
export default function Funcmp2(props) {
return (
{/* 这个类名为aaa的标签,不会被渲染到页面上 */}
这是一个函数自增长的函数类型组件 {num}
通过React.useEffect来开启一个定时器
删除这个组件
)
};
额外说明:
可以使用<> xx >代替Fragment
Fragment组件上,只能添加一个key属性,否则会报错 Invalid prop "name" supplied to "React.Fragment". React.Fragment can only have "key" and "children" props.
减少不必要的组件render 问题1: 父子组件嵌套,子组件没有引用父组件要修改的数据,但是父组件setState改变数据,会触发render,连带子组件也都一并被render了,因为组件的shouldComponentUpdate默认返回的布尔值为true 问题2: this.setState({}); 设置一个空对象,也会触发render
解决思路:
通过shouldComponentUpdate的参数nextProp、nextState与组件之前的状态比较,只有发生变化才返回true,允许更新组件,反之返回false
父组件:
export default class Cmp1 extends Component {
....
render() {
return (
)
}
}
子组件:
import React, { Component } from 'react'
export default class Cmp2 extends Component {
shouldComponentUpdate(nextProp,nextState){
//使用父级的state有变化
if(nextProp.num !== this.props.num){
return true;
}else{
return false;
}
}
render() {
let { num } = this.props;
//如果父级改变的数据,不是Cmp2使用的数据,为了优化,子组件可以不用跟随父组件一起render
return (
)
}
}
通过React的内置组件PrueComponent来自动进行prop与state变化比较
使用PureComponent的例子 子组件:
import React, { PureComponent } from 'react'
export default class Cmp2 extends PureComponent {
...
render() {
let { num } = this.props;
//如果父级改变的数据,不是Cmp2使用的数据,为了优化,子组件可以不用跟随父组件一起render
return (
)
}
}
额外注意的: 第一,PureComponent自己一个封装shouldComponentUpdate; 如果PureCompnent里书写shouldComponentUpdate会报错 shouldComponentUpdate should not be used when extending React.PureComponent.
Please extend React.Component if shouldComponentUpdate is used.
第二,修改state使用setState时候,需要修改一个新对象,而不能修改对象上的属性,否则React浅比较,不会对同一个对象做render
//数据
state = { name:'张三' }
changeName = () => {
let obj = this.state;
//react不会render页面,因为obj是原来的对象,引用的内存地址是同一个
obj.name='李四';
this.setState(obj);
//这个会render,因为setState里是一个全新的对象
this.setState({
name: '李四'
});
};
第三,设置state为空,react将不会调用render方法
this.setState({}) //设置空对象的方法,将不会触发render,在PureComponent组件中
不用PureComponet的情况下,依然来手动判断,什么情况下更新组件 在React17开始,在使用setState时候,用函数式书写方式,返回一个null,react将不会触发render
//在事件处理函数内,对setState进行二次逻辑判断,数值没有变化返回null,反之 正常返回
changeName = (ev) => {
let {name} = this.state;
let changeName = ev.target.value;
this.seState(()=>{
if(name===changeName ){
return null;
}else{
return { name: changeName }
}
})
}
render props 组件插槽
父组件通过挂载属性,等于一个函数,函数的返回值为一个组件,通过函数传参可以将父组件的数据传递至子组件,而父组件内需要预留一个位置,放这个未知的组件,这个未知的地方就是插槽的地方
类似Vue在组件标签体内写的内容,会出现组件内部的slot内,react的标签体就是一个自定义属性,而在组件标签内只需调用该属性对应函数,将组件内的数据作为该函数的参数即可
一般为了阅读快速理解,不成文规定大家将返回组件的自定义属性名,设置为'render'
父组件:
import React, { Component } from 'react'
import Cmp1 from './components/Cmp1'
import Cmp2 from './components/Cmp2'
export default class App extends Component {
render() {
return (
{ return }} />
)
}
}
Cmp1组件:
export default class Cmp1 extends Component {
state = {
num:1,
name:'张三'
}
render() {
return (
//子组件在Cmp1中希望放置的位置
我是组件Cmp1
{this.props.render(this.state.num)}
)
}
Cmp2组件:
export default class Cmp2 extends PureComponent {
render() {
let { num } = this.props;
return (
)
}
}
组件渲染降级,getDerivedStateFromError
getDerivedStateFromError 是静态属性 对因为不确定因素无法渲染的子组件,进行降级渲染,防止整个组件嵌套体系全部因为改子组件错误,而无法渲染界面
getDerivedStateFromError 在生产环境下,按照我们预期会渲染一下,然后依旧报错,这个在生产环境下会正常
componentDidCatch 生命周期,可以帮助我们汇总错误,发送服务器后台
export default class Cmp1 extends Component {
state = {
list: [ xxx,xxx ]
hasError: false
}
static getDerivedStateFromError(){
//改组件嵌套的子组件存在错误,因此通过开关,阻止子组件进一步渲染
return { 'hasError': true };
}
componentDidCatch(error, info){
//将错误信息发送至后台服务器,用于记录错误日志,便于开发人员汇总与修改
console.log('info ~~~ ');
console.log(typeof info.componentStack); //string
}
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return Something went wrong. ;
}
return ;
}
}
花式报错整合
给一个不存在的组件设置数据 移除了一个组件,或者切换路由,之前的组件开启定时器或者异步数据请求,且在恰当的时候,类组件调用了setState(函数式组件调用了React.useState的设置state的方法) The node you're attempting to unmount was rendered by React and is not a top-level container. Instead, have the parent component update its state and rerender in order to remove this component
解决方法: 在类组件compontentWillUnmount(函数式组件 React.useEffect第一个函数参数回调的函数内)
let [num,addNum] = React.useState(0)
React.useEffect(()=>{
let iTimer = setInterval(function(){
addNum((num)=>{
return num+1
})
},1000);
return (()=>{
clearInterval(iTimer);
})
},[]);
针对AJAX数据请求,设置开关,阻止其setState 类组件关闭AJAX异步数据设置操作
componentDidMount(){
this._isMounted = true;
$.ajax('xxxxx',{})
.then(res => {
if(this._isMounted){
this.setState({
aa:true
})
}
})
.catch(err => {})
}
componentWillUnMount(){
this._isMounted = false;
}
或 网上的方法:
componentDidMount(){
$.ajax('xxxx',{})
.then(res => {
this.setState({
data: datas,
});
})
.catch(err => {})
}
componentWillUnmount(){
this.setState = (state,callback)=>{
return;
};
}
使用react.useEffect定义的变量xxxxxxx,不能定义在react.useEffect外面 Assignments to the 'xxxxxxx' variable from inside React Hook React.useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside React.useEffect
解决方式: 修改书写方式,如:
export default function Funcmp2(props) {
let [num,addNum] = React.useState(0)
//不能写在外面,要写在React.useEffect里面,去定义变量
//let iTimer = null; //错误的
React.useEffect(()=>{
let iTimer = setInterval(function(){
// addNum(num+1);
addNum((num)=>{
console.log(num+1);
return num+1
})
},1000);
return (()=>{
//第一个函数的返回值为 componentWillUnmount
clearInterval(iTimer);
})
},[]);
}
使用React.useState定义的state,在React.useEffect内进行数据修改时候,不能写成对象形式,要写成函数形式 React Hook React.useEffect has a missing dependency: 'num'. Either include it or remove the dependency array. You can also do a functional update 'addNum(n => ...)' if you only need 'num' in the 'addNum' call react-hooks/exhaustive-deps
解决方法:
export default function Funcmp2(props) {
let [num,addNum] = React.useState(0)
React.useEffect(()=>{
//错误
//addNum(num+1);
//正确
addNum((num)=>{
console.log(num+1);
return num+1
})
},[]);
}
给一个非ReactDOM.render的组件,进行组件移除操作 Warning: unmountComponentAtNode(): The node you're attempting to unmount was rendered by React and is not a top-level container. Instead, have the parent component update its state and rerender in order to remove this component.
解决方法: 父组件
export default class Head extends Component {
componentDidMount(){
//渲染一个自组件
ReactDOM.render( ,document.getElementById('timerBox'));
}
render() {
let {addTxt} = this.state;
return (
)
}
}
子组件
import React from 'react'
import ReactDOM from 'react-dom'
export default function Funcmp2(props) {
let [num,addNum] = React.useState(0)
React.useEffect(()=>{
let iTimer = setInterval(function(){
addNum((num)=>{
console.log(num+1);
return num+1
})
},1000);
return (()=>{
clearInterval(iTimer);
})
},[]);
function delFn(){
if(props.removeNodeId){
ReactDOM.unmountComponentAtNode(document.getElementById(props.removeNodeId));
}
}
return (
这是一个函数自增长的函数类型组件 {num}
通过React.useEffect来开启一个定时器
删除这个组件
)
}
未完待续....