React学习笔记
一、React 简介
React 是用于构建用户界面的 JavaScript 库,是一个将数据渲染为 HTML 视图的开源 JavaScript 库,只关注操作 DOM 和呈现页面
React 由 Facebook 开发且开源
1、原生 JS 的痛点
(1)原生 JavaScript 操作 DOM 繁琐、效率低(DOM-API 操作 UI),
(2)使用 JavaScript 直接操作 DOM,浏览器会进行大量的重绘重排
(3)原生 JavaScript 没有组件化编码方案,代码复用率低
2、React 的特点
(1)采用组件化模式,声明式编码,关注结果,过程能自动完成(而非命令式编码,需要关注中间过程),提高开发效率及组件复用率
(2)在 React Native 中可以使用 React 语法进行移动端原生应用开发(利用 js 完成安卓和 ios 移动端开发)
(3)使用虚拟 DOM + 优秀的 Diffing 算法,尽量减少与真实 DOM 的交互
3、React 高效的原因
(1)使用虚拟 DOM,不总是直接操作页面真实 DOM
(2)DOM Diffing 算法,最小化页面重绘
4、相关 js 库
babel.min.js:解析 JSX 代码语法转为 JS 代码的库
react.development.js:React 核心库
react-dom.development.js:提供操作 DOM 的 react 扩展库
要先引入 react-dom.development.js,后引入 react.development.js
5、虚拟 DOM 和 真实 DOM
虚拟 DOM 本质是 Object 类型的对象(一般对象)
虚拟 DOM 比较 “轻”,属性较少,而真实 DOM 较 “重”,因为虚拟 DOM 是 React 内部在用,无需真实 DOM 上那么多的属性(如 style 等)
虚拟 DOM 最终会被 React 转化为真实 DOM 呈现在页面上
6、创建虚拟 DOM
方式一:纯 js(一般不用)
<body>
<div id="test"></div>
<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/JavaScript"> /* 此处写 JavaScript */
//1.使用 js 创建虚拟 DOM
const VDOM = React.createElementt('h1',{id:'title'},'Hello')
//2.渲染虚拟 DOM 到页面
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
</body>
方式二:JSX
<body>
<div id="test"></div>
<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/babel"> /* 此处一定要写 babel */
//1.使用 jsx 创建虚拟 DOM
const VDOM = <h1>Hello</h1> /* 此处一定不要写引号 */
//2.渲染虚拟 DOM 到页面
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
</body>
当要创建嵌套标签的虚拟 DOM 时,使用 js 很繁琐,而使用 jsx 就像写 html 一样很方便,babel 编译后依然是使用 js 创建虚拟 DOM 时的语法,因此 JSX 创建虚拟 DOM 就是 js 创建虚拟 DOM 写法的语法糖
7、JSX
JSX(JavaScript XML)是 react 定义的一种类似于 XML 的 JS 扩展语法(JS + XML)
(XML 早期用于存储和传输数据,后来用 JSON 用的更多更方便简单)
JSX 本质是 React.createElementt(标签名,{属性名:属性值},'标签体内容')
方法的语法糖
作用:用来简化创建虚拟 DOM,var ele = <h1>Hello</h1>
,注意它不是字符串也不是 HTML/XML 标签,它最终产生的就是一个 JS 对象
JSX 中标签名任意,可以是 HTML 标签或其他标签
jsx 语法规则
(1)定义虚拟 DOM 时不要写引号
(2)标签中混入 JS 表达式时要用 {}
(3)样式的类名指定不要用 class,要用 className
(4)内联样式要用 style={{key:value}}
的形式写
(5)虚拟 DOM 必须只有一个根标签
(6)标签必须闭合,如 <input />
或 <input></input>
(7)标签首字母
1)若是小写字母开头,则将该标签转为 html 中同名元素,若 html 中无该编起对应的同名元素,则报错
2)若大写字母开头,react 就去渲染对应的组件,若组件没有定义,则报错
(8)给一个{数组} react 会自动遍历,但给一个对象不会
<head>
<style>
.title{
background-color:red;
width: 100px;
}
</style>
</head>
<body>
<div id="test"></div>
<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/babel">
const myId = 'xXx';
const myData = '123aBc';
const VDOM = (
<h1 className="title" id={myId.toLowerCase()}>
<span style={{color:'white',fontSize:'20px'}}>{{myData.toLowerCase()}}</span>
</h1>
)
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
</body>
区分 js 语句(代码)与 js 表达式
表达式:一个表达式会产生一个值(即通过 const x =
来接能接到值就是表达式,否则不是),可放在任何一个需要值的地方,如
a
a+b
demo(1) //函数调用表达式
arr.map()
function test(){}
console.log()
而语句(代码)如
if(){}
for(){}
switch(){case:xxx}
二、模块与组件、模块化与组件化
模块
模块是向外提供特定功能的 js 程序,一般就是一个 js 文件
为什么要拆成模块?:随着业务逻辑增加,代码越来越多且复杂
作用:复用 js,简化 js 的编写(每个 js 文件不那么庞大),提高 js 运行效率
组件
组件用来实现局部功能效果的代码和资源的集合(html/css/js/image等)
为什么用组件?:一个界面的功能更复杂
作用:复用编码,简化项目编码,提高运行效率
模块化
当应用的 js 都以模块来编写的,这个应用就是一个模块化的应用
组件化
当应用是以多组件的方式实现,这个应用就是一个组件化的应用
三、React 面向组件编程
可现在 Chrome 浏览器中安装插件 React Developer Tools
简单组件与复杂组件
若组件中有状态 state 就是复杂组件
组件的数据存在 state 里,组件的状态 state 驱动页面
定义组件
方式一:函数式组件
用函数定义的组件适用于简单组件
<body>
<div id="test"></div>
<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/babel">
//1.创建函数式组件
function Demo(){ //组件首字母要大写
console.log(this); //此处的 this 是 undefined,因为 babel 编译后开启了严格模式(禁止自定义函数中 this 指向 window)
return <h1>xxx</h1>
}
//2.渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>
</body>
执行了 ReactDOM.render(<demo/>...)
后发生了什么?
— 1.React 解析组件标签,找到了 Demo 组件
— 2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中
方式二:类式组件
用类定义的组件适用于复杂组件
构造器 constructor 调用 1 次
render 调用 1+n 次(其中 1 是初始化的那次,n 是状态 state 更新的次数)
事件函数触发几次(如点击)调用几次
复习类相关知识
类中的构造器不是必须写的,要对实例进行初始化操作时(如添加指定属性)才写
类中的构造器方法中的 this 是类的实例对象
类中的一般方法是放在了类型的原型对象上(__proto__),供实例使用,通过实例对象调用类中的方法式,方法中的 this 就是实例对象
若 A 类继承了 B 类,且 A 类中写了构造器,则 A 类构造器中的 super 是必须调用的
类中的方法默认开启了局部的严格模式,所以方法中的 this 为 undefined
且在外部通过 const x = 实例对象.方法,然后 x() 进行调用此时依然不是通过实例对象调用,而是直接调用,因此调用的方法中的 this 不是实例对象
类中可以直接写赋值语句 `a:1`,相当于给实例对象自身上添加一个属性 a 值为 1
类中箭头函数中没有this,但若使用this不报错,而是去外部找this
创建类式组件
<body>
<div id="test"></div>
<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/babel">
//1.创建类式组件
class MyComponent extends React.Component{ //必须继承React.Component
render(){ //必须有 render 函数,render 是放在 MyComponent 类的原型对象上,供实例使用,render 中的 this 是 MyComponent 实例对象(也叫 MyComponent 实例对象)
return <h1>xxx</h1>
}
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test'))
</script>
</body>
执行了 ReactDOM.render(<MyComponent/>...)
后发生了什么?
— 1.React 解析组件标签,找到了 MyComponent 组件
— 2.发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到原型上的 render 方法
— 3.将 render 返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中
组件实例(用类定义的组件)的三大核心属性
1、state
state 是组件实例对象最重要的属性,值是对象,可包含多个 key-value 的组合
组件被称为“状态机”,通过更新组件的 state 来更新对应的页面显示(重新渲染组件)
注意:(1)组件中 render 方法中的 this 为组件实例对象
(2)组件自定义的方法中 this 为 undefined 如何解决?
a)强制绑定 this:通过函数对象的 bind()
b)赋值语句+箭头函数
(3)状态数据不能直接修改或更新,需要使用 this.setState({state中属性:值}),且更新是一种合并(重名的覆盖掉),不是替换
<script type="text/babel">
//1.创建类式组件
class Weather extends React.Component{
constructor(props){
super(props)
//初始化状态
this.state={isHot:true}
}
render(){
return <h1>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
state 的简写方式
<script type="text/babel">
//1.创建类式组件
class Weather extends React.Component{
//constructor(props){
//super(props)
//this.state={isHot:true}
//}
state={isHot:true}
render(){
return <h1 onClick={this.demo}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
}
//自定义方法:用赋值语句的形式+箭头函数,因为类中箭头函数中没有this,但若使用this不报错,而是去外部找this(这里即实例对象),这样在定义事件方法时就不要再this.demo=this.demo.bind(this)
demo=()=>{
console.log('哈哈哈')
this.setState({isHot:!isHot})
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
1、props
每个组件对象都会有 props(properties的简写)属性
组件标签的所有属性都保存在 props 中
作用:
—(1)通过标签属性从组件外向组件内传递变化的数据
—(2)注意:组件内部不要修改 props 数据
<script type="text/babel">
//1.创建类式组件
class Person extends React.Component{
render(){
return (
<ul>
<li>姓名:{this.props.name}</li>
<li>性别:{this.props.ex</li>
</ul>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Person name='xxx' sex='男'/>, document.getElementById('test'))
ReactDOM.render(<Person name='xxxxx' sex='女'/>, document.getElementById('test1'))
</script>
注意:props 是只读的
props 的批量传递
在标签属性中使用 {...对象}
,react + babel 允许使用展开运算符展开对象,...对象
这也仅适用于标签属性的传递,如在 console.log(...对象)
就无效,啥也不输出,注意这里的花括号表示里面表达式,和 js 中拷贝对象的含义不同
const p ={name:'xxx',sex:'男'}
ReactDOM.render(<Person {...p}/>, document.getElementById('test'))
对 props 进行限制
需要引入 prop-types,用于对组件标签属性进行类型、必要性、默认值限制,引入后全局多了个对象 PropTypes
<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/javascript" src="./prop-types.js"></script>
<script type="text/babel">
//1.创建类式组件
class Person extends React.Component{
render(){
return (
<ul>
<li>姓名:{this.props.name}</li>
<li>性别:{this.props.ex</li>
</ul>
)
}
}
Person.propTypes = {
name:PropTypes.string.isRequired, //使得 name 必传且为字符串。在 React v15.5 之前使用 name:React.PropTypes.string.isRequired,且不需要引入 prop-types,v15.5 之后单独封装成 prop-types,减轻 React 重量
sex:PropTypes.string,
age:PropTypes.number,
speak:PropTypes.func
}
Person.defaultProps = { //设置不传某属性时使用的默认值
sex:'男',
age:18
}
//2.渲染组件到页面
ReactDOM.render(<Person name='xxx' sex='男' speak={speak}/>, document.getElementById('test'))
function speak(){
console.log('xxx')
}
</script>
props 的简写方式
把 组件对象.propTypes
和 组件对象.defaultProps
放到类内部,前面加上 static 关键字
<script type="text/javascript" src="./react.development.js"></script>
<script type="text/javascript" src="./react-dom.development.js"></script>
<script type="text/javascript" src="./babel.min.js"></script>
<script type="text/javascript" src="./prop-types.js"></script>
<script type="text/babel">
//1.创建类式组件
class Person extends React.Component{
static propTypes = {
name:PropTypes.string.isRequired, //使得 name 必传且为字符串
sex:PropTypes.string,
age:PropTypes.number,
speak:PropTypes.func
}
static defaultProps = { //设置不传某属性时使用的默认值
sex:'男',
age:18
}
render(){
return (
<ul>
<li>姓名:{this.props.name}</li>
<li>性别:{this.props.ex</li>
</ul>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Person name='xxx' sex='男' speak={speak}/>, document.getElementById('test'))
function speak(){
console.log('xxx')
}
</script>
类式组件中的构造器与 props
在 React 中构造函数仅用于以下两种情况:
—(1)通过 this.state = {...}
来初始化 state(但这种初始化方式可在构造器外直接使用 state = {}
来赋值)
—(2)为事件处理函数绑定实例 this.demo=this.demo.bind(this)
(但这种绑定可直接在定义函数时使用箭头函数来省略这步)
因此构造器不是必须的
但是有时需要接收和访问实例对象的 props 值,需要在构造器中接收 props 并传递给 super(props),若要访问 this.props
则必须将 props 传给 super
即构造器是否接收 props,是否传递给 super 取决于是否希望在构造器中通过 this 访问 props
constructor(props){
super(props)
console.log(this.props)
}
函数式组件使用 props
若不是通过类创建组件,而是通过函数定义的组件,则无法使用 state 和 refs(除非使用最新版 React 中的 hooks),因为没有类就没有实例就没有 this,但可以使用 props
<script type="text/babel">
//1.创建函数式组件
function Person(props){
const {name,age,sex} = props
return (
<ul>
<li>姓名:{this.props.name}</li>
<li>性别:{this.props.ex</li>
</ul>
)
}
Person.propTypes = {
name:PropTypes.string.isRequired, //使得 name 必传且为字符串
sex:PropTypes.string,
age:PropTypes.number,
speak:PropTypes.func
}
Person.defaultProps = { //设置不传某属性时使用的默认值
sex:'男',
age:18
}
//2.渲染组件到页面
ReactDOM.render(<Person name="x"/>, document.getElementById('test'))
</script>
3、refs
组件内的标签可以定义 ref 属性来标识自己,这样就可以不使用 id 和 getElementById,而是使用 this.refs.ref名 来替代(注意这样拿到的是真实 DOM 节点,而不是虚拟 DOM 节点)
定义形式:
—(1)字符串形式的 ref <input ref="input1"/>
(因为存在效率问题,这种方式以不被官方推荐使用,可能在未来 React 版本中移除)
<script type="text/babel">
//1.创建类式组件
class Demo extends React.Component{
showData=()=>{
const {input1} = this.refs
console.log(input1.value)
}
showData2=()=>{
const {input2} = this.refs
console.log(input2.value)
}
render(){
return(
<input ref="input1" type="text"/>
<button onClick={this.showData}>点击</button>
<input ref="input2" onBlur={this.showData2} type="text"/>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
—(2)回调形式的 ref <input ref={(c)=>{this.input1=c}}/>
执行 render 函数时就会执行该回调函数,其中参数 c 表示当前所处 DOM 节点,把当前 DOM 节点挂在实例自身(this)上,并取名为 input1
<script type="text/babel">
//1.创建类式组件
class Demo extends React.Component{
showData=()=>{
const {input1} = this
console.log(input1.value)
}
showData2=()=>{
const {input2} = this
console.log(input2.value)
}
render(){
return(
<input ref={(c)=>{this.input1 = c}} type="text"/>
<button onClick={this.showData}>点击</button>
<input ref={(c)=>{this.input2 = c}} onBlur={this.showData2} type="text"/>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
注意:若 ref 回调函数是以内联函数的方式定义,在更新过程中(第二次触发 render() 开始)它会被执行两次,第一次传入参数 null,第二次才传入 DOM 元素,因为每次渲染时会创建一个新的函数实例,所以 React 会传入 null 清空旧的 ref 再设置新的
通过 ref 回调函数定义或 class 的绑定函数的方式可避免上述问题,但大多数情况下它是无关紧要的,可以使用内联方式
<script type="text/babel">
//1.创建类式组件
class Demo extends React.Component{
showData=()=>{
const {input1} = this
console.log(input1.value)
}
saveInput=(c)=>{
this.input1 = c
console.log(c)
}
render(){
return(
<input ref={this.saveInput} type="text"/>
<button onClick={this.showData}>点击</button>
<input ref={(c)=>{this.input2 = c}} onBlur={this.showData2} type="text"/>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
—(3)createRef 创建 ref 容器 myRef = React.createRef()
,调用后会返回一个容器,该容器可存储被 ref 标识的节点,把当前 ref 所在 DOM 节点直接存储到容器中(官方推荐使用)
注意:该容器是 “专人专用” 的,如果两个 DOM 节点都放在同一个容器中,后放入的会覆盖前面的
<script type="text/babel">
//1.创建类式组件
class Demo extends React.Component{
myRef = React.createRef() //把这个容器挂在组件实例自身上
myRef2 = React.createRef()
showData=()=>{
console.log(this.myRef.current.value)
}
showData2=()=>{
console.log(this.myRef2.current.value)
}
render(){
return(
<input ref={this.myRef} type="text"/>
<button onClick={this.showData}>点击</button>
<input onBlur={this.showData2} ref={this.myRef2} type="text"/>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
注意:不要过度使用 ref,如当发生事件的元素和当前操作的元素是同一个就可以不使用 ref,而是 event.target 来获取,如 <input onBlur={this.showData2} ref={this.myRef2} type="text"/>
可换成 <input onBlur={this.showData2} type="text"/>
在 showData2 中定义如下
showData2=(event)=>{
console.log(event.target.value)
}
事件绑定
在 React 中使用onClick
、onBlur
等绑定事件,注意在原生 js 中是 onclick
、onblur
方式一:
<script type="text/babel">
//1.创建类式组件
class Weather extends React.Component{
constructor(props){
super(props)
this.state={isHot:true}
}
render(){
return <h1 onClick={demo}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
function demo(){
console.log('哈哈哈')
}
</script>
方式二:
<script type="text/babel">
//1.创建类式组件
class Weather extends React.Component{
constructor(props){
super(props)
this.state={isHot:true}
this.demo1=this.demo.bind(this)//生成一个新函数,这个新函数的this为实例对象,并把这个新函数取名为demo1挂在实例上,解决 demo 中 this 指向问题
}
render(){
return <h1 onClick={this.demo1}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1> //这里 demo 在 Weather 原型对象上,供实例使用,由于 demo 是作为 onClick 的回调,所以不是通过实例调用的,是直接调用,且类中的方法默认开启局部的严格模式,因此 demo 中的 this 为 undefined,所以需要 this.demo=this.demo.bind(this)
}
demo(){
console.log('哈哈哈')
this.setState({isHot:!isHot})
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
方式三:
<script type="text/babel">
//1.创建类式组件
class Weather extends React.Component{
//constructor(props){
//super(props)
//this.state={isHot:true}
//}
state={isHot:true}
render(){
return <h1 onClick={this.demo}>今天天气{this.state.isHot?'炎热':'凉爽'}</h1>
}
//自定义方法:用赋值语句的形式+箭头函数,因为类中箭头函数中没有this,但若使用this不报错,而是去外部找this(这里即实例对象),这样在定义事件方法时就不要再this.demo=this.demo.bind(this)
demo=()=>{
console.log('哈哈哈')
this.setState({isHot:!isHot})
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
事件处理
(1)通过 onXxx 属性指定事件处理函数(注意大小写)
— React 使用的是自定义(合成)事件(如 onClick),而不是使用原生 DOM 事件(如 onclick),这是为了更好的兼容性
— React 中的事件是通过事件委托(冒泡)方式处理的(委托给组件最外层的元素),这是为了更高效
(2)通过 event.target 得到发生事件的 DOM 元素对象,这可以解决过度使用 ref 的情况
非受控组件
页面中所有输入类的 DOM 节点(如 checkbox、radio 等)现用现取就是非受控组件
<script type="text/babel">
//1.创建类式组件
class Login extends React.Component{
handleSubmit=(event)=>{
event.preventDefault() //阻止表单提交这一默认事件
const {username,password}=this
alert(${username.value},${password.value})
}
render(){
return(
<form onSubmit={this.handleSubmit}>
用户名:<input ref={c=>this.username=c} type="text" name="username"/>
密码:<input ref={c=>this.password=c} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test'))
</script>
受控组件
页面中所有输入类的 DOM 节点,随着输入的变化存入维护到状态 state 中,需要时从 state 中取,这种就属于受控组件
受控组件的优势在于可以省略掉 ref
<script type="text/babel">
//1.创建类式组件
class Login extends React.Component{
state={
username:''.
password:''
}
saveUsername=(event)=>{
this.setState({username:event.target.value})
}
savePassword=(event)=>{
this.setState({password:event.target.value})
}
handleSubmit=(event)=>{
event.preventDefault() //阻止表单提交这一默认事件
const {username,password}=this.state
alert(${username},${password})
}
render(){
return(
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username"/>
密码:<input onChange={this.savePassword} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test'))
</script>
注意上述代码中是把 this.saveUsername 函数传给 onChange 作为事件回调
高阶函数
高阶函数:若一个函数符合下面两个规范中的任何一个,那么该函数就是高阶函数
(1)若 A 函数接收的参数是个函数,那么 A 就可称之为高阶函数
(2)若 A 函数调用的返回值依然是个函数,那么 A 就可称之为高阶函数
常见的高阶函数有:Promise、setTimeout、setInterval、arr.map() 等
函数柯里化
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式,如
function sum(a){
return (b)=>{
return (c)=>{
return a+b+c
}
}
}
例子:
<script type="text/babel">
//1.创建类式组件
class Login extends React.Component{
state={
username:''.
password:''
}
saveFormData=(dataType)=>{
return (event)=>{
this.setState({[dataType]:event.target.value})
}
}
handleSubmit=(event)=>{
event.preventDefault() //阻止表单提交这一默认事件
const {username,password}=this.state
alert(${username},${password})
}
render(){
return(
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
密码:<input onChange={this.saveFormData('password')} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test'))
</script>
注意上述代码中 onChange={this.saveFormData('username')
的 saveFormData 后加了括号,所以表示把 saveFormData 的返回值传给 onChange,而不是把 saveFormData 函数传给 onChange
上述代码也可不用函数柯里化实现
<script type="text/babel">
//1.创建类式组件
class Login extends React.Component{
state={
username:''.
password:''
}
saveFormData=(dataType,event)=>{
this.setState({[dataType]:event.target.value})
}
handleSubmit=(event)=>{
event.preventDefault() //阻止表单提交这一默认事件
const {username,password}=this.state
alert(${username},${password})
}
render(){
return(
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={event => this.saveFormData('username',event)} type="text" name="username"/>
密码:<input onChange={event => this.saveFormData('password',event)} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test'))
</script>
组件的生命周期
组件对象从创建到死亡会经历特定阶段
React 组件对象包含一系列勾子函数(生命周期回调函数),在特定的时刻调用
在定义组件时,在特定的生命周期回调函数中做特定工作
生命周期钩子中的 this 都是组件实例对象
render:初始化渲染、状态更新之后调用
componentDidMount:组件挂在页面之后调用
componentWillReceiveProps(props):子组件将要接收新的 props 的钩子,也可以不传参,但是父子组件第一次 render 时不会调用该函数,当父组件第二次 render 时给子组件传 props 才会调用该函数
shouldComponentUpdate:控制组件更新的 “阀门” 表示组件是否应该被更新,调用 setState 后会调用 shouldComponentUpdate,返回布尔值,默认返回 true,若写了 shouldComponentUpdate 函数就要写返回值
componentWillUpdate:组件将要更新的钩子
componentDidUpdate:组件更新完毕的钩子
componentWillUnmount:组件将要卸载时调用
旧版的 React 生命周期
生命周期的三个阶段(旧版):
(1)初始化阶段:由 ReactDOM.render() 触发—初次渲染
1. constructor()
2. componentWillMount()
3. render()
4. componentDidMount() ==> 常用,一般在这个钩子中做一些初始化的事,如开启定时器、发送网络请求、订阅消息
(2)更新阶段:由组件内部 this.setState() 或父组件重新 render 触发
1. shouldComponentUpdate()
2. componentWillUpdate()
3. render() ==> 必须使用的一个
4. componentDidUpdate()
(3)卸载组件:由 ReactDOM.unmountComponentAtNode() 触发
1. componentWillUnmount() ==> 常用,一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
例子:
h2 标签中的文字2s逐渐变淡直到完全透明后又变黑再逐渐变淡,而当点击按钮时删除 h2
<script type="text/babel">
//1.创建类式组件
class Life extends React.Component{
state={opacity:1}
death=()=>{
//卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
//组件挂载完毕
componentDidMount(){
this.timer = setInterval(()=>{
let {opacity} = this.state
opacity -= 0.1
if(opacity <= 0) opacity = 1
this.setState({opacity}) //这里相当于 this.setState({opacity:opacity}),因为俩 opacity 同名所以可以简写
},200)
}
//组件将要卸载
componentWillUnmount(){
clearInterval(this.timer)
}
render(){
return(
<div>
<h2 style={{opacity:this.state.opacity}}>xxx</h2>
<button onClick={this.death}>xx</button>
</div>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Life/>, document.getElementById('test'))
</script>
例子:
每点击一次按钮就 + 1
<script type="text/babel">
//1.创建类式组件
class Count extends React.Component{
constructor(props){
console.log('constructor')
super(props)
this.state={count:0}
}
add=()=>{
const {count} = this.state
this.setState({count:count+1})
}
force=()=>{
this.forceUpdate() //强制更新
}
//组件将要挂载
componentWillMount(){
console.log('componentWillMount')
}
//组件挂载完毕
componentDidMount(){
console.log('componentDidMount')
}
//组件将要卸载
componentWillUnmount(){
}
render(){
console.log('render')
return(
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点击+1</button>
<button onClick={this.force}>强制更新</button>
</div>
)
}
}
//2.渲染组件到页面
ReactDOM.render(<Count/>, document.getElementById('test'))
</script>
新版的 React 生命周期(v17.0.1之后)
与旧版本不同的是 componentWillReceiveProps、componentWillMount、componentWillUpdate 重命名为 UNSAFE_componentWillReceiveProps、UNSAFE_componentWillMount、UNSAFE_componentWillUpdate,且 v18.x 版本开始必须加 UNSAFE_ 才行
注意:UNSAFE_ 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染后
新旧版本对比:新版本废弃了三个钩子 componentWillReceiveProps、componentWillMount、componentWillUpdate,新引入了两个钩子 getDerivedStateFromProps 和 getSnapshotBeforeUpdate
getDerivedStateFromProps(props,state):返回一个对象来更新 state,或返回 null 则不更新任何内容,并且这个更新的 state 无法修改,值只能是 props 的值
static getDerivedStateFromProps(props){
return props
}
注意:getDerivedStateFromProps 适用于 state 的值在任何时候都取决于 props 值的情况
getDerivedStateFromProps 会导致代码冗余,并使组件难以维护
getSnapshotBeforeUpdate:在最近一次渲染输出(提交到 DOM 节点)之前调用,它使得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置),此生命周期的任何返回值将作为参数传给 componentDidUpdate,返回 snapshot 或 null
getSnapshotBeforeUpdate(){
return 'xx' //或 return null
}
此用法不常见,可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等
如随着时间 list 的顶部一直增加一定高度的内容,但下拉至某位置后滚动条位置不动且不影响上面继续添加
getSnapshotBeforeUpdate(){
return this.refs.list.scrollHeight
}
componentDidUpdate(preProps,preState,height){
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
}
componentDidUpdate(preProps,preState):
componentDidUpdate(preProps,preState,snapshotValue){
console.log('上一个props'+preProps+'上一个state'+preState+'getSnapshotBeforeUpdate return来的'+snapshotValue)
}
生命周期的三个阶段(新版):
(1)初始化阶段:由 ReactDOM.render() 触发—初次渲染
1. constructor()
2. getDerivedStateFromProps()
3. render()
4. componentDidMount() ==> 常用,一般在这个钩子中做一些初始化的事,如开启定时器、发送网络请求、订阅消息
(2)更新阶段:由组件内部 this.setState() 或父组件重新 render 触发
1. getDerivedStateFromProps
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate()
5. componentDidUpdate()
(3)卸载组件:由 ReactDOM.unmountComponentAtNode() 触发
1. componentWillUnmount() ==> 常用,一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
DOM 的 Diffing 算法
验证 Diffing 算法
key
问题1: react/vue 中 key 有什么作用?(key 的内部原理是什么?)
虚拟 DOM 中 key 的作用:
1)简单的说:key 是虚拟 DOM 对象的标识,在更新显式时 key 起着极其重要的作用
2)详细的说:当状态中数据发生变化时,react 会根据【新数据】生成【新的虚拟 DOM】,随后 react 进行【新虚拟 DOM】与【旧虚拟 DOM】的 diff 对比,比较规则如下:
a.旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key
若虚拟 DOM 中内容没变,直接使用之前的真实 DOM
若虚拟 DOM 中的内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM
b.旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key,则根据数据创建新的真实 DOM,随后渲染到页面
问题2:为什么遍历列表时 key 最好不要用 index?
用 index 作为 key 可能会引发的问题:
1)若对数据进行逆序添加、逆序删除等破坏顺序操作时会产生没有必要的真实 DOM 更新 => 界面效果没问题,但效率低
2)若结构中还包含输入类的 DOM,会产生错误 DOM 更新 => 界面会出问题
3)注意若不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用 index 作为 key 是没有问题的
问题3:开发中如何选择 key?
1)最好使用每条数据的唯一标识作为 key,如 id、手机号、身份证号、学号等唯一值
2)若确定只是简单的展示数据,用 index 也是可以的
例子:点击按钮后在数组头部增加一条数据并显示
add=()=>{
const {personArr} = this.state
const p = {id:personArr.length+1,name:'c',age:20}
this.setState({personArr:[p,...personArr]})
}
render(){
return(
<ul>
{
this.state.personArr.map((personObj,index)=>{ //这里用 index 做 key
return <li key={index}>{personObj.name}---{personObj.age}</li>
})
}
</ul>
)
}
当使用 index 作为 key 时
初始数据:
{id:1,name:'a',age:18}
{id:2,name:'b',age:18}
初始的虚拟 DOM
<li key=0>a---18<input type="text"/></li>
<li key=1>b---19<input type="text"/></li>
更新后的数据:
{id:3,name:'c',age:20}
{id:1,name:'a',age:18}
{id:2,name:'b',age:18}
更新后的虚拟 DOM
<li key=0>c---20<input type="text"/></li>
<li key=1>b---19<input type="text"/></li>
<li key=2>a---18<input type="text"/></li>
这种情况下增加数据后,头两条数据的 input 框中内容和之前一样,这是已导致了混乱
当使用 id 作为 key 时
初始数据:
{id:1,name:'a',age:18}
{id:2,name:'b',age:18}
初始的虚拟 DOM
<li key=1>a---18<input type="text"/></li>
<li key=2>b---19<input type="text"/></li>
更新后的数据:
{id:3,name:'c',age:20}
{id:1,name:'a',age:18}
{id:2,name:'b',age:18}
更新后的虚拟 DOM
<li key=30>c---20<input type="text"/></li>
<li key=1>b---19<input type="text"/></li>
<li key=2>a---18<input type="text"/></li>
这种情况下增加数据后,第一条数据后的 input 框中为空白,后两条数据的 input 框中内容和之前一样
四、React 应用
1、react 脚手架
xxx 脚手架是用来帮助程序员快速创建一个基于 xxx 库的模板项目
脚手架中包含了所有需要的配置(语法检查、jsx 编译、devServer…)、下载好了所有相关依赖、可以直接运行一个简单的效果
react 提供了一个用于创建 react 项目的脚手架库:create-react-app
项目的整体技术架构为 react + webpack + es6 + eslint
使用脚手架开发的项目的特点:模块化、组件化、工程化(工程化就是写好代码后会自动进行语法检查、代码压缩、编译、兼容性处理等一系列操作)
2、使用脚手架创建 react 项目并启动
(1)全局安装 create-react-app 库
npm install -g create-react-app
(2)切换到想创建项目的目录,并创建项目 create-react-app 项目名
(3)进入项目文件夹,并启动项目 npm start
3、react 脚手架项目结构
public — 静态资源文件夹
facion.icon --- 网站页签图标
index.html --- 主页面
logo192.png --- logo 图
logo512.png --- logo 图
manifest.json --- 应用加壳(将网络加壳成安卓或 ios 应用)的配置文件
robots.txt --- 爬虫协议文件
src — 源码文件夹
App.css --- App 组件的样式
App.js --- App 组件
App.test.js --- 用于给 App 做测试
index.css --- 样式
index.js --- 入口文件
logo.svg ---logo 图
reportWebVitals.js --- 页面性能分析文件(需要 web-vitals 库的支持)
setupTests.js --- 用于做应用的整体测试或组件单元测试(需要 jest-dom 库支持)
4、样式模块化
若组件内都直接使用 import ‘./组件.css’ 引入样式,不同组件间可能定义了 className 相同的标签而设置的样式不同就会产生冲突
要解决这种冲突的一种方式是使用 less
另一种方式是组件的 css 文件都命名为 xxx.module.css,在 组件.js 文件中引入样式时使用 import xxx from './xxx.module.css'
,使用样式时如 <h1 className={xxx.类名}></h1>
5、vscode 中 react 插件
ES7 React/Redux/GraphQL/React-Native snippets 插件便于生成代码模板,如 rcc + 回车
即可生成类式组件代码模板,rfc + 回车
即可生成函数式组件代码模板
6、功能界面的组件化编码流程
(1)拆分组件:拆分界面,抽取组件
(2)实现静态组件:使用组件实现静态页面效果
(3)实现动态组件:
— 1)动态显示初始化数据:数据类型、数据名称、保存在哪个组件
— 2)交互(从绑定事件监听开始)
7、如何确定将数据放在哪个组件的 state 中?
若某个组件使用,则放在其自身的 state 中
若某些组件使用,则放在他们共同的父组件 state 中(也称为状态提升)
状态在哪,操作状态的方法就在哪
8、父子组件间数据传递
【父组件】给【子组件】传数据可在子组件传属性,在子组件中通过 props 获取数据
【子组件】给【父组件】传数据,在父组件定义函数并传给子组件,在子组件中调用该函数 this.props.函数()
五、react ajax
React 本身只关注于界面,并不包含发送 ajax 请求的代码
前端应用需要通过 ajax 请求与后台进行交互(json 数据)
react 应用中需要集成第三方 ajax 库(或自己封装)
1、常用的 ajax 请求库
(1)jQuery:比较重,若需要另外引入不建议使用
(2)axios:轻量级,建议使用
—a)封装 XmlHttpRequest 对象的 ajax
—b)promise 风格
—c)可以用在浏览器端和 node 服务器端
2、跨域问题
ajax 引擎会拦截不同源返回的数据,所以需要一个中间代理服务器,该服务器的和前端 ajax 引擎是同源的,前端给服务器发请求时是给代理服务器发送请求,代理服务器再转发至服务器,然后服务器将数据返回给代理服务器,最后再返回给前端
解决方式一:在脚手架中配置代理解决跨域
在 package.json 文件中添加
"proxy":"服务器端地址:服务器端口"
但这种方式会把服务器端口写死,只能向这个地址这个端口发请求
在代码中发送请求时地址写前端运行的端口
axios.get('http://localhost:前端端口/获取后端数据的api')
因为在前端找不到相应 api,所以代理服务器会转发至后端
优点:配置简单,前端请求资源时可以不加任何前缀
缺点:不能配置多个代理
工作方式:当请求了前端如 3000 端口不存在的资源时,该请求会转发给配置的服务器如 5000 端口(优先匹配前端资源)
解决方式二:也是在脚手架中配置代理解决跨域
新加文件 src/setupProxy.js 文件,并添加如下内容,React 脚手架会自动找到这个文件进行代理服务器配置
const proxy = require('http-proxy-middleware')
module.exports = function(app){
app.use(
proxy('/api1',{ //遇见 /api1 前缀的请求,就会触发该代理配置
target:'http://localhost:5000', //请求转发给哪个服务器
changeOrigin: true, //控制服务器收到的请求头中 Host 字段的值,设为 true 后服务器端输出请求头的 Host 值时就不是真正前端地址,而是服务器自身地址,这样服务器设置一些限制时不会有问题
pathRewrite:{'^/api1':''} //重写请求路径,删掉前面的 /api1
}),
/*changeOrigin 为 true 时,服务器收到的请求头中 host 为如 localhost:5000(服务器自身地址)
changeOrigin 为 false 时,服务器收到的请求头中 host 为如 localhost:3000(前端地址)
changeOrigin 默认为 false,一般将其设为 true
*/
proxy('/api2',{
target:'http://localhost:5001',
changeOrigin: true,
pathRewrite:{'^/api2':''}
})
)
}
在代码中发送请求时写的地址要加上 /api1
或 /api2
这样会去相应服务器中获取数据
优点:可以配置多个代理,可灵活的控制请求是否走代理
缺点:配置繁琐,前端请求资源时必须加前缀,若不写前缀就不走代理
3、fetch 发送请求
关于 fetch 可查看 github 和 思否 的相关文档
之前发送请求都是通过 XMLHttpRequest,即使是使用 jQuery、axios、zepto 等,这些其实是对 XMLHttpRequest 的封装
此外有另一种发送请求的方式 fetch(因为兼容性问题所以不常用)
fetch 是原生函数,不再使用 XMLHttpRequest 对象提交 ajax 请求,注意老版本浏览器可能不支持
fetch 体现了 “关注分离” 的原则(即不是一步到位,而是分步走)
fetch(url, options).then(
response => {
console.log('联系服务器成功了',response.json()) //response.json() 返回 Promise 实例对象,若联系服务器成功且获取数据成功则里面存着获取到数据,若联系服务器成功但获取数据失败则里面存着失败的原因
return response.json() //返回 Promise 实例对象,因此才能有下面的 .then 链式调用
},
error => {
console.log('联系服务器失败了')
return new Promise(()=>{}) //返回初始化状态的 Promise 来阻断往下走执行下面的 then,若没有这个 return 则会返回 undefined,且当前这个 then 对应的 Promise 返回是成功状态,值为 undefined
}
).then(
response => {console.log('获取数据成功了',response);},
error => {console.log('获取数据失败了',error)}
)
对上面的代码进行优化
fetch(url, options).then(
response => {
console.log('联系服务器成功了',response.json())
return response.json()
},
).then(
response => {console.log('获取数据成功了',response);},
).catch(
error => {console.log('请求出错',error)}
)
对上面代码继续优化
事件响应函数 = async() => {
try{
const response = await fetch(url) //此时等到的就是上面未优化代码中 Promise 成功时返回的 response.json()
const data = await response.json()
console.log(data)
}catch(error){
console.log('请求出错',error)
}
}
六、任意组件间的通信:消息订阅——发布机制
需要借助工具库 PubSubJS
下载 npm install pubsub-js --save
使用
import PubSub from 'pubsub-js' //引入
let token = PubSub.subscribe('订阅的消息',回调函数(消息名,收到的数据)=>{...}) //在需要获取消息的组件中订阅
PubSub.unsubscribe(token) //取消订阅
PubSub.publish('消息名',携带的数据) //发布消息
如在订阅消息的组件中
componentDidMount(){
this.token = PubSub.subscribe('xx',(_,data){
conosle.log(data)
})
}
//取消订阅
componentWillUnmount(){
PubSub.unsubscribe(this.token)
}
在发布消息的组件中
PubSub.publish('xx',{name:'tom',age:18})
七、React 路由
1、SPA 的理解
单页 Web 应用(single page web application, SPA)中整个应用只有一个完整的页面,react、vue 等写的都是单页面多组件应用
点击页面中的链接不会刷新页面,只会做页面的局部更新
数据都需要通过 ajax 请求获取,并在前端异步展现
2、路由的理解
一个路由就是一个映射关系(key:value),key 为路径,value 可能是 function 或 component
路由的分类
(1)后端路由:
后端路由的 value 是 function,用来处理客户端提交的请求
注册路由:router.get(path,function(req,res))
工作过程:当 node 接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
(2)前端路由:
浏览器路由 value 是 component,用于展示页面的内容
原生 html 中,靠 <a>
跳转不同的页面,在 React 中靠路由链接实现切换组件
注册路由:<Route path="/test" component={Test}>
工作过程:点击某组件引起路由跳转,路径改变,当浏览器的 path 变为 /test 时,被前端路由器监测到进行匹配组件,当前路由组件就会变为 Test 组件
history
在 BOM 对象(window 也在 BOM 上)上有个属性 history 专门管理浏览器的路径、历史记录等,这是前端路由的基石
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
//方法一
let history = History.createBrowserHistory() //这种方式是直接使用 H5 推出的 BOM 上 history 身上的 API,这行代码里定义的 history 最终操作的就是 BOM 上的 history
//方法二
//let history = History.createHashHistory() //这种方式是使用 hash 值(锚点),路由为 http://xx.xxx.xx.x/xx#/xxx,其中 path 放在 # 后面,# 后的资源都不会发给服务器,且锚点跳转不会引起页面刷新,但会留下历史记录,且这种方式对浏览器兼容性极佳
function push(path){
history.push(path)
}
function replace(path){
history.replace(path)
}
function back(){
history.goBack()
}
function forward(){
history.goForward()
}
history.listen((location)=>{
conosle.log('请求路由变化了',location)
})
</script>
3、react 中路由的基本使用
react-router-dom
react-router 库有三种实现,分别给三种平台使用,一种是 react-router-dom 给网页开发应用人员,一种是 native 给 React Native 用 React 做原生应用开发,还有一种是 any 在哪都能用,虽然通用性更强但学习它的相关 API 没有前两种方便
react-router-dom 是 react 的一个插件库,专门用来实现一个 SPA 应用
基于 react 的项目基本都会用到此库
react-router 相关 API
内置组件
<BrowserRouter>
<HashRouter>
<Route>
<Redirect>
<Link>
<NavLink>
<Switch>
其它
history 对象
match 对象
withRouter 函数
react-router-dom 基本使用
需要下载 npm install react-router-dom
这样下载的是最新版本(目前是版本6),也可指定版本下载 npm install react-router-dom@5
在项目的入口文件 index.js 中引入路由器(BrowserRouter 或 HashRouter),该文件内容如下
//引入 react 核心库
import React from 'react'
//引入 ReactDOM
import ReactDOM from 'react-dom'
//引入路由器
import {BrowserRouter} from 'react-router-dom'
//引入 App 组件
import App from './App'
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>,
document.getElementById('root')
)
在组件中引入 Link 和 Route
import {Link,Route} from 'react-router-dom'
在组件中使用,导航区的 a 标签改为 Link 标签,展示区写 Route 标签进行路径的匹配
{/*编写路由链接*/}
<Link to="/xxx">xxx</Link>
{/*注册路由*/}
<Route path="/xxx" component={组件名}/>
路由组件和一般组件
(1)写法不同
一般组件是直接通过 <xxx/>
使用
而路由组件是通过 <Route path="/xxx" component={组件名}/>
进行路由匹配
(2)存放位置不同
一般组件放在 components 文件夹下
路由组件放在 pages 文件夹下
(3)接收到的 props 不同
一般组件写组件标签时传递了什么就能收到什么,若没传属性,this.props 中就为空
而路由组件会收到三个固定的属性,是路由器发送的三个 props,分别是 history、location、match
history:
action:
block:
createHref:
go:
goBack:
goForward:
length:
listen:
location:
push:
replace:
location:
hash:""
key:"随机生成"
pathname:"/xx"
search:
state:
match:
isExact:
params:{}
path:"/xx"
url:"/xx"
其中 history.location 和 直接获取 location 相同
NavLink
若要给 Link 标签对应的导航栏中当前所在路由对应的按钮添加高亮显示,可把 Link 标签替换为 NavLink,在 NavLink 中可添加属性 activeClassName 表示给要高亮显示的标签追加有个样式类(不写该属性的话默认是给 className 中添加 active),然后自己定义这个类对应的高亮样式即可
使用 NavLink
需要现引入
import {NavLink,Route} from 'react-router-dom'
再使用
{/*编写路由链接*/}
<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<NavLink activeClassName="xxx" to="/xxx2">组件2</NavLink>
{/*注册路由*/}
<Route path="/xxx1" component={组件1名}/>
<Route path="/xxx2" component={组件2名}/>
封装 NavLink
可定义一个组件如 MyNavLink
import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component{
render(){
return(
<NavLink activeClassName="xxx" {...this.props}/>
) //这里显式写标签体是因为标签体是特殊的标签属性,它其实是标签的 children 属性,外面传来的所有属性都在 this.props 中
}
}
在其他组件中使用自己定义的 MyNavLink
import MyNavLink from './components/MyNavLink/MyNavLink'
<MyNavLink to="/xxx">标签体</MyNavLink>
<Route path="/xxx" component={组件名}/>
Switch 的使用
使用 <Route path="/xxx" component={组件名}/>
进行路由匹配,若有多个组件匹配同一个路由地址,默认情况下路由地址匹配到一个组件后会继续往下匹配,会都展示,但是若这两行代码中间有很多个其他路由匹配代码,那么效率就很低
因此可利用 Switch 提高路由匹配效率(单一匹配)
在 Route 外包裹 <Switch>
,那么匹配到当前路由的组件后就不会继续往下匹配了
<Switch>
<Route path="/xxx1" component={组件1名}/>
<Route path="/xxx2" component={组件2名}/>
</Switch>
解决样式丢失问题
对于有多级路由的地址(如 /xx1/xx2),若点击浏览器的刷新按钮后会出现 index.html 中引入的 css 文件(如 <link rel="stylesheet" href="./css/bootstrap.css">
)失效的问题
假设 bootstrap.css 文件存放在 public 文件夹(该文件夹其实就是对应 http://localhost:3000 这个地址)下,正常情况下要获取样式访问的是 http://localhost:3000/css/bootstrap.css,而刷新后样式失效是因为此时访问的是 http://localhost:3000/xx1/css/bootstrap.css
解决方式一:修改 public/index.html 中引入样式的地址,去掉前面的 .
,使用根目录(在浏览器中根目录就是 public 文件夹,且对应 http://localhost:3000)
<link rel="stylesheet" href="/css/bootstrap.css">
解决方式二:在 public/index.html 中引入样式的地址使用绝对路径,%PUBLIC_URL%
表示的就是 public 文件夹所在路径
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
解决方式三:修改路由模式,使用 HashRouter(这种解决方式比较少见),在入口文件 index.js 中将 BrowserRouter 换成 HashRouter,这样会在根路径后添加 #,如 http://localhost:3000/#/xx1/xx2,# 后表示都是前端资源
//引入 react 核心库
import React from 'react'
//引入 ReactDOM
import ReactDOM from 'react-dom'
//引入路由器
import {HashRouter} from 'react-router-dom'
//引入 App 组件
import App from './App'
ReactDOM.render(
<HashRouter>
<App/>
</HashRouter>,
document.getElementById('root')
)
路由的模糊匹配与严格匹配
默认是模糊匹配,即输入的路径必须包含要匹配的路径,且顺序要一致
<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<Route path="/xxx1/a/b" component={组件1名}/>
此时访问 /xx1 不能显示
<NavLink activeClassName="xxx" to="/xxx1/a/b">组件1</NavLink>
<Route path="/xxx1" component={组件1名}/>
此时访问 /xx1 可正常显示
<NavLink activeClassName="xxx" to="/a/xxx1/b">组件1</NavLink>
<Route path="/xxx1" component={组件1名}/>
此时访问 /xx1 也不能显示
在注册路由时添加属性 exact={true}
或 exact
就可以开启精准匹配
<Route exact path="/xxx1" component={组件1名}/>
或
<Route exact={true} path="/xxx1" component={组件1名}/>
注意:若模糊匹配时页面可正常显示就不要开启严格匹配,有时开启严格匹配会导致无法继续匹配二级路由
Redirect 的使用
Redirect 是 react-router-dom 的内置组件
一般把 Redirect 写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到 Redirect 指定的路由
引入
import {NavLink,Route,Redirect} from 'react-router-dom'
使用
<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<NavLink activeClassName="xxx" to="/xxx2">组件2</NavLink>
<Route path="/xxx1" component={组件1名}/>
<Route path="/xxx2" component={组件2名}/>
<Redirect to="/xxx1"/>
当路由谁都匹配不上时就去 Redirect 设置的地址
4、嵌套路由(二级路由)的使用
注册子路由时要写上父路由的 path 值
路由的匹配是按照注册路由的顺序进行的,每次路由匹配都是从最开始注册的路由开始过一遍
如一级路由下组件先注册
<NavLink activeClassName="xxx" to="/xxx1">组件1</NavLink>
<NavLink activeClassName="xxx" to="/xxx2">组件2</NavLink>
<Route path="/xxx1" component={组件1名}/>
<Route path="/xxx2" component={组件2名}/>
<Redirect to="/xxx1"/>
二级路由下组件后注册
<NavLink activeClassName="xxx" to="/xxx11">组件11</NavLink>
<NavLink activeClassName="xxx" to="/xxx12">组件12</NavLink>
<Route path="/xxx1/xx11" component={组件11名}/>
<Route path="/xxx1/xx12" component={组件12名}/>
<Redirect to="/xxx1/xx11"/>
当点击【组件1】时路由会自动跳转到 /xxx1/xx11,因为 /xxx1 匹配后,相同一级路由下的【组件11】和【组件12】就会挂载,此时这两个组件都未点击,所以 redirect 到 /xxx1/xx11
当访问 /xxx1/xx11 时,先匹配先注册的 /xxx1,再匹配到 /xxx1/xx11,所以【组件1】和【组件11】的内容都会渲染
5、向路由组件传递参数数据
方式一:向路由组件传递 params 参数
{/*向路由组件传递 params 参数*/}
<Link to={`/xxx1/xx11/x111/${变量1}/${变量2}`}>xxx</Link>
{/*声明接收 params 参数*/}
<Route path="/xxx1/xx11/x111/:id/:title" component={组件名}/>
这些变量在组件中通过 this.props.match.params 获取这些传来的变量
const {id,title} = this.props.match.params
方式二:向路由组件传递 search 参数
{/*向路由组件传递 search 参数*/}
<Link to={`/xxx1/xx11/x111/?id=${变量1}&title=${变量2}`}>xxx</Link>
{/*search 参数无需声明接收,正常注册路由即可*/}
<Route path="/xxx1/xx11/x11" component={组件名}/>
这些变量在组件中通过 this.props.location.search 获取这些传来的search 参数,但收到的是 "?id=xxx&title=xxx"
(key=value&key=value 这种形式称为 urlencoded 编码)的字符串
const {search} = this.props.location
可借助 querystring 库解析,对 urlencoded 编码和对象进行互相转换
import qs from 'querystring'
const {search} = this.props.location
cosnt {id,title} = qs.parse(search.slice(1)) //urlencoded => 对象形式
qs.stringfy(对象) //对象形式 => urlencoded
方式三:向路由组件传递 state 参数
注意这里的 state 是路由组件上独有的 state
{/*向路由组件传递 state 参数*/}
<Link to={{pathname:'/xxx1/xx11/x111',state:{id:变量1,title:变量2}}}>xxx</Link>
{/*state 参数无需声明接收,正常注册路由即可*/}
<Route path="/xxx1/xx11/x11" component={组件名}/>
这些变量在组件中通过 this.props.location.state 接收 state 参数
const {id,title} = this.props.location.state || {} // || {} 是为了若第一次直接访问 /xxx1/xx11/x111 时没有传参时不报错
这种方式在地址栏中没有体现,地址栏中都是 /xxx1/xx11/x111,但刷新也可以保留住参数,因为都在 history 中维护
6、路由跳转的两种模式 push 和 replace
默认是使用 push 模式
可给 Link 标签添加 replace={true}
replace
开启 replace 模式,使用 replace 不会留下痕迹
<Link replace={true} to={`/xxx1/xx11/x111`}>xxx</Link>
或
<Link replace to={`/xxx1/xx11/x111`}>xxx</Link>
7、编程式路由导航
在按钮上绑定 onClick,并定义响应函数如下
replaceShow = (id,title){
//replace 跳转 + 携带 params 参数
//this.props.history.replace(`/xxx1/xx11/x111/${id}/${title}`)
//replace 跳转 + 携带 search 参数
//this.props.history.replace(`/xxx1/xx11/x111/?ID=${id}&title=${title}`)
//replace 跳转 + 携带 state 参数
//this.props.history.replace(`/xxx1/xx11/x111`,{id,title})
}
pushShow = (id,title){
//push 跳转 + 携带 params 参数
//this.props.history.push(`/xxx1/xx11/x111/${id}/${title}`)
//push 跳转 + 携带 search 参数
//this.props.history.push(`/xxx1/xx11/x111/?ID=${id}&title=${title}`)
//push 跳转 + 携带 state 参数
//this.props.history.push(`/xxx1/xx11/x111`,{id,title})
}
back = () => {
this.props.history.goBack()
}
forward = () => {
this.props.history.goForward()
}
go = () => {
this.props.history.go(整数) //正整数表示前进 n 步,负整数表示后退 n 步
}
注意使用 push 或 replace 中携带不同参数则对应的传参方式和接收参数的方式也要改
8、withRouter 的使用
withRouter 可以加工一般组件,让一般组件上也有路由组件中特有的 API(history、location、match)
withRouter 的返回值是一个新组件
在一般组件中
import React, {Component} from 'react'
import {withRouter} form 'react-router-dom'
class 组件名 extends Component {...}
export default withRouter(组件名)
9、BrowserRouter 与 HashRouter 的区别
(1)底层原理不一样
BrowserRouter 使用的是 H5 的 history(this.props.histyor 是 react 对 H5 的 history 的二次封装) API,不兼容 IE9 及以下版本
HashRouter 使用的是 URL 的哈希值
(2)path 表现形式不一样
BrowserRouter 的路径中没有 #,例如 localhost:3000/xxx/xx
HashRouter 的路径包含 #,例如 localhost:3000/#/xxx/xx
(3)刷新后对路由 state 参数的影响
a. BrowserRouter 没有任何影响,因为 state 保存在 history 对象中
b. HashRouter 刷新后会导致路由 state 参数丢失
(4)HashRouter 可用于解决一些路径错误相关的问题(如样式丢失问题)
八、React UI 组件库
(1)国外的 material-ui,可访问官网或GitHub
(3)国内蚂蚁金服 ant-design,可访问官网或GitHub
(3)饿了么团队出的 Element UI for react,Element UI 原先是基于 vue 的
(4)有道团队推出的 vantUI,主要针对移动端(基于 vue)
antd
antd 的基本使用
下载 ant-design
npm install antd --save-dev
如要使用 antd 中封装好的按钮:
要先引入该组件
import {Button} from 'antd' //引入 Button 组件
粘贴官方文档中的使用代码,若要修改属性可查看文档中的 API
<Button type="primary">Primary Button</Button>
引入样式
import 'antd/dist/antd.css'
优化:按需引入样式
高级配置
可参考官方文档
先下载 react-app-rewired(靠这个库启动)和 customize-cra(靠这个库执行规则的修改)
npm install react-app-rewired customize-cra -D
修改 package.json 文件中的 “scripts”
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
}
然后在项目根目录创建一个 config-overrides.js 文件用于修改默认配置
module.exports = function override(config, env) {
//修改或做一些 webpack 配置...
return config;
};
按需引入样式
babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件
下载
npm install babel-plugin-import -D
将 config-overrides.js 文件改为
const { override, fixBabelImports } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
);
此时不要在组件文件中 import 'antd/dist/antd.css'
引入样式
antd 自定义主题
antd 中的样式最初是使用 less 写的然后编译成 css,自定义主题需要用到 less 变量覆盖功能
首先需要下载 less 和 less-loader
npm install less less-loader -D
将 config-overrides.js 文件改为
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
lessOptions:{
javascriptEnabled: true,
modifyVars: { '@primary-color': '#1DA57A' },
}
}),
);
在 addLessLoader 的 lessOptions 中的 modifyVars 里设置主题颜色
九、redux
redux 介绍
redux 是一个专门用于做状态管理的 JS 库(不是 react 插件库)
它可以用在 react、angular、vue 等项目中,但基本与 react 配合使用
作用:集中式管理 react 应用中多个组件共享的状态
什么情况下需要使用 redux
(1)某个组件的状态需要让其他组件可以随时拿到(共享)
(2)一个组件需要改变另一个组件的状态(通信)
(3)总体原则:能不用就不用,若不用比较吃力才考虑使用
redux 工作流程图
Action Creators 负责把动作包装成一个对象
Reducers 负责初始化状态和加工状态
redux 三个核心概念
action
action 是动作的对象,包含两个属性
type:标识属性,值为字符串,唯一,必要属性
data:数据属性,值类型任意,可选属性
如 {type:'ADD_STUDENT',data:{name:'xx',age:18}}
reducer
reducer 用于初始化状态、加工状态
加工时,根据旧的 state 和 action,产生新的 state 的纯函数
store
store 是将 state、action、reducer 联系在一起的对象
如何得到 store 对象
(1)import {createStore} from 'redux'
(2)import reducer from './reducers'
(3)const store = createStore(reducer)
store 对象的功能
(1)getState():得到 state
(2)dispatch(action):分发 action,触发 reducer 调用,产生新的 state
(3)subscribe(listener):注册监听,当产生了新的 state 时,自动调用
redux 的简单使用
下载 npm install redux -D
例子:下拉框中选择数字,按钮有加、减、奇数时加、异步加
(1)创建 src/redux 文件夹
(2)创建 reducers/reducer.js(可以不为此名) 文件,用于创建一个为某组件服务的 reducer
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
const {type,data} = action //从 action 对象中获取 type、data
switch(type){
case 'increment':
return preState + data
case 'decrement':
return preState - data
default:
return preState
}
}
reducer 的本质是一个函数,reducer 函数会接收到两个参数:之前的状态(preState)和动作对象(action)
reducer 第一次被调用是 store 自动触发的,传递的 preState 为 undefined,action 为 {type:@@REDUX/INIT.x.x.x.x}
(3)创建 store.js 文件,该文件专门用于暴露一个 store 对象,整个应用只有一个 store 对象
import {createStore} from 'redux' //引入 createStore 专门用于创建 redux 中最核心的 store 对象
import countReducer from './reducer.js' //引入为 Count 组件服务的 reducer
export default createStore(countReducer) //暴露 store
(4)在要使用 store 的组件中
引入 store
import store form '../../redux/store.js'
在要使用 store 调用 reducer 的地方
store.dispatch({type:'increment',data:1})
在要读取 store 中状态值的地方
store.getState()
监测 redux 中状态的变化,只要变化,就调用 render
componentDidMount(){
store.subscribe(()=>{
this.setState({}) //只要调用 setState 就会自动触发 render
})
}
(5)优化:上面最后一步监测 redux 中状态变化调用 render 可不写在组件中,直接写在入口文件 index.js 中,这样写一次就行不用每个组件都写,在 index.js 中添加如下代码
import store from './redux/store'
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
只要 store 中状态有一个发生改变就调用 render,因为 react 渲染时使用 diff 算法所以这样效率也不会低
注意:redux 只负责状态管理,状态发生变化时不会自动调用 render 重新渲染,需要自己监测并手动调用 render
redux 的完整写法
相比于上述 redux 的简单使用,增加如下部分
(1)在 src/redux 文件夹下创建 constant.js 文件用于定义 action 对象中 type 类型的常量值,便于管理的同时防止程序员单词写错
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
(2)在 src/redux 文件夹下创建 actions/count_action.js(也可以不为此名),该文件专门为 Count 组件生成 action 对象
import {INCREMENT,DECREMENT} from './constant'
export const createIncrementAction = data => {
return {type:INCREMENT,data}
}
export const createDecrementAction = data => ({type:DECREMENT,data}) //这种写法相当于上面的写法,返回的是个对象
(3)在 reducer.js 中同样使用 constant.js 中定义的常量
import {INCREMENT,DECREMENT} from '../constant'
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
const {type,data} = action //从 action 对象中获取 type、data
switch(type){
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
(4)在组件中
引入 actionCreator(专门用于创建 action 对象)
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
在要使用 store 调用 reducer 的地方
store.dispatch(createIncrementAction(1))
异步 action
若 action 的值为 Object 类型的一般对象就是同步 action
若 action 的值是个函数(因为只有函数才能开启异步任务,而数字、数组等不行)就是异步 action,该函数由 store 调用
异步 action 中一般都会调用同步 action
何时需要异步 action?
当延迟的动作不想交给组件自身,想交给 action
想要对状态进行操作,但具体的数据靠异步任务返回时
异步 action 不是必须的,也可以自己在组件中等待异步任务的结果再去分发同步 action
使用异步 action
要使用异步 action 需要安装中间件
npm install redux-thunk
修改 store.js 文件
import {createStore,applyMiddleware} from 'redux' //引入 createStore 专门用于创建 redux 中最核心的 store 对象
import countReducer from './reducer.js' //引入为 Count 组件服务的 reducer
import thunk from 'redux-thunk' //引入 redux-thunk 用于支持异步 action
export default createStore(countReducer,applyMiddleware(thunk)) //执行中间件 thunk 并暴露 store
在 action 文件中
import store from './store'
//异步 action
export const createIncrementAsyncAction = (data,time) => {
return () => {
setTimeout(() => {
store.dispatch(createIncrementAction(data))
},time)
}
}
或
因为异步 action 返回的函数由 store 调用,所以,可不手动引入 store
//异步 action
export const createIncrementAsyncAction = (data,time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
},time)
}
}
在组件中使用异步 action
store.createIncrementAsyncAction(1,1000)
react-redux
react-redux 是 Facebook 出的插件库
react-redux 的模型图如下
所有的 UI 组件都应该包裹一个容器组件,他们是父子关系
容器组件是真正和 redux 打交道的,里面可以随意使用 redux 的 api
UI 组件中不能使用任何 redux 的 api
容器组件会通过 props 传给 UI 组件(1)redux 中保存的状态(2)用于操作状态的方法
利用 react-redux 后一个组件要和 redux 打交道要经过哪几步?
(1)定义 UI 组件 --- 不暴露
(2)引入 connect 生成一个容器组件,并暴露,写法如下
connect(
state => ({key:value}), //映射状态
{key:xxxAction} //映射操作状态的方法
)(UI组件)
(3)在 UI 组件中通过 this.props.xxx 读取和操状态
连接容器组件与 UI 组件
下载 npm install react-redux -D
新建 src/containers 用于存放容器组件,UI 组件依然存放在 src/components 里
在 containers 文件夹中新建一个容器组件的文件夹(如 Count),Count 文件夹下新建 index.js
//引入 Count 的 UI 组件
import CountUI from '../../components/Count'
//引入 action
import {createIncrementAction,createDecrementAction,createIncrementAsyncAction} from '../../redux/count_action'
//引入 connect 用于连接 UI 组件与 redux
import {connect} from 'react-redux'
//mapStateToProps 函数返回的是个对象,返回的对象中的 key 就作为传递给 UI 组件 props 的 key,value 就作为传递给 UI 组件 props 的 value,mapStateToProps 用于传递状态
function mapStateToProps(state){ //react-redux 调用的 a 函数,调用时已经 store.getState() 获取到状态,并作为参数传给 a 函数
return {count:state} //这样相当于 <CountUI count=xxx/>
}
//mapDispatchToProps 函数返回的是个对象,返回的对象中的 key 就作为传递给 UI 组件 props 的 key,value 就作为传递给 UI 组件 props 的 value,mapDispatchToProps 用于传递操作状态的方法
function mapDispatchToProps(dispatch){
return {
inc:number => dispatch(createIncrementAction(number)),
dec:number => dispatch(createDecrementAction(number)),
incAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time))
}
}
//使用 connect()() 创建并暴露一个 Count 的容器组件
export defualt connect(mapStateToProps,mapDispatchToProps)(CountUI)
最后应渲染容器组件,在使用 Count 组件的组件(如 App.js)中
import React,{ Component } from 'react'
import store from './redux/store'
import Count from './containers/Count'
export default class App extends Component{
render(
return(
<div>
<Count store={store}/>
</div>
)
)
}
注意:容器组件中的 store 是靠 props 传进去的,不是在容器组件中直接 import 引入的
优化程序编写
优化 mapDispatchToProps
mapDispatchToProps 可以是个函数也可以是个对象
import CountUI from '../../components/Count'
import {createIncrementAction,createDecrementAction,createIncrementAsyncAction} from '../../redux/count_action'
//引入 connect 用于连接 UI 组件与 redux
import {connect} from 'react-redux'
//使用 connect()() 创建并暴露一个 Count 的容器组件
export defualt connect(
state => ({count:state}),
//mapDispatchToProps 的一般写法
/*dispatch => (
{
inc:number => dispatch(createIncrementAction(number)),
dec:number => dispatch(createDecrementAction(number)),
incAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time))
}
)*/
//mapDispatchToProps 的简写
{
inc:createIncrementAction, //这是因为 react-redux 收到传来的 action 对象能自动掉 dispatch
dec:createDecrementAction,
incAsync:createIncrementAsyncAction
}
)(CountUI)
无需对 store 中状态进行监测重新 render
使用 react-redux 后可删除之前在入口文件 index.js 文件中写的
import store from './redux/store'
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
因为 react-redux 中使用 connect()() 创建容器组件就会自动监测 store 中的状态变化
Provider 组件的使用
之前需要给容器组件传递 store,有几个容器组件就要传几次 store
<Count store={store}/>
利用 Provider 只需写一次即可给所有容器组件传 store,在入口文件 index.js 中
import React from 'react'
import ReactDOM from 'react-dom'
import {HashRouter} from 'react-router-dom'
import App from './App'
import store from './redux/store'
import {Provider} from 'react-redux'
ReactDOM.render(
<Provider store={store}> /*Provider 让该组件的所有后代容器组件都能接收到 store*/
<App/>
</Provider>,
document.getElementById('root')
)
整合 UI 组件与容器组件
有多少需要和 redux 关联的 UI 组件就需要写多少容器组件,目前文件太多,从文件层面进行优化,可以把 UI 组件和容器组件写在一个文件中
import React, {Component} from 'react'
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction
} from '../../redux/count_action'
//引入 connect 用于连接 UI 组件与 redux
import {connect} from 'react-redux'
//定义 UI 组件
class CountUI extends Component{...} //UI 组件中通过 this.props.xxx 来使用容器组件中从 redux 获取的状态和操作状态的方法
//使用 connect()() 创建并暴露一个 Count 的容器组件
export defualt connect(...)(CountUI)
react-redux 使用的最终版
(1)新建 src/containers/组件名/index.js,
import React,{ Component } from 'react'
import { createIncrementAction } from '../../redux/count_action'
import {connect} from 'react-redux'
class Count extends Component{
add = () => {
this.props.inc(1)
}
render(
return(
<div>
<h1>{this.props.num}</h1>
<button onClick={this.add}>加一</button>
</div>
)
)
}
export defualt connect(
state => ({num:state}), //映射状态
{inc:createIncrementAction}//映射状态的方法
)(Count)
多个组件间的数据共享
假设有两个组件 Count 和 Person,且已写好两个组件相关的 action 和 reducer,Count 组件中可点击+1,Person 组件可添加一个人的信息
新建 src/reducers/index.js 文件用于汇总所有的 reducer 为一个总的 reducer
import {combineReducers} from 'redux'
//引入为 Count、Person 组件服务的 reducer
import count from './reducers/count'
import person from './reducers/person'
//汇总所有 reducer 变为一个总的 reducer
export default combineReducers({ //这里传入的对象就是 store 中存的对象
num:count,
person:person
})
修改 store.js 文件
import {createStore,applyMiddleware} from 'redux'
import reducer from './reducers/index.js'
import thunk from 'redux-thunk'
export default createStore(reducer,applyMiddleware(thunk)) //执行中间件 thunk 并暴露 store
两个 reducer 进行状态传递时应进行修改,如 Count 中
export default connect(
state => ({count:state.num,person:state.person})//注意这里不是之前的 state => ({count:state}),因为之前 state 中存只是个数字,现在 state 中存的是个对象,用来存放很多组件的状态
)(Count)
纯函数
纯函数:只要同样的输入(实参),必定得到同样的输出(返回)
纯函数必须遵守以下一些约束:
(1)不得改写参数数据,如 function func(a){a=1}
(2)不会产生任何副作用,例如网络请求(可能会断网等),输入和输出设备
(3)不能调用 Date.now() 或 Math.random() 等不纯的方法
redux 的 reducer 函数必须是一个纯函数,如
export default function personReducer(preState=initState,action){
const {type,data} = action
switch(type){
case ADD_PERSON:
return [data,...preState] //注意这里不能使用 preState.unshift(data),因为这样导致 preState 被改写了,personReducer 就不是纯函数了
default:
return preState
}
}
redux 开发者工具
在 Chrome 浏览器中安装插件 Redux DevTools
在项目中下载 redux-devtools-extension
npm install redux-devtools-extension -D
在 src/redux/store.js 文件中添加
//引入
import {composeWithDevTools} from 'redux-devtools-extension'
//修改原先的 export default...
export default createStore(reducer,composeWithDevTools(applyMiddleware(thunk)))
十、项目打包运行
执行 npm run build
生成文件夹 build
全局安装 serve 来查看打包项目的运行效果
npm i serve -g
serve build
十一、扩展
setState 更新状态的两种写法
setState 是同步的,setState 引起 React 后续更新状态的动作是异步的
若更改完 state 状态后要马上输出查看新的 state 要在回调函数中查看,若在外部查看输出依然是旧值
(1)setState(stateChange, [callback])————对象式的 setState
stateChange 为状态改变对象(该对象可体现出状态的更改)
callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render 调用后)才被调用
如 this.setState({count:count+1})
(2)setState(updater, [callback])————函数式的 setState
updater 为返回 stateChange 对象的函数,可接收 state 和 props
callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render 调用后)才被调用
如 this.setState((state,props) => {
console.log(state,props)
return {count:state.count+1}
})
总结:对象式的 setState 是函数式的 setState 的简写方式(语法糖)
使用原则:
(1)若新状态不依赖原状态————使用对象方式
(2)若新状态依赖于原状态————使用函数/对象方式
(3)若需要在 setState() 执行后获取最新状态数据,要在第二个 callback 函数中读取
lazyLoad
做懒加载最多的是路由组件 lazyLoad
import React, { Component,lazy } from 'react'
//通过 React 的 lazy 函数配合 import() 函数动态加载路由组件 ==> 路由组件代码会被分开打包
const Login = lazy(()=>import('@/pages/Login'))
//通过 <Suspense> 指定在加载得到路由打包文件前显示一个自定义 loading 界面,当网速慢时会先显示 fallback 中的东西
<Suspense fallback={<h1>Loading...</h1>}>
<Switch>
<Route path="/xx" component={Xxx}>
<Redirect to="/login/">
</Switch>
</Suspense>
Hooks
Hook 是 React 16.8.0 版本增加的新特性/新语法,使得在函数组件中也可以使用 state 以及其他 React 特性
三个常用的 Hook
State Hook:React.useState()
State Hook 让函数组件也可以有 state 状态,并进行状态数据的读写操作
语法:const [xxx,setXxx] = React.useState(initValue)
useState() 中
参数:第一次初始化指定的值在内部作缓存
返回值:包含 2 个元素的数组,第 1 个为内部当前状态值,第 2 个为更新状态值的函数
setXxx() 两种写法
setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
setXxx(value => newValue):参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值
例子
import React from 'react'
function Demo(){
const [count,setCount] = React.useState(0)
function add(){
setCount(count+1)//或 setCount(count => count+1)
}
return(
<h1>{count}</h1>
<button onClick={add}>加一</button>
)
}
export default Demo
Effect Hook:React.useEffect()
Effect Hook 让函数组件也可以执行副作用操作(副作用操作用于模拟类组件中的生命周期钩子)
React 中的副作用操作:
发 ajax 请求数据获取
设置订阅、启动定时器
手动更改真实 DOM
语法:
React.useEffect(()=>{
//在此可以执行任何带副作用操作
return () => { //组件卸载前执行
//在此做一些收尾工作,如清除定时器/取消订阅
}
},[stateValue]) //第二个参数表示监测的值,若为 [],回调函数只会在第一次 render() 后执行
可以把 useEffect Hook 看作三个函数的组合:componentDidMount、componentDidUpdate、componentWillUnmount
React.useEffect() 该函数相当于 componentDidMount 和 componentDidUpdate,具体是哪个取决于传入的第二个参数,若为空数组[],则相当于 componentDidMount,若数组中有要监测变化的值[xxx],则相当于 componentDidUpdate
React.useEffect() 中若返回函数,返回的函数相当于 componentWillUnmount
例子
import React from 'react'
function Demo(){
const [count,setCount] = React.useState(0)
React.useEffect(()=>{
let timer = setInterval(()=>{
setCount(count => count + 1)
},1000)
return ()=>{
clearInterval(timer)
}
},[])
function unmount(){
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return(
<h1>{count}</h1>
<button onClick={unmount}>卸载组件</button>
)
}
export default Demo
Ref Hook:React.useRef()
Ref Hook 可在函数组件中存储/查找组件内的标签或任意其他数据
语法:const refContainer = useRef()
作用:保存标签对象,功能与 React.createRef() 一样
例子
import React from 'react'
function Demo(){
const [count,setCount] = React.useState(0)
const myRef = React.useRef()
function show(){
alert(myRef.current.value)
}
return(
<input type="text" ref={myRef}>
<h1>{count}</h1>
<button onClick={show}>点击提示input中输入的数据</button>
)
}
export default Demo
Fragment
在组件中 jsx 语法一定要求只能有一个根标签,这样使得在组件嵌套后会有很多没用的根标签,而使用 Fragment 作为根标签在实际渲染时会忽略
import React, { Component,Fragment } from 'react'
export deffault clss Demo extends Component{
render(
return(
<Fragment>
<input type="text"/>
<input type="text"/>
</Fragment>
)
)
}
<Fragment></Fragment>
或
<></>
<Fragment>
可以添加 key 属性,用于遍历
可以不用必须有一个真实的 DOM 根标签了,而 <>
空标签里不能添加任何属性
Context
Context 是一种组件间通信方式,常用于【祖组件】与【后代组件】间的通信
使用:
(1)在祖孙组件都能访问到的地方创建 Context 容器对象
const xxxContext = React.createContext() //xxxContext 首字母需要大写,因为后面它还会作为组件标签使用
(2)在祖组件渲染子组件时外面包裹 xxxContext.Provider,通过 value 属性给后代组件传递数据
<xxxContext.Provider value={数据}>
子组件
</xxxContext.Provider>
(3)后代组件读取数据
//方式一:仅适用于类组件
static contextType = xxxContext //声明接收 context
this.context //读取 context 中的value 数据
//方式二:函数组件与类组件都可以
<xxxContext.Consumer>
{
value => { //value 就是 context 中的 value 数据
//要显示的内容
}
}
</xxxContext.Consumer>
注意:在应用开发中一般不用 context,一般都用它的封装 react 插件(react-redux 中的 Provider)
例子:A 组件是 B 组件的父组件,B 是 C 的父组件,要让 C 不通过 B,直接拿到 A 的数据
import React, { Component } from 'react'
//创建 Context 对象
const MyContext = React.createContext()
const {Provider,Consumer} = MyContext
export default class A extends Component{
state = {username:'xx',age:18}
render(
const {username} = this.state
return(
<div>
<Provider value={{username:username,age:age}}>
<B/>
</Provider>
</div>
)
)
}
class B extends Component{
render(
return(
<div>
<C/>
</div>
)
)
}
class C extends Component{
render(
static contextType = MyContext
return(
<div>
<h1>{this.context.username}</h1>
</div>
)
)
}
//若 C 是函数式组件
function C(){
return (
<div>
<Consumer>
{
value => {
return `${value.username},${value.age}`
}
}
</Consumer>
</div>
)
}
PureComponent
Component 的 2 个问题:
(1)只要执行 setState() 即使不改变状态数据,组件也会重新 render()
(2)只当前组件重新 render(),就会自动重新 render 子组件,就算子组件没有用到父组件的任何数据也会触发子组件重新 render,这会导致效率低
效率高的做法:只有当组件的 state 或 props 数据发生改变时才重新 render()
原因:Component 的 shouldComponentUpdate() 总是返回 true
解决:
办法1:重写 shouldComponentUpdate() 方法
比较新旧 state 或 props 数据,若有变化才返回 true,若没有返回 false
在父子组件中重写:
shouldComponentUpdate(nextProps,nextState){
console.log(this.props,this.state) //目前的 props 和 state
console.log(nextProps,nextState) //接下来要变化的新 props 和 state、
return !this.state.xx === nextState.xxx
}
办法2:使用 PureComponent
PureComponent 重写了 shouldComponentUpdate(),只有 state 或 props 数据有变化才返回 true
注意:只是进行 state 和 props 数据的浅比较,只有对象地址改变时返回 true,若只是数据对象内部数据变了,返回 false,所以不要直接修改 state 数据,而是要产生新数据
用法如下:
import React, { PureComponent } from 'react'
class 组件 extends PureComponent{...}
项目中一般使用 PureComponent 来优化
render props
html 标签里的标签体内容能直接展示,而自定义组件中的标签体(如 <自定义组件>xxx</自定义组件>
)不能直接展示,而是作为 this.props.children 属性的值
如何想组件内部动态传入带内容的结构(标签)?
在 Vue 中,使用 slot 技术,即通过组件标签体传入结构 <A><B/></A>
在 React 中
使用 children props:通过父组件标签体传入结构(子组件)
使用 render props:通过组件标签属性传入结构,来形成父子关系,,且可携带数据,一般用 render 的函数属性
(1)childer props
在 A 的父组件中形成 AB 的父子关系
<A>
<B>xxx</B>
</A>
在 A 组件中通过如下代码展示 B 组件
{this.props.children}
但是若 B 组件需要 A 组件内的数据则做不到
(2)render props
在 A 的父组件中形成 AC 的父子关系
<A render={(data)=><C data={data}></C>}></A>
A 组件中渲染 C 组件并给 C 传数据
{this.props.render(要给 C 传的数据)}
C 组件中读取 A 组件传入的数据显示
{this.props.data}
ErrorBoundary 错误边界
错误边界(ErrorBoundary)用来捕获后代组件错误,渲染出备用页面
错误边界就是让错误控制在一定范围内不要往外扩散,子组件出错父组件依然能正常渲染
错误边界只适用于生产环境
特点:只能捕获后代组件生命周期函数中产生的错误,普通函数中不行,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式:getDerivedStateFromError 配合 componentDidCatch
state = {
hasError:'' //用于标识子组件是否产生错误
}
//声明周期函数,一旦后代组件报错,就会触发,并携带错误信息
static getDerivedStateFromError(error){
console.log(error)
//在 render 之前触发
//返回新的 state
return {hasError: true}
}
render(){
return(
<div>
{this.state.hasError?<h2>当前网络不稳定,稍后再试</h2>:<子组件/>}
</div>
)
}
componentDidCatch(error,info){ //当页面中子组件出现问题时该钩子会被调用
//统计页面的错误,发送请求发送到后台去,用于通知编码人员解决 bug
console.log(error,info)
}
十二、组件通信方式总结
组件间的关系:
父子组件
兄弟组件(非嵌套组件)
祖孙组件(跨级组件)
通信方式:
(1)props
children props
render props
(2)消息订阅-发布
pubs-sub、event(C#中用的多) 等
(3)集中式管理
redux、dva 等
(4)conText
生产者-消费者模式
比较好的搭配方式:
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)
十三、ReactRouter 6
React Router 以三个不同的包发布到 npm 上,分别为:
(1)react-router:路由的核心库,提供很多组件、钩子
(2)react-router-dom:包含 react-router 所有内容,并添加到一些专门用于 DOM 的组件,如 <BrowserRouter> 等
(3)react-router-native:包括 react-router 所有内容,并添加一些专门用于 ReactNative 的 API,如 <NativeRouter> 等
相比 ReactRouter 5.x 的变化
(1)内置组件的变化:移除 <Switch/>
,新增 <Routes/>
等
(2)语法的变化:component={组件}
变为 element={<组件名/>}
等
(3)新增多个 hook:useParams、useNavigate、useMatch 等
(4)官方明确推荐函数式组件了
<Routes/>
与 <Route/>
(1)v6 版本中移除了 <Switch>
,引入了替代者 <Routes/>
(2)<Routes/>
和 <Route/>
要配合使用,且必须要用 <Routes/>
包裹 <Route/>
(3)<Route/>
相当于一个 if 语句,若其路径与当前 URL 匹配,则程序对应组件
(4)<Route caseSensitive/>
属性用于指定匹配时是否区分大小写(默认为 false)
(5)当 URL 发生变化时,<Routes/>
都会查看其所有子 <Route/>
元素以找到最佳匹配并呈现组件
(6)<Route/>
也可以嵌套使用,且可配合 useRoutes() 配置“路由表”,但需要通过 <Outlet/>
组件来渲染其子路由
import {NavLink,Routes,Route,Navigate} from 'react-router-dom'
<Routes> {/*功能和 Switch 一样(路由一旦匹配到就不继续往下匹配),但必须包裹 Routes*/}
<Route path='/about' element={<About/>}>
<Route path='/xx' element={<Xxx/>}> {/*嵌套路由,/about/xx*/}
</Route>
<Route path='/' element={<Navigate to="/about"/>}/> {/*没有 Redirect 了,而是换成 Navigate,且 Navigate 组件一旦被渲染一定会触发视图切换*/}
</Routes>
<Navigate/>
重定向
作用:只要 <Navigate/>
组件被渲染,就会修改路径,切换视图
replace 属性用于控制跳转模式(push 或 replace,默认是 false)
import React,{useState} from 'react'
import {NavLink,Routes,Route,Navigate} from 'react-router-dom'
export default function About(){
const [sum,setSum] = useState(1)
return(
<div>
{sum === 1 ? <h2>xxx</h2> : <Navigate to="about" replace={true}/>}
<button onClick={()=>setSum(2)}>点击后sum变为2</button>
</div>
)
}
<NavLink/>
高亮
在 v5 中高亮是使用 <NavLink activeClassName="xxx"/>
来控制标签高亮
在 v6 中废弃了 activeClassName 属性,将要高亮的类也添加在 className 中,值为一个函数
<NavLink className={({isActive})=>isActive?'定义的高亮样式类':''} to="/about">About</NavLink>
上述方式在每个有可能高亮的组件中都要使用较多重复代码,所以可以单独定义成一个函数
function computedClassName({isActive}){
return isActive?'定义的高亮样式类':''
}
在 NavLink 标签中使用
<NavLink className={computedClassName} to="/about">About</NavLink>
useRoutes 路由表
新建 src/routes/index.js
import {Navigate} from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'
export default[
{
path:'/about',
element:<About/>
},
{
path:'/home',
element:<Home/>
},
{
path:'/',
element:<Navigate to="/about"/>
}
]
在组件中
import React from 'react'
import {NavLink,useRoutes} from 'react-router-dom'
import routes from './routes'
export default function App(){
//根据路由表生产对应的路由规则
const element = useRoutes(routes)
return (
<div>
{element}
</div>
)
}
嵌套路由
当 <Route>
产生嵌套时,渲染其对于的后续子路由使用 <Outlet />
假设 <News/>
和 <Message/>
是 <Home/>
的子组件,他们在 <Home/>
所在路由的二级路由下
修改 src/routes/index.js 文件
import {Navigate} from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/News'
import Message from '../pages/Message'
export default[
{
path:'/about',
element:<About/>
},
{
path:'/home',
element:<Home/>,
children:[
{
path:'news',
element:<News/>
},
{
path:'message',
element:<Message/>
}
]
},
{
path:'/',
element:<Navigate to="/about"/>
}
]
在 <Home/>
组件中
import {NavLink,Outlet} from 'react-router-dom'
{/* 路由链接,end 属性表示若子级路由匹配上则父级路由不高亮 */}
<NavLink to="news">News</NavLink>{/* News 组件对应的是 /home/news 路径,这里要么两级路由写完整,要么只写第二级且不加 /,若加 / 表示从根目录开始,或使用 ./news 表示从当前路径开始加 */}
<NavLink end to="message">Message</NavLink>
{/* 指定路由组件呈现位置 */}
<Outlet />
路由的 params 参数
在 src/routes/index.js 文件中
import {Navigate} from 'react-router-dom'
import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/News'
import Message from '../pages/Message'
export default[
{
path:'/about',
element:<About/>
},
{
path:'/home',
element:<Home/>,
children:[
{
path:'news',
element:<News/>
},
{
path:'message',
element:<Message/>
children:[
{
path:'detail/:id/:title', //携带params 参数形式
element:<Detail/>
},
{
path:'message',
element:<Message/>
}
]
}
]
},
{
path:'/',
element:<Navigate to="/about"/>
}
]
在组件中
<NavLink to={`detail/${变量1}/${变量2}`}>xxx</NavLink>
<Outlet />
在接收 params 路由参数的组件中(使用函数式定义的情况下)可使用 useParams 或 useMatch
import {useParams,useMatch} from 'react-router-dom'
export default function Detail(){
//方式一:
const {id,title} = useParams()
//方式二:
const x = useMatch('/home/message/detail/:id/:title')
console.log(x)
return (
<ul>
<li>{id}</li>
<li>{title}</li>
</ul>
)
}
路由的 search 参数
在组件中
<NavLink to={`detail?id=${变量1}&title=${变量2}`}>xxx</NavLink>
<Outlet />
在接收 search 路由参数的组件中(使用函数式定义的情况下),可使用 useSearchParams(返回包含两个值的数组[search参数,更新search的函数]) 或 useLocation
import {useSearchParams,useLocation} from 'react-router-dom'
export default function Detail(){
//方式一:
const [search,setSearch] = useSearchParams()
const id =search.get('id')
const title = search.get('title')
//方式二:
const x = useLocation()
console.log(x)
return (
<ul>
<li>{id}</li>
<li>{title}</li>
<li><button onClick={()=>setSearch('id=111&title=xxx')}>点击更新收到的 search 参数</button></li>
</ul>
)
}
路由的 state 参数
在组件中
<NavLink to="detail" state={{id:变量1,title:变量2}}>xxx</NavLink>
<Outlet />
在接收 state 路由参数的组件中(使用函数式定义的情况下)
import {useLocation} from 'react-router-dom'
export default function Detail(){
const {state:{id,title}} = useLocation()
return (
<ul>
<li>{id}</li>
<li>{title}</li>
</ul>
)
}
编程式路由导航
使用 useNavigate 实现路由跳转
import React from 'react'
import {Link,Outlet,useNavigate} from 'react-router-dom'
export default function Message(){
const navigate = useNavigate()
function showDetail(m){
navigate('detail',{
replace:false,
state:{
id:m.id, //假设 m 传来是个对象
title:m.title
}
}) //触发showDetail函数就会发生路由跳转
}
return (
<ul>
<li><button onClick={()=>showDetail(数据)}>查看详情</button></li>
</ul>
)
}
对于一般组件
import React from 'react'
import {useNavigate} from 'react-router-dom'
export default function Header(){
const navigate = useNavigate()
function back(m){
navigate(-1)
}
function forward(m){
navigate(1)
}
return (
<div>
<button onClick={()=>back(数据)}>后退</button>
<button onClick={()=>forward(数据)}>前进</button>
</div>
)
}
一些其他较不常用的 Hooks
(1)useInRouterContext()
作用:若组件在 <Router>
的上下文中呈现,则 useInRouterContext 钩子返回 true,否则返回 false
(2)useNavigationType()
作用:返回当前的导航类型(用户是如何来到当前页面的)
返回值:POP、PUSH、REPLACE
备注:POP 是指在浏览器中直接打开了这个路由组件(是刷新当前页面)
(3)useOutlet()
作用:用来呈现当前组件中要渲染的嵌套路由组件
const result = useOutlet()
console.log(result)
//若嵌套路由没有挂载,则 result 为 null
//若嵌套路由已经挂载,则显示嵌套的路由对象
(4)useResolvedPath()
作用:给定一个 URL 值,解析其中的 path、search、hash 值
其他
defaultChecked 和 checked
勾选框 checkbox 初始时是否勾选可使用 defaultChecked(只能在显示初始值时起作用),这样设置后可修改值
<input type="checkbox" defaultChecked=xxx?true:false>
第一次显示时有效,但当 xxx 变量值发生改变时,checkbox 的勾选情况不会随着改变
或者使用 checked + onChange 来初始和监听是否勾选,这样可实时修改勾选情况
React 中定义的一些事件
onClick:点击
onBlur:失去焦点
onKeyUp:键盘按下松开
event.KeyCode:按下键盘按键的编码(回车的 KeyCode 是 13)
onMouseLeave:鼠标移入
onMouseEnter:鼠标移出
id 生成
生成数据 id,可通过 uuid 库(比较大)或 nanoid 库(较小,推荐)
npm i nanoid
import {nanoid} from 'nanoid' //暴露的 nanoid 是个函数,可生成全世界唯一的字符串
const Obj = {id:nanoid(),name:'xx'}
确认对话框
若点击按钮需要弹出确认对话框当用户点击“确定”后方可执行按钮事件可利用 window 下的 confirm 函数
if(window.confirm('提示语句')){执行相应代码}
Chrome 中插件
FeHelper 可帮助整理 json 数据缩进显示
解构赋值
解构赋值
let {对象中的属性} = 对象
连续解构赋值
let {对象中的属性对象:{属性对象中的属性}} = 对象
let obj = {a:{b:{c:1}}}
const {a:{b:{c}}} = obj
console.log(c) //输出 1
连续解构赋值并重命名
let {对象中的属性:新名字} = 对象
let obj = {a:{b:1}}
const {a:{b:data}} = obj
console.log(data) //也能输出 1