react从入门到入魔

  • 友情提示
  • React的基本使用
  • 创建虚拟dom的两种方式
    • 使用jsx创建虚拟dom
    • 使用原生js创建虚拟dom
  • 虚拟dom是什么
  • 什么是jsx
    • jsx简介
    • jsx语法规则
  • react中的组件
    • 函数组件
    • 类式组件
  • 组件的三大核心属性
    • state
      • 对state的基本理解
      • state的基本使用
      • react中事件绑定的基本使用
      • 更新state状态
      • state的简写状态
      • state总结
    • props
      • props的使用场景
      • props的用法
      • react’插槽’用法
      • 批量传递props
      • 对props进行限制
      • 使用场景
      • props类型限制的基本使用
      • props类型限制的简单写法
  • 类式组件中的构造器
  • 函数式组件的props
  • refs与事件处理
    • refs的基本使用
    • 回调形式的refs
    • api方式创建ref
  • 组件生命周期
    • 生命周期图示 (旧 15.X之前的版本)
    • 普通组件生命周期
    • 更新状态生命周期
      • 普通更新
      • 强制更新
    • 父组件更新子组件状态
    • 卸载组件
  • 生命周期图示(新)
    • 组件挂载时
    • 组件更新时
      • 普通更新
      • getSnapShotBeforeUpdate使用场景
  • diffing算法
    • diffing算法与虚拟dom
    • 列表渲染中key的作用
      • 为什么不能使用index来作为列表渲染的key?
  • react脚手架
    • 创建一个简单的hello world组件
      • 模块化组件样式文件(解决样式冲突)
    • 实现一个简单的react TODOlist实??
  • 什么是SPA(单页面应用)
    • react路由的基本使用
      • 安装react-router-dom
    • 路由组件和一般组件
    • navLink的使用
    • switch的基本使用
    • 路由的严格匹配和模糊匹配
    • redirect(重定向)
    • 嵌套路由
    • 路由传参的三种方式
      • 向路由传递params参数
      • 向路由传递search参数
      • 向路由传递state参数
    • push跳转和replace跳转
    • 编程式路由导航
      • 编程式跳转发送params参数
      • 编程式导航发送search参数
      • 编程式路由导航发送state参数
    • withRouter的基本使用(使一般组件变成路由组件)
    • BroswerRouter和HashRouter
  • Redux
    • redux 工作流程图
    • redux的三个核心概念
      • action
      • reducer
      • store
    • redux实例??
    • 状态全局化
      • 创建全局store
      • 创建reducers
      • 在组件中使用
      • 改造实例
      • 效果展示
      • 补充说明
    • 异步action
  • react-redux
    • react-redux关系图示
    • 连接组件和store
      • 连接容器组件和UI组件
      • 使用容器组件
      • 完善react-redux
      • mapDispatchToProps的简写形式
      • redux模块化
      • react-redux的优化

友情提示

本文全篇5w字+,胆小者慎入! react新手入门指南手册!

React的基本使用

直接上hello word案例

 <!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">Iberia<meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>hello react</title>
</head><body><div id="app"></div><!-- react核心库,提供react语法  引入后暴露一个全局变量React--><script src="../js/react-18.js"></script><!-- react扩展库,提供react操作dom的能力 引入后暴露一个全局变量ReactDOM --><script src="../js/react-dom.js"></script><!-- 引入babel.js,将ES6语法转换成浏览器支持的ES5语法,这里主要是用于将JSX语法转换成浏览器支持的JS --><script src="../js/babel.js"></script><script type="text/babel">// 1、创建虚拟domconst VDOM = <h1>hello react</h1>ReactDOM.render(VDOM, document.getElementById('app'))</script>
</body>
</html>

案例中把所需要的react依赖库,react、react-dom、babel都下载到本地了这样页面加载速度会更快,cdn地址如下

 <script src="https://unpkg.com/react@17/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

tips:
1.需要注意的是,react-dom需要在react库之后再引入,因为react-dom依赖了react核心库中的一些变量
2.注意最后一个script标签里面是text/babel而不是text/script
3.

const VDOM = <h1>hello react</h1>

虚拟dom赋值语句不用加引号包起来,因为用的是jsx语法,不是js语法

创建虚拟dom的两种方式

使用jsx创建虚拟dom

使用jsx创建虚拟dom的写法和直接写html一样,十分简单

 const VDOM = <h1 id='title'>hello react</h1>

使用原生js创建虚拟dom

使用原生js则需要用到React暴露的一个专门用来创建虚拟dom的api-----createElement。具体用法入下

 const VDOM = React.createElement(标签名,标签属性,标签内容)

以创建一个同样的上例中的h1标签为例

  const VDOM = React.createElement('h1', { id: 'title' }, '我是一个h1标签')

相比此时还不太能看出jsx创建虚拟dom的优越性,那么此时有如下的需求 h1标签里面有个span标签里面有一个a标签
使用jsx语法创建如下

  const VDOM = <h1 id='title'><span><a href="/">这就是一个链接</a></span></h1>

使用原生js创建如下

 const VDOM = React.createElement('h1', { id: 'title' }, React.createElement('span', {}, React.createElement('a', {}, '这就是一个链接')))

而在实际项目开发中,嵌套三四层的html结构是很常见的,相比较使用jsx创建虚拟dom,原生js显得臃肿而繁琐,所以在实际项目中,我们一般使用jsx语法来创建虚拟dom。

虚拟dom是什么

我们使用jsx创建一个Vdom之后再在控制台输出它。

     const VDOM = <h1 id='title'><span><a href="/">这就是一个链接</a></span></h1>ReactDOM.render(VDOM, document.getElementById('app'))console.log(VDOM)

控制台输出如下

显而易见,是一个普通的对象。那么为什么要用虚拟dom呢,它和真实dom又有什么区别呢?
在控制台输出一个真实dom。由于chrome对dom结构的输出策略不同,因此我们在打印dom结构时有一点点小细节需要注意

        console.log([document.querySelector('#app')]) //这里把dom对象放到数组里面 以便在控制台可以正常查看

控制台打印的真实dom如下

总结:
1、虚拟dom本质上是一般对象
2、虚拟dom比较轻量,因为它是react内部在使用,不需要真实dom中冗余且不必要的属性
3、虚拟dom最终会被react转换成真实dom展示在页面上

什么是jsx

jsx简介

1、全称 JavaScript XML ,react定义的一种类似于XML的JS扩展语法
2、本质是React.createElement的语法糖,jsx语法就是用来简化创建虚拟dom的过程

jsx语法规则

1、定义html标签时不需要加引号
2、标签里在需要混入js表达式的地方使用“{}”包起来
3、在jsx定义的标签中 样式类 class替换成className(为了避免es6推出的类关键词冲突) 样式style需要加上{}使之变成js表达式
打一个栗子

 const VDOM = <h1 className={`${className} other`} style={{ color: 'red' }}>18xsa</h1>

4、jsx定义的虚拟dom只允许有一个根标签
5、jsx定义标签名,如果是小写字母开头,渲染成html同名标签,如果不存在,则报错。如果是大写字母开头,渲染成react组件

细心的小伙伴发现了 在jsx定义虚拟dom的{}中只能写js的表达式,那么什么是JS表达式呢
来看个例子
如果后端返回的数据格式如下

 const resArr = [{studentName: '张三'}, {studentName: '李四'}, {studentName: '王五'}]

我们应该如何把它渲染到页面中呢?

其实还不用打开网页,编译器就已经提示报错了,{}内需要一个表达式

主要区别
表达式可以产生一个值,可以放在任何一个需要值的地方
其实以我个人理解就是,可以放在等号右边的 就是js表达式。

所以如果我们需要渲染一个数组到页面,可以使用数组的map方法

 const VDOM = (<div><ul>{resArr.map(item => (<li>{item.studentName}</li>))}</ul></div>)

写到这里可以在网页看到数组已经正常渲染出来了,控制台会报出一个错误与数组渲染无关,它是指数组渲染到页面必须要加上唯一值key,在之后的细讲虚拟dom的时候会讲到这个问题,这里只提供解决方法,不做深究。

 resArr.map(item => (<li key={index}>{item.studentName}</li>))

react中的组件

函数组件

函数式组件的基本使用如下

 function ComponentDemo(){return `我是一个函数式组件,适用于一些简单的场景`}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

需要注意的是因为react会将首字母小写的标签名转换成html同名标签,找不到同名标签则会报错,因此,这里使用函数定义首字母需要大写。

类式组件

 class ComponentDemo extends React.Component{render(){return (<div>我是一个类式组件,用来定义复杂组件</div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

ReactDOM.render在渲染一个函数式组件之后,发生了什么 1、react解析组件标签,找到了MyComponent组件 2、如果是类式组件,通过new关键字新建一个类实例,并调用了类的render方法 3、将render中的虚拟dom转换成真实dom,呈现到页面中

组件的三大核心属性

state

对state的基本理解

可以看到,我们在上一节通过function创建了一个简单组件,通过class创建了一个复杂组件,那么什么是简单组件,什么是复杂组件呢?
这里直接给出定义
有状态的就是复杂组件,无状态的就是简单组件。

state的基本使用

将组件改写成如下的形式

 class ComponentDemo extends React.Component {constructor(props) {super(props)this.state = {isHot: true}}render() {const { weather } = this.statereturn (<div>今天的天气热吗?{this.state.isHot ? '热' : '冷'}</div>)}}

这里用到了组件三大核心属性之二的 props,因为是根据官网示例来写的,不写props会报错。所以不做深究。
可以看到页面中的展示文案跟状态中ishot的值有关。ishot为true时页面展示如下

如果将ishot的值改为false,此时页面展示如下

react中事件绑定的基本使用

直接看案例

 changeState() {console.log(1111)}render() {const { weather } = this.statereturn (<div onClick={this.changeState}>今天的天气热吗?{this.state.isHot ? '热' : '冷'}</div>)}

需要注意的是
1、react对所有的原生事件进行了封装,封装后的事件为on+首字母大写事件名
2、事件后直接加函数名,不需要加小括号,添加的函数名会被react内部处理成事件的回调

更新state状态

从上例中我们看出,页面展示和state的状态有关。在这一小节中我们要实现一个点击更新state从而更新页面的功能。
第一想法直接上手

changeState() {this.state.isHot = !this.state.isHot}

如果修改函数如上所示,控制台不出意外的会报错。

错误信息,找不到undefined中的属性state
修改函数输出this

 changeState() {console.log(this)}


可以看到在此函数中,控制台打印出的this是undefined。

这里给出答案,类中的方法默认使用的是严格模式。
在严格模式中,类中函数的this是不会指向类实例。render函数是react内部调用的,react在内部进行的处理,所以除render函数之外的其他函数中的this都不会指向类实例。解决方法:使用ES6箭头函数
修改后的函数如下

changeState = () => {this.state.isHot = !this.state.isHotconsole.log(this.state.isHot)}

控制台的打印输出显示state的状态确实有更新。

但是页面并没有更新
原因是react有自己定义的方法来更新state,其他方法更新的state都不会引发页面更新。

 changeState = () => {this.setState({isHot: !this.state.isHot})}

修改函数如上所示后便可以正常看到页面更新啦!
总结:在react中使用绑定事件时,如果需要用到this,需要将函数改写成箭头函数。state中的状态值不能直接更新,需要用到react提供的api,setState。

state的简写状态

可以看到上例中我们是在类的构造函数中定义的state,而在实际项目开发中,往往是在组件类中直接定义state。如下所示

class ComponentDemo extends React.Component {state = {isHot: false}changeState = () => {this.setState({isHot: !this.state.isHot})}render() {const { weather } = this.statereturn (<div onClick={this.changeState}>今天的天气热吗?{this.state.isHot ? '热' : '冷'}</div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

state总结

1、state是组件对象最重要的属性,值是一个对象,包含多个键值对
2、通过更新state来更新组件的页面显示

props

props的使用场景

首先展示使用场景,比如我们要实现以下的页面

第一想法肯定是直接在页面渲染

 class ComponentDemo extends React.Component {state = {classList: [['一班学生1', '一班学生2', '一班学生3'], ['二班学生1', '二班学生2', '二班学生3'], ['三班学生1', '三班学生2', '三班学生3']]}render() {const { classList } = this.statereturn (<div>{classList.map(classItem => (<ul>{classItem.map(student => <li>{student}</li>)}</ul>))}</div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

如果要实现组件化,我们以班级为单位拆分成组件

class ClassComponent extends React.Component {render() {return (<ul><li>??????</li></ul>)}}class ComponentDemo extends React.Component {state = {classList: [['一班学生1', '一班学生2', '一班学生3'], ['二班学生1', '二班学生2', '二班学生3'], ['三班学生1', '三班学生2', '三班学生3']]}render() {const { classList } = this.statereturn (<div>{classList.map(classItem => <ClassComponent></ClassComponent>)}</div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

那么父子组件之间如何传递数据呢?那么就要用到_props参数_了。

props的用法

首先,我们在不进行任何操作的情况下,输出一下组件的props,我们在子组件的render函数中增加

 console.log(this.props)

来查看props对象,再次打开控制台

可以看到,在没有进行任何数据传输的情况下。组件的props是一个空对象。接下来开始进行数据传递,在父组件调用子组件的地方改写如下

  {classList.map(classItem => <ClassComponent name='wxs'></ClassComponent>)}

此时再次查看控制台输出

可以看到这就是传递props最基本的用法。如果使用props来实现本小节开头的那个例子。改写如下

 class ClassComponent extends React.Component {render() {const { studentList } = this.propsreturn (<ul>{studentList.map(student => <li>{student}</li>)}</ul>)}}class ComponentDemo extends React.Component {state = {classList: [['一班学生1', '一班学生2', '一班学生3'], ['二班学生1', '二班学生2', '二班学生3'], ['三班学生1', '三班学生2', '三班学生3']]}render() {const { classList } = this.statereturn (<div>{classList.map(classItem => <ClassComponent studentList={classItem}></ClassComponent>)}</div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

react’插槽’用法

其实在react里它不叫作插槽,为方便描述,这里用了vue的叫法。
在标签体中传入的内容会被自动添加到this.props.children内。

批量传递props

如果我们要将一个对象的全部属性传递给子组件

class SonComponent extends React.Component {render() {console.log(this.props)return 1}}class ComponentDemo extends React.Component {state = {name:'张三',age:13,job:'无',hobby:'无'}render() {return (<div><SonComponent data={this.state} ></SonComponent></div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

但是此时控制台会输出两层结构

但是我想直接在props的第一级结构中拿到数据就要这样写

 <SonComponent name={this.state.name} age={this.state.age} job={this.state.job} hobby={this.state.hobby} ></SonComponent>

如果数据量更多,写法就颇为麻烦,这里介绍一种props的批量写法

 <SonComponent {...this.state} ></SonComponent>

子组件控制台正常打印

对props进行限制

使用场景

某些时候我们会有这样的需求,我们要在传入的数据中再次加工,比如我们要将传入学生的年龄显示成3年后,如果传入的props中的age是字符串的话,那么我们传入的15,三年后就会变成153而不是18如下图所示。

class SonComponent extends React.Component {render() {return (<div>我叫{this.props.name},我今年{this.props.age}岁了,我三年后就{this.props.age + 3}</div>)}}class ComponentDemo extends React.Component {render() {return (<div><SonComponent name='wxs' age='15' ></SonComponent></div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

页面显示如下

props类型限制的基本使用

在15.X之前的写法中,限制props类型是这样限制的

 SonComponent.propTypes = {age:React.PropTypes.Number}

但是在15.X之后的版本中,React开发者认为在React上挂载PropsType对象会使React变得更重,且相当一部分使用者并不需要使用到PropTypes。所以在15.X之后的版本中,react对象上就不存在PropType属性了,在需要使用时在按需引入。所以在15.x以后的版本中使用propsType的方法如下

 //首先需要按需导入Props-type库<script src="../js/prop-type.js"> </script>//类型限制使用方法如下SonComponent.propTypes = {age:PropTypes.number}

经过这样改写之后,上例在控制台中就会报错,因为我们在props类型限制中限制了age属性要是number类型,但是传入的age是string类型的
在增加了类型限制之后的页面中,虽然页面表现依然不变,但是控制台会出现红色报错信息

再将传入的age改成数字类型以消除报错信息

 class ComponentDemo extends React.Component {render() {return (<div><SonComponent name='wxs' age={15} ></SonComponent></div>)}}

页面显示正常

同时控制台报错消失
除此之外,PropType还可以用来设置默认值
如果我们在使用子组件时没有传递name,页面显示将会十分奇怪

所以我们可以使用PropsType来设置默认值,基本使用方法如下

 SonComponent.defaultProps = {name:'这是一个默认姓名'}

tips:限制类型为函数的写法为PropType.func

props类型限制的简单写法

可以从上例中看到,无论是限制props类型还是设置类型的默认值

 SonComponent.propTypes = {age:PropTypes.number}SonComponent.defaultProps = {name:'这是一个默认姓名'}

本质上都是在类上设置静态属性(通过类直接取到,无法通过类实例得到),而设置类的静态属性有一种更为简单的写法如下

 class SonComponent extends React.Component {static propTypes = {age: PropTypes.number}static defaultProps = {name: '这是一个默认姓名'}render() {return (<div>我叫{this.props.name},我今年{this.props.age}岁了,我三年后就{this.props.age + 3}岁了</div>)}}

类式组件中的构造器

在此小节中我们探讨两个问题

1、constructor用于什么场景?
答:直接摘抄自react官方文档

而第一条,我们可以直接在组件类内部初始化state

class SonComponent extends React.Component {state={name:'2123'}}

第二条,我们可以通过将事件处理函数修改成箭头函数来达到目的,所以说在实际项目开发中,并没有刚需需要用到,因此,在实际工作中,我们很少用到constructor。

通过在子组件的构造器中有两个写法,页面显示以及控制台都正常
2、以下两种写法有什么区别?

 constructor(props){super(props)}constructor(){super()}

区别,构造器中是否传递props,取决于是否想在构造器中通过组件实例获取props。

 constructor(props){super(props)console.log(this.props)// 打印正常,不报错}constructor(){super()console.log(this.props)// 报错,无法获取this中的props属性}

函数式组件的props

函数式组件不可以使用类式组件的state和refs(下文介绍),但是可以正常使用props。

 function SonComponent(props){console.log(props)return <h1>我是一个简单的函数式组件</h1>}class ComponentDemo extends React.Component {render() {return (<div><SonComponent age={15} ></SonComponent></div>)}}ReactDOM.render(<ComponentDemo/>, document.getElementById('app'))


控制台打印正常

refs与事件处理

refs的基本使用

这一节我们将通过实现如下的例子来掌握refs的基本用法

使用方法极其简单直接上例子

 class ComponentDemo extends React.Component {showInfo = () => {window.confirm(this.refs.input1.value)}showInfo1 = () => {window.confirm(this.refs.input2.value)}render() {return (<div id='app'><input ref='input1' type="text" placeholder='点击按钮显示数据'/><button onClick={this.showInfo}>点击我显示输入框里的数据</button><input ref='input2' onBlur={this.showInfo1} placeholder='失去焦点显示数据'/></div>)}}ReactDOM.render(<ComponentDemo/>, document.getElementById('app'))

回调形式的refs

上一小节我们介绍的refs是字符串形式的refs,也是最简单最容易使用的,但是这种写法是不被官方所推荐的,官方原文“会引发性能问题”,所以在实际开发中,我们一般使用其他两种写法,接下来介绍第一种—回调形式的refs。
使用方式也极其简单直接上实例

 class ComponentDemo extends React.Component {showInfo = () => {window.confirm(this.input1.value)}showInfo1 = () => {window.confirm(this.input2.value)}render() {return (<div id='app'><input ref={(comp) => { this.input1 = comp }} type="text" placeholder='点击按钮显示数据' /><button onClick={this.showInfo}>点击我显示输入框里的数据</button><input ref={(comp) => {this.input2 = comp}} onBlur={this.showInfo1} placeholder='失去焦点显示数据' /></div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

需要注意的点:回调形式的ref在dom更新过程中会被执行两次。看实例

 class ComponentDemo extends React.Component {state = {name: 1123}changeState = () => {this.setState({name: this.state.name++})}render() {return (<div id='app'><h1 onClick={this.changeState} ref={(c) => { this.ref1 = c; console.log(this.ref1) }}>一个普通的h1标签</h1></div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))


原因是react每次执行render函数都会创建一个全新的ref回调函数,此时打印出来的ref组件是没有被赋值的,也就是null

api方式创建ref

 class ComponentDemo extends React.Component {myRef = React.createRef()showMsg = () => {console.log(this.myRef.current)}render() {return (<div ref={this.myRef} onClick={this.showMsg}>我是一个api方式创建的dom元素</div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

组件生命周期

生命周期图示 (旧 15.X之前的版本)


直接理解这张图可能会比较复杂,所以我们将图拆解成常用的三种情况来理解并记忆它。

普通组件生命周期

在不进行任何操作的最普通的情况下,组件的生命周期如下

 class ComponentDemo extends React.Component {constructor(props){super(props)console.log('第1步,执行constructor构造函数')}componentWillMount(){console.log('第2步,组件执行render之前的回调函数componentWillMount')}render(){console.log('第3步,执行render函数')return (<div>一个普通的生命周期</div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

在一个普通组件的生命周期中,依次会执行

 constructor ---> componentWillmount ---> render

控制台打印结果如下

更新状态生命周期

普通更新

1、任何一个react组件在更新状态时,生命周期如下所示

  class ComponentDemo extends React.Component {state = {count: 1}addFn = () => {this.setState({count: this.state.count + 1})}render() {console.log('第3步:执行render')return (<div><div>{this.state.count}</div><button onClick={this.addFn}>点击我加1</button></div>)}shouldComponentUpdate(){console.log(`第1步:更新状态是否被允许(返回值true则更新,false则终止流程)`)return true}componentWillUpdate(){console.log(`第2步:更新状态被允许之后,调用render之前执行的回调函数`)}componentDidUpdate(){console.log('第4步:更新状态完成回调')}       }ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

组件执行更新操作依次执行的生命周期钩子函数为:

 XXXfn(执行setState的函数) ---> shouldComponentUpdate ---> componentWillUpdate  ---> render --->  componentDidUpdate

控制台打印结果

强制更新

2、以上是正常执行状态更新触发的生命周期,在某些情况下,我们因为种种原因没有更新state但是需要重新执行render函数,此时就需要用到强制更新API了

 class ComponentDemo extends React.Component {update = () => {this.forceUpdate()}componentWillUpdate() {console.log('第1步:组件强制更新之后,执行render之前')}render() {console.log('第2步:组件执行render函数')return (<div><button onClick={this.update}>点击我强制更新</button></div>)}componentDidUpdate() {console.log('第3步:组件更新完毕')}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

组件执行强制更新时执行生命周期的顺序为

 componentWillUpdate ---> render ---> componentDidUpdate


注意此时组件的更新状态是不受shouldComponentUpdate控制的,因为强制更新流程中不包括此生命周期钩子

父组件更新子组件状态

第三种情况就是父组件通过更新state来更新子组件状态,生命周期如下所示

 class SonComponent extends React.Component {componentWillReceiveProps() {console.log('第1步:子组件接收到更新的参数');}shouldComponentUpdate() {console.log('第2步:组件是否应该更新')return true}componentWillUpdate() {console.log('第3步:组件即将更新,在render执行之前执行的回调')}render() {console.log('第4步:执行render函数')return (<div>{this.props.count}</div>)}componentDidUpdate() {console.log('第5步:组件更新完成执行的回调')}}class ComponentDemo extends React.Component {state = {count: 1}addFn = () => {this.setState({count: this.state.count + 1})}render() {return (<div><SonComponent count={this.state.count} ></SonComponent><button onClick={this.addFn}>点击我加1</button></div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

在更新子组件的状态的过程中,依次执行的生命周期是

 componentWillReceiveProps ---> shouldComponentUpdate --->  componentWillUpdate ---> render ---> componentDidUpdate

控制台打印如下

卸载组件

最后一个所有组件都会经历的生命周期—组件卸载

 componentWillUnmount

组件卸载之前调用

生命周期图示(新)

react新生命周期图示摈弃了三个钩子,新增了两个,弃用的三个钩子都是带有will关键字的(componentWillMount,componentWillReceiveProps,componentWillUpdate),带有will关键字的钩子除了componentWillUnmount之外全部弃用了,官方文档解释(react在尝试异步渲染页面,而这三个钩子会带来不安全的编码实践)

组件挂载时

首先对比一下组件挂载时的新旧生命周期钩子。

 constructor -->  componentWillMount ---> render ---> componentDidMount

 constructor ---> getDerectiveStateFromProps  --->  render ---> componentDidMount

差别:将componentWillMount换成了getDerivedStateFromProps
实际运用一下新周期钩子,需要特别注意的是 getDerectiveStateFromProps 不是组件实例使用的方法,是类自身使用的,需要加上static关键字使之变成静态方法。
实例

 class ComponentDemo extends React.Component {state = {}constructor(props) {super(props)console.log('第1步:执行类组件构造函数');}static getDerivedStateFromProps() {console.log('第2步:在constructor之后在render之前执行getDerivedStateFromProps')return null}render() {console.log('第3步:执行构造函数')return (<div>123123</div>)}componentDidMount() {console.log('第4步:执行componentDidMount')}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

控制台打印

此钩子不常用,实际使用极其罕见,做了解即可

组件更新时

普通更新

 componentWillReceiveProps ---> shouldComponentUpdate ---> componentWillUpdate ---> render ---> componentDidMount

 getDerivedStateFromProps --->  shouldComponentUpdate ---> render ---> getSnapshotBeforeUpdate ---> componentDidMount

差别::去除了componentWillReceiveProps,增加了getSnapshotBeforeUpdate
实例

 class ComponentDemo extends React.Component {state = {count: 1}addFn = () => {console.log('第1步:触发state更新')this.setState({count: this.state.count + 1})}shouldComponentUpdate() {console.log('第2步:组件是否允许被更新')return true}render() {console.log('第3步:执行render函数')return (<div><div>{this.state.count}</div><button onClick={this.addFn}>点击我加1</button></div>)}getSnapshotBeforeUpdate(state) {console.log('第4步:执行getSnapshotBeforeUpdate函数,获取页面更新前的state信息', state)return 111}componentDidUpdate(preProps, preState, aaa) {console.log('第5步:组件完成更新', preProps, preState, aaa)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

需要注意的是,getSnapshotBeforeUpdate返回值将作为componentDidUpdate回调的第三个参数,前两个参数分别是页面更新前的props-----preProps,更新前的state----->preState

getSnapShotBeforeUpdate使用场景

虽然新版本推出的两个钩子使用十分罕见,但是react官方文档还是给出了它的实际使用场景

实例场景:一个接受新闻推送的新闻列表

  class ComponentDemo extends React.Component {state = {newsCount: 1,newsList: []}render() {console.log(this.state.newsList)return (<div><ul className='news-content'>{this.state.newsList.map((item, index) => <li key={index}>{item}</li>)}</ul></div>)}componentDidMount() {setInterval(() => {this.setState({newsCount: this.state.newsCount + 1,newsList: [...this.state.newsList, `第${this.state.newsCount}条新闻`]})}, 1000)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))/*.news-content {background-color: aqua;height: 120px;width: 300px;overflow: auto;}.news-li {height: 30px;}*/

接下来我们要使用getSnapshotBeforeUpdate实现的功能是将滚动条滚动至最下方时,永远停留在最新一条新闻的位置。
在类式组件中增加一下两个钩子

 getSnapshotBeforeUpdate(state){return this.newsUl.scrollTop}componentDidUpdate(preProps,preState,scrollTop){this.newsUl.scrollTop += 30}

再看结果

diffing算法

diffing算法与虚拟dom

vue及react能在其他第三方框架之间脱颖而出的一个重要原因是他们更新dom的效率比较高,直接得益于它们都使用了diffing算法来更新虚拟dom,那么它们都是如何提高更新效率的呢。
一个简单的??

 lass ComponentDemo extends React.Component {state = {currentTime: new Date().toLocaleString()}render() {return (<div><span>{this.state.currentTime}</span><br /><input type="text" /></div>)}componentDidMount() {setInterval(() => {this.setState({currentTime: new Date().toLocaleString()})},1000)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))

页面效果如下

其中的时间是不断更新的,那么diffing算法将执行以下的操作


由图可知,diffing算法只会比较发生变化的dom元素以发生替换,从而最大程度的提高了更新效率。

列表渲染中key的作用

为什么不能使用index来作为列表渲染的key?

在diffing算法中,比较新旧dom时,如果比较到标签名和key一致的情况,那么算法就会进一步比较其内容是否有更新,进而决定要不要更新此dom标签。
好了,实际上上面的回答已经解释了这个问题,接下来将举个??更详细的阐述一下。

 state = {studentList: [{name: '学生1',age: 1},{name: '学生2',age: 2},{name: '学生3',age: 3},{name: '学生4',age: 4},{name: '学生5',age: 5}]}addStudent = () => {const newPerson = {name: '张三',age: 123}this.setState({studentList: [newPerson, ...this.state.studentList]})}render() {return (<div><ul>{this.state.studentList.map((student, index) =><li key={index}><span ><span style={{ 'marginRight': '10px' }}>{student.name}</span></span></li>)}</ul><button onClick={this.addStudent}>点击我添加一个人儿</button></div>)}}ReactDOM.render(<ComponentDemo />, document.getElementById('app'))


可以看到,页面展示是没有问题的,但是我们探究原理性问题时不应该满足于此。更应该探究一下它的性能性问题。
首先看看更新前的数据以及生成的虚拟dom

在更新dom之后,key=0的位置变成了小王,key等于1的位置变成了学生1,key=2的位置变成了学生2…,由于不是使用唯一的key作为列表项的标识符,此列表更新了五次。如果key使用的是唯一的,那么diffing算法可以识别出原有的五个列表项其实是没有变化的可以直接复用,就可以只进行一次dom更新操作。所以使用key作为标识符的第一个弊端
会引发严重的效率问题
如果说这种性能上的缺陷你可以接受的话。那么接下来增加一个需求
学生信息后面加入一个输入框

此时再看页面表现

分析更新过程
更新前
key为0的虚拟dom为

 <li key=0><span>学生1<span><input type='text' /></li>

执行diffing算法生成一个key为0的虚拟dom

 <li key=0><span>小王</span><input type='text' /></li>

外层标签li,key值相同,可以直接复用,再比较内层,span标签内容由学生1更新成了小王,input标签并没有更新,直接复用但是此input框内以及输入了学生1的信息,就会出现列表数据全部错位的问题,使用index作为key的第二个问题
当存在输入类操作的时候,会引发数据错位

react脚手架

从此章节起我们将使用企业开发react项目的方式来编写代码。类似于vue-cli工具,react也有一个脚手架工具(其实不应该这么说哈,应该是先有的react再有的vue)
第一步,全局安装react-cli工具

 npm i react-create-app -g

第二步,创建react-cli项目

 create-react-app XXX(项目名)

第三步,启动项目,在项目目录文件夹下执行

 npm run start

react首页出现

创建一个简单的hello world组件

此章节我们将通过实现一个简单的hello world组件来掌握如何在react-cli中使用组件。
首先看到src目录下的index.js文件,这是我们整个项目的入口文件。可以看到以下代码

这段代码就是我们之前在没有使用react-cli时写的

 ReactDom.render('组件','目标dom');

所以上例中的app组件应该就是渲染到主页上的组件,从引入路径可以得知app组件在同级目录下,所以再次查看同级目录下的app.js文件查看app组件

我们要做一个属于自己的项目,所以将app.js里的react预先写好的结构与样式全部删掉,将app组件改写如下

 <div className="App">hello world</div>

并且将同级目录下的App.css里的样式全部删掉。
此时再次打开react项目可以看到页面显示如下

再接下来进行更为规范的模块化编程
在src目录下新建component文件夹。新增helloworld组件如下

 //  helloworld组件import React, { Component } from 'react'export default class helloworld extends Component {render() {return (<div>helloworld</div>)}}

在app.js中引入并使用

 import Helloworld from "./components/HelloWorld/Helloworld";function App() {return (<div className="App"><Helloworld></Helloworld></div>);}export default App;

模块化组件样式文件(解决样式冲突)

先写一个简单的demo来展示一下问题
在components下再建一个新组件Mycomponent
helloworld组件及样式文件如下

  <div className='title'>helloworld</div>.title{color: red;}

Mycomponent组件及样式文件如下

  <div className='title'>helloworld</div>.title{color: blue;}

然后将两个组件都渲染到主页

import Helloworld from "./components/HelloWorld/Helloworld";
import MyComponent from "./components/MyComponent/MyComponent";
function App() {return (<div className="App"><Helloworld></Helloworld><MyComponent></MyComponent></div>);
}
export default App;

我们预计的结果是helloworld显示红色,而MyComponent显示蓝色而实际页面结果是

很明显,两个组件样式都为title的类名产生了冲突,对此,可提供的解决方案如下
1、将样式文件组件化 ---- 修改文件名

2、修改引用方式

 import helloStyle from "./index.module.css";

3、修改使用方式

 <div className={helloStyle.title}>helloworld</div>;

我们将两个组件都按此步骤修改之后的页面显示如下

tips:这只是可解决的方案,真实项目中一般不会用到此方案,因为在实际项目开发中,我们一般使用sass…

实现一个简单的react TODOlist实??

app.js

 import TodoList from "./components/TodoList/TodoList";function App() {return (<div className="App"><TodoList></TodoList></div>);}export default App;

todoList.js

 import React, { Component } from "react";import listStyle from "./index.module.css";const UUID = require("uuid-js");export default class extends Component {state = {todoList: [{id: UUID.create().hex,name: "第一件事儿",showDeleteBtn: false,status: false,},{id: UUID.create().hex,name: "第二件事儿",showDeleteBtn: false,status: true,},{id: UUID.create().hex,name: "第三件事儿",showDeleteBtn: false,status: false,},],};showDeleteBtnFn = (item) => {this.changeBtnStatus(item, true);};hideDeleteBtnFn = (item) => {this.changeBtnStatus(item, false);};changeBtnStatus = (targetItem, status) => {const newList = this.state.todoList.map((item) => {if (item.id === targetItem.id) {item.showDeleteBtn = status;}return item;});this.setState({todoList: newList,});};inputHandle = (e) => {if (e.keyCode === 13) {// 按回车结束const newEvent = {id: UUID.create().hex,name: this.refInput.value,howDeleteBtn: false,status: false,};this.setState({todoList: [newEvent, ...this.state.todoList],});}};changeEventStatus = (e, target) => {this.newList = this.state.todoList.map((item) => {if (item.id === target.id) {item.status = e.target.checked;}return item;});this.setState({todoList: this.state.todoList,});};deleteDownEvent = () => {const currentList = this.state.todoList.filter((item) => !item.status);this.setState({todoList: currentList,});};render() {return (<div className={listStyle.todoListStyle}><div><inputref={(c) => (this.refInput = c)}style={{ marginLeft: "10px" }}type="text"placeholder="请输入你将要做的事儿回车键结束"onKeyDown={($event) => this.inputHandle($event)}></input></div><div><ul>{this.state.todoList.map((item) => {return (<listyle={{ marginBottom: "20px" }}key={item.id}onMouseEnter={() => this.showDeleteBtnFn(item)}onMouseLeave={() => this.hideDeleteBtnFn(item)}><inputonChange={($event) => this.changeEventStatus($event, item)}type="checkbox"defaultChecked={item.status}></input><span style={{ marginRight: "10px" }}>{item.name}</span>{item.showDeleteBtn && <button>删除</button>}</li>);})}</ul></div><div>已完成({this.state.todoList.filter((item) => item.status).length})/总共({this.state.todoList.length})<button onClick={this.deleteDownEvent}>删除已完成的事</button></div></div>);}}

实??效果

什么是SPA(单页面应用)

定义:只有一个页面的应用,点击页面的链接不会刷新页面,只会进行局部的更新

react路由的基本使用

本章我们需要实现的实??如下

安装react-router-dom

1、移动到项目目录下

 npm i react-router-dom@5.x

2、移步到index.js下
从第三方router库中导入BrowserRouter对象

 import { BrowserRouter } from "react-router-dom";

然后将app组件包起来,使app组件的所有子组件都处于一个路由内部

 ReactDOM.render(<BrowserRouter><React.StrictMode><App /></React.StrictMode></BrowserRouter>,document.getElementById("root")
);

然后移步到app.js文件正式使用路由,先写两个路由标签

     <div><Link to="/home">Home</Link></div><div><Link to="/about">About</Link></div>

再写两个路由出口

 import Home from "./components/Home/Home";import About from "./components/About/About";<Route path="/home" component={Home}></Route> <Route path="/about" component={About}></Route>

至此,效果完成

路由组件和一般组件

作为路由匹配路径出口的组件就是路由组件,如上例中的<Route path="/home" component={Home}></Route>,作为组件直接渲染到页面上的,称为一般组件。
路由组件和一般组件的区别:一般组件无默认props,靠组件外传递,路由组件默认props我们打印出来看看如下所示

navLink的使用

我们虽然已经初步实现了路由的基本功能,但是日常我们所见到的路由都是有选中效果的,所以接下来我们要实现如下??

第一步,普通的路由链接是无法实现此功能的,所以我们需要把普通的Link换成更高级的NavLink

 import { NavLink,Route } from "react-router-dom";<div><NavLink to="/home">Home</NavLink></div><div><NavLink to="/about">About</NavLink></div>

换成这个高级link之后,选中的link就会有特殊类名active如图

最后我们就可以通过特殊类名来编写选中样式啦!

 a.active {background-color: skyblue;}

同时我么也可以自己指定选中时的类名

  <NavLink activeClassName='wxsDiy' to="/home">Home</NavLink>

switch的基本使用

实际项目开发中往往不止两个路由,如果路由数量多起来,使用Route写法就会显得冗余。

       <Route path="/home" component={Home}></Route> <Route path="/about" component={About}></Route><Route path="/a" component={a}></Route> <Route path="/b" component={b}></Route><Route path="/c" component={c}></Route> <Route path="/d" component={d}></Route><Route path="/about" component={e}></Route>....

以上例来看,route写法不仅冗余,且如果出现了同path的路由,路由组件会一直往后找直到找到最后一个为止,而在实际项目开发中这样是效率极低的,因为我们往往想的是找到一个匹配的路由之后就直接渲染页面,而不是一直找到最后。这就要用到组件的switch了。

 import { Switch, NavLink, Route } from "react-router-dom";<Switch><Route path="/home" component={Home}></Route><Route path="/about" component={About}></Route><Route path="/a" component={a}></Route><Route path="/b" component={b}></Route><Route path="/c" component={c}></Route><Route path="/d" component={d}></Route><Route path="/about" component={e}></Route></Switch>

switch唯一作用是对匹配中的路由直接渲染,提高效率

路由的严格匹配和模糊匹配

来看一个路由的模糊匹配实例

  <div className="App"><div><NavLink to="/home">Home</NavLink></div><div><NavLink to="/about/a/b/c">About</NavLink></div><Switch><Route path="/home" component={Home}></Route><Route path="/about" component={About}></Route></Switch></div>

可以看到第二个navlink需要匹配的path路径是about/a/b/c,而router能匹配的路由是about,应该是无法匹配的,但实际页面效果显示正常,路由匹配正常,这就是路由的模糊匹配。
模糊匹配规则(输入路径要包含匹配的路径,且顺序要一致)
如上例中navLink中显示的about,a,b,c,路由中含有规则中的第一个about,则视为匹配成功。
但是在实际项目中,我们往往要开启精准匹配模式,使用如下

 <Route exact path="/about/" component={About}></Route>

使用很简单,就是在route标签中加上exact属性。(注:严格模式不能随便开启,有时候会导致无法匹配二级路由,实例见《嵌套路由章节》)

redirect(重定向)

??到目前为止依然有一个缺陷,虽然我们已经能够实现页面跳转,但是在首次进页面时,我们并没有匹配上任何路由,导致首页是白屏。

对此,我们需要一个重定向功能来实现没有匹配任何路由时自动跳转到首页的功能。具体使用如下。

 <Redirect to='/home'></Redirect>

嵌套路由

相比较于vue的子路由,react也有对应的嵌套路由。嵌套路由的用法和一级路由一样。直接看??
App.js

 import {Redirect, Switch, NavLink, Route } from "react-router-dom";import Home from "./components/Home/Home";import About from "./components/About/About";import "./App.css";function App() {return (<div className="App"><div><NavLink to="/home">Home</NavLink></div><div><NavLink to="/about">About</NavLink></div><Switch><Route path="/home" component={Home}></Route><Route path="/about" component={About}></Route><Redirect to='/home'></Redirect></Switch></div>);}export default App;

About.js

 import React, { Component } from "react";import { NavLink, Switch, Route } from "react-router-dom";import Page1 from "../../pages/page1/Page1";import Page2 from "../../pages/page2/Page2";export default class About extends Component {render() {return (<div><div><NavLink to="/about/page1">我去page1页面儿</NavLink></div><div><NavLink to="/about/page2">我去page2页面儿</NavLink></div><div><Switch><Route path="/about/page1" component={Page1}></Route><Route path="/about/page2" component={Page2}></Route></Switch></div></div>);}}


上文中讲到开启精准匹配有可能会导致二级路由匹配失败,这里来看个典型案例。
我们将about路由改成精准匹配

 <Route exact path="/about/page2" component={Page2}></Route>

开启精准匹配后通过navLink注册的路由表如下
/home /about /about/page1 /about/page2
此时如果我们如果点击次级路由 /about/page1
在一级路由中 /home /about均不匹配,那么开启了精准匹配的路由就会直接进入redirect,所以说,点击次级路由的任何一个都会直接进入redirect的home页面儿

 <Redirect to='/home'></Redirect>

路由传参的三种方式

改写page2组件如下
Page2.js

 import React, { Component } from "react";import { Link, Route, Switch } from "react-router-dom";import Content from "../content/Content";const UUID = require("uuid-js");export default class Page2 extends Component {state = {newsList: [{id: UUID.create().hex,title: "这是第一条新闻儿",content: "这是第一条新闻内容",},{id: UUID.create().hex,title: "这是第二条新闻儿",content: "这是第二条新闻内容",},{id: UUID.create().hex,title: "这是第三条新闻儿",content: "这是第三条新闻内容",},],};render() {return (<div style={{ display: "flex" }}><div><ul style={{ listStyle: "none" }}>{this.state.newsList.map((newItem) => (<li key={newItem.id}><Link to="/about/page2/content">{newItem.content}</Link></li>))}</ul></div><div><Route path="/about/page2/content" component={Content}></Route></div></div>);}}

我们需要做的是点击到对应的新闻链接时显示对应的新闻内容

向路由传递params参数

1、传递params参数

 //link传递<Link to={`/about/page2/content/${index+1}/${newItem.content}`}>{newItem.content}</Link>//route接收入<Route path="/about/page2/content/:newsId/:content" component={Content}></Route>//路由组件内使用const { newsId, content } = this.props.match.params;

效果

向路由传递search参数

 //传递<Link to={`/about/page2/content/?newsId=${index}&content=${newItem.content}`}>{newItem.content}</Link>//接收(无需额外写接收代码)<Route path="/about/page2/content" component={Content}></Route>//使用this.props.location.search// newsId=0&content=%E8%BF%99%E6%98%AF%E7%AC%AC%E4%B8%80%E6%9D%A1%E6%96%B0%E9%97%BB%E5%86%85%E5%AE%B9


缺陷,传递过来的search参数未编码需要自己手动解析 (需要额外安装第三方urlencoding解析库),使用如下

 import qs from "querystring";console.log(qs.parse(this.props.location.search));

向路由传递state参数

可以看到以上两种方式都会将参数显示在url中,而向路由传递state参数可以避开这一点,基本使用如下

 // 传递<li key={newItem.id}><Linkto={{pathname: "/about/page2/content",state: { newsId: index + 1, content: newItem.content },}}>这是第{index + 1}条新闻</Link></li>    // 接收<Route path="/about/page2/content" component={Content}></Route>// 使用this.props.location.state

push跳转和replace跳转

太简单了,不详细说,push为推入history栈,replace为替换history栈顶。

编程式路由导航

编程式路由导航是一种不经过点击链接,通过代码实现路由跳转的方式

编程式跳转发送params参数

 // 传递this.props.history.push('/about/page2/content/1111/')this.props.history.replace('/about/page2/content/1111/')// 接收<Route path="/about/page2/content/:newsId" component={Content}></Route>

编程式导航发送search参数

 // 传递this.props.history.push('/about/page2/content/?newsId=1&content=123123')// 接收<Route path="/about/page2/content/" component={Content}></Route>

编程式路由导航发送state参数

 // 传递this.props.history.push({pathname: "/about/page2/content/",state: { name: "wxs" },});// 接收<Route path="/about/page2/content/" component={Content}></Route>

withRouter的基本使用(使一般组件变成路由组件)

上文中说到,一般组件和路由组件的区别是路由组件的props里默认带有history,location,match等与之相关的参数,但是一般组件里并没有,那么如何在一般组件里实现编程式路由跳转呢

import React, { Component } from "react";export default class Normal extends Component {constructor(props) {super(props);console.log(this.props); // 控制台打印空对象 {}}render() {return <div>一个一般组件</div>;}
}

改写一般组件如下

 1、引入withRouter库import { withRouter } from "react-router-dom";2、暴露封装后的react组件类export default withRouter(Normal);

withRouter接收的参数是一个一般组件变成路由组件来使用

BroswerRouter和HashRouter

原理性的东西,仅做记录
底层原理:
BroswerRouter使用的是H5的historyAPI,不兼容低版本浏览器
HashRouter使用的是window.location的Hash值来确保url和页面UI保持一致
url显示:
BroswerRouter没有"#"号
参数
BroswerRouter刷新参数不丢失,因为参数保存在history中
HashRouter刷新参数丢失

Redux

作用,组件直接传递参数(非父子,非兄弟,且嵌套层级较多的组件之间传参)。

redux 工作流程图


理解性记忆为 组件比如像执行添加操作(do what),他会先通知Action creater创建一个”添加动作对象“,全局仓库(store)收到这个对象就会把它丢给执行者(reducer)执行,同时会传递给执行者之前的store状态,reducer执行完毕之后将最新的状态返回给store,然后任意组件都可以通过store拿到最新的状态。

redux的三个核心概念

在开始实??之前先简要减少一下redux的三个核心属性

action

注意,途中黄框内的action creator不是redux的核心属性,action才是,action creator只负责创建action对象。
action对象包含两个重要的属性,type:用于描述action的类型,data,action包含的数据。

reducer

接收一个之前的store对象用于加工,返回一个加工之后的新的store对象

store

用于连接action,reducer和各大组件,存储数据的地方。

redux实例??

首先,将首页其他无关代码全部删除,改写如下

 // App.jsimport Counter from "./components/Counter/Counter";import Total from "./components/Total/Total";import "./App.css";function App() {return (<div className="App" style={{ display: "flex",justifyContent:"space-between" }}><Counter></Counter><Total></Total></div>);}export default App;// Counter.jsimport React, { Component } from "react";import "./index.css";export default class Counter extends Component {state = {calculateStatus: false,calculateType: null}btnClickHandle = ($event) => {const { type, key: data } = $event.target.datasetif (type === 'number' && !this.state.calculateStatus) {// 计算机正常态,回显按键数据this.inputRef.value = data}if (type === 'number' && this.state.calculateStatus) {// 计算态const currentValue = this.inputRef.valueif (this.state.calculateType === '+') {this.inputRef.value = Number(currentValue) + Number(data)}if (this.state.calculateType === '-') {this.inputRef.value = Number(currentValue) - Number(data)}if (this.state.calculateType === '*') {this.inputRef.value = Number(currentValue) * Number(data)}if (this.state.calculateType === '/') {this.inputRef.value = Number(currentValue) / Number(data)}}if (type === 'operate') {this.setState({calculateStatus: true,calculateType: data})}}render() {return (<div><div><input ref={(c) => this.inputRef = c} /></div><div className="keyboard-class"><button onClick={this.btnClickHandle} data-key='7' data-type='number' className="key-class">7</button><button onClick={this.btnClickHandle} data-key='8' data-type='number' className="key-class">8</button><button onClick={this.btnClickHandle} data-key='9' data-type='number' className="key-class">9</button><button onClick={this.btnClickHandle} data-key='+' data-type='operate' className="key-class">+</button><button onClick={this.btnClickHandle} data-key='4' data-type='number' className="key-class">4</button><button onClick={this.btnClickHandle} data-key='5' data-type='number' className="key-class">5</button><button onClick={this.btnClickHandle} data-key='6' data-type='number' className="key-class">6</button><button onClick={this.btnClickHandle} data-key='-' data-type='operate' className="key-class">-</button><button onClick={this.btnClickHandle} data-key='1' data-type='number' className="key-class">1</button><button onClick={this.btnClickHandle} data-key='2' data-type='number' className="key-class">2</button><button onClick={this.btnClickHandle} data-key='3' data-type='number' className="key-class">3</button><button onClick={this.btnClickHandle} data-key='*' data-type='operate' className="key-class">*</button><button onClick={this.btnClickHandle} data-key='0' data-type='number' className="key-class">0</button><button onClick={this.btnClickHandle} data-key='/' data-type='operate' className="key-class">/</button></div></div>);}}// css样式表// Result.jsimport React, { Component } from 'react'export default class Result extends Component {render() {return (<div>我来显示计算器的计算结果</div>)}}

最终的实现的一个简易计算器的效果图

状态全局化

可以看到,在某个组件内部实现简易计算器功能还是很简单的,接下来我们要做的工作就是将组件state全局共享,使任何一个组件都能获取并操作计算器!

创建全局store

引入redux第三方库

 npm i redux

在src文件夹下新建store文件夹,新建store.js

 import { createStore } from 'redux'import countReducer from './reducers/count_reducer'const store = createStore(countReducer)export default store

创建reducers

store文件夹下新建reducers文件夹,因为我们要新建一个为count服务reduce,所以新建count_reduce.js

 function countReducer(preState, action) {console.log(preState, action);}export default countReducer;

在组件中使用

在result.js中引入store

import React, { Component } from "react";
import store  from "../../store/store";
export default class Result extends Component {constructor(props) {super(props);}render() {return <div>我来显示计算器的计算结果</div>;}
}

此时在打开页面可以看到控制台打印如下(tips:reducer首次执行是被redux自动触发!其type值是init加一串乱码)

分别对应了count_reducer中的preState和action,由于是第一次执行,所以肯定没有之前的state状态,打印为undefined,action在首次执行时是redux默认传递的随机的字符串。

改造实例

首先将组件内的state迁移到reducer中
将counter.js代码中的整段剪切到reducer中作为preState的默认值

此时控制台输出preState就有默认值了

但是此时组件儿内的加减乘除方法都会报错,因为组件内已经没有state对象了。

所以很明显,接下来我们要把操作state的方法改写成操作store里state。这时候就轮到action creator登场了,它唯一的作用就是创建一个action对象。
store文件夹下新建actionCreator文件夹,新建count_action_creator.js

 export const updateActionCreator = (data) => ({type: "updateCountState",data,});

在count.js中引入此文件,并且count.js中所有使用到原state的地方都需要更新(上图已经框出)。此时要用到获取全局store状态的api —store.getState()
1、使用state改写如下

 const { type, key: data } = $event.target.dataset;const { calculateStatus, calculateType } = store.getState();if (type === "number" && !calculateStatus) {// 计算机正常态,回显按键数据this.inputRef.value = data;}if (type === "number" && calculateStatus) {// 计算态const currentValue = this.inputRef.value;if (calculateType === "+") {this.inputRef.value = Number(currentValue) + Number(data);}if (calculateType === "-") {this.inputRef.value = Number(currentValue) - Number(data);}if (calculateType === "*") {this.inputRef.value = Number(currentValue) * Number(data);}if (calculateType === "/") {this.inputRef.value = Number(currentValue) / Number(data);}}

2、更新state就要用到另一个api-----store.dispatch()

 //  改写之前if (type === "operate") {this.setState({calculateStatus: true,calculateType: data,});}// 改写之后store.dispatch(updateActionCreator({calculateStatus: true,calculateType: data,calculateResult: this.inputRef.value,}));// 注意此处的updateActionCreator就是actionCreatores导出的对象,接受一个data,返回一个{type,data}对象

此时reducer就会收到更新通知以及传递的数据啦!
最后,在reducer收到通知时执行更新逻辑

 const initialState = {calculateStatus: false,calculateType: null,};function countReducer(preState = initialState, action) {console.log(preState, action);const { type, data } = action;switch (type) {case "updateCountState":return data;default:return preState;}}export default countReducer;

至此,全局组件改写完成。

总结:redux整个流程

组件通知action_creator创建action对象

action_creator通知store更新

store收到更新通知,执行操作并返回最新的state值

组件获取state值

在熟悉了整个redux流程之后,肯定会提出这样一个疑问组件为什么要通过action_creator来创建action对象呢,明明在组件内部组件内部直接写就可以了。额外维护一个action_creator就是使代码更加规范且易于维护。举个??,如果有十个组件同时dispatch({ type:“更新”,data:1 }),如果业务或者代码变化,需要更新data为data为+1之后的值,那么没有actionCreator.js的话就要改动十个地方,而通过action_creator就只需要修改一个地方。

效果展示

为了更好的展示效果,我们把计算结果也共享到store中。
改写reducer.js中的初始值

改写组件通知时传递的参数

最后改写result.js组件

 import React, { Component } from "react";import store  from "../../store/store";export default class Result extends Component {constructor(props) {super(props);}render() {return <div>我来显示计算器的计算结果-------{store.getState().calculateResult}</div>;}}

正当我们以为万事大吉,打开页面看效果时,却发现result的状态并没有更新,而控制台显示calculateResult值确实已经更新了

这里要讲到store的一个坑。在组件内部使用setState方法时,react执行完setState之后会自动执行组件的render函数以确保页面显示的是最新值,但是组件无法检测到store是否更新
方法1
在组件内部监听store变化,在变化发生之后手动触发render函数
result组件增加以下代码

 componentDidMount(){store.subscribe(() => {this.forceUpdate() 或者 this.setState({})})}

注意不能写this.render手动触发render,因为render函数不支持代码触发!
方法2
方法1只能对某个组件实施监听store,所以在实际项目中我们往往使用第二种方法监听整个组件

最终效果

补充说明

在实际代码过程中,为了使代码更加规范和易维护,在store文件夹下往往还会新建一个constant文件来保存常量。
目的1:可以看到我们用到常量的地方有两处
第一处 reducer.js

第二处 actionCreator.js

如果需要对常量变化,那么需要改动两次
目的2:如果因为常量写错而出现bug是很难短时间内看出来的
因为以上两个原因,我们通常新建一个constant.js来保存常量

 // constant.jsexport const UPDATE_COUNT_STATE = 'updateCountState'

creator.js改写如下

 import { UPDATE_COUNT_STATE } from '../constant'export const updateActionCreator = (data) => ({type: UPDATE_COUNT_STATE,data,});

reducer改写如下

 import { UPDATE_COUNT_STATE } from '../constant'const initialState = {calculateStatus: false,calculateType: null,calculateResult: 0,};function countReducer(preState = initialState, action) {console.log(preState, action);const { type, data } = action;switch (type) {case UPDATE_COUNT_STATE:return data;default:return preState;}}export default countReducer;

异步action

上例中所介绍的都是同步action,与之对应的还有异步action。不常用但需做了解。redux的设计在使用上的主要区别来说,同步action传递的是一个对象

 export const updateActionCreator = (data) => ({type: UPDATE_COUNT_STATE,data,});

而异步action传递的是一个函数,但是如果直接将action写成函数形式是会报错。看??。
actioncreater新建异步actioncreator

 export const updateActionAsyncCreator = (data, time) => () => {setTimeout(() => {store.dispatch(updateActionCreator(data));}, time);};

组件修改使用方法

 store.dispatch(updateActionAsyncCreator(data,500))

控制台报错信息如下

action必须是一个普通对象,而实际是一个‘function’,你可能需要使用一个中间件来处理其他的值
解决方法
1、安装react-thunk

 npm install redux-thunk

2、在store.js中使用

 import { createStore, applyMiddleware } from "redux";import countReducer from "./reducers/count_reducer";// 引入redux-thunk,支持异步actionimport thunk from "redux-thunk";const store = createStore(countReducer, applyMiddleware(thunk));export default store;

3、修改action_creator.js

 export const updateActionAsyncCreator = (data, time) => (dispatch) => {setTimeout(() => {dispatch(updateActionCreator(data));}, time);};

tips:异步action传递的函数,默认带有dispatch参数。异步action里面往往含有同步action。

最终效果

总结,异步action不是必须的,在组建内部使用settimeout也可以实现同样的效果

react-redux

react和redux是毫无关系的,react是Facebook公司开发的,而redux是第三方社区活跃者开发的开源状态管理库。因为很多开发者喜欢在react项目中使用redux,react索性开发了一个react内部专用的redux版本-----react-redux。

react-redux关系图示

相比较第三方插件库redux而言,react-redux的流程和使用会有些许差别。

实际项目中,UI组件一般是放在components文件夹中,容器组件一般是放在container文件夹中,学习的时候我们也与实际开发保持一致

连接组件和store

连接容器组件和UI组件

从上例图示来看,我们先连接UI组件和容器组件。在container文件夹下新建Counter.jsx作为Counter的容器组件用法如下
第一步,如果需要连接容器组件和UI组件,自然需要在容器组件里引入UI组件。

 // 映入UI组件import CountUI from '../../components/Counter/Counter'

第二步,引入用于连接的connect

 import { connect } from 'react-redux'

connect用法简介如下,connnet函数接受两个函数作为参数,第一个函数需要返回一个普通对象,作为组件的state值,第二个函数也需要返回一个普通对象,作为操作组件state的方法,connet函数的返回值也是一个函数,需要传入UI组件。我这么干说可能已经绕晕了。直接上实??

 import CountUI from '../../components/Counter/Counter'import { connect } from 'react-redux'// 作为组件的stateconst mapStateToProps = () => ({key:'这是一些值'})// 作为操作state的方法const mapDispatchToProps = () => { return {add:() => {console.log('这是一个加法')},reduce:() => {console.log('这是一个减法')}}}
export default connect(mapStateToProps,mapDispatchToProps)(CountUI)

最后我们在counterUI组件的render函数中打印一下props看一下实际效果

使用容器组件

在上一节容器组件成功连接到UI组件了之后,直接渲染到页面的就不应该是Counter的UI组件了,应该是Count的容器组件。所以在App.js中,我们需要将Counter组件的引入路径修改一下。

 // 原来引入UI组件// import Counter from './components/Counter/Counter';// 现在引入容器组件import Counter from '../src/container/Count/index'

容器组件在使用上和UI组件有些许的区别,容器组件需要传递store对象

 import "./App.css";<Counter  store={store} />

完善react-redux

回顾上两节的容器组件,很明显,我们在组件中不止是需要使用固定的值,和一些无意义的方法。我们需要的值是store里的值和操作store的方法。那么就需要对容器组件进行改造。经过上一节将容器组件挂载到实际页面且传递了store对象之后,容器组件connet的两个参数就可以正常使用store的state和操作方法了。
对mapStateToprops方法改造如下。该方法可以经过传递了store对象之后可以使用一个新参数state。此state就是store对象存储的state。

 // 改造之前const mapStateToProps = () => ({key:'这是一些值'})// 改造之后const mapStateToProps = (state) => state

同样的,mapDispatchToProps也可以接收到新参数,用于触发store更新的函数dispatch。

 //  修改之前const mapDispatchToProps = () => {return {add: () => {console.log("这是一个加法");},reduce: () => {console.log("这是一个减法");},};};// 修改之后import { updateActionCreator } from "../../store/actionCreators/count_action_creator";const mapDispatchToProps = (dispatch) => {return {updateStore: (data) => {dispatch(updateActionCreator(data));},};
};

最后在counterUI组件中对store的使用方式进行改写,之前是直接通过store对象获取,现在是通过this.props获取

 // 改造之后btnClickHandle = ($event) => {const { type, key: data } = $event.target.dataset;const { calculateStatus, calculateType } = this.props;if (type === "number" && !calculateStatus) {// 计算机正常态,回显按键数据this.inputRef.value = data;}if (type === "number" && calculateStatus) {// 计算态const currentValue = this.inputRef.value;if (calculateType === "+") {this.inputRef.value = Number(currentValue) + Number(data);}if (calculateType === "-") {this.inputRef.value = Number(currentValue) - Number(data);}if (calculateType === "*") {this.inputRef.value = Number(currentValue) * Number(data);}if (calculateType === "/") {this.inputRef.value = Number(currentValue) / Number(data);}}this.props.updateStore({calculateStatus: true,calculateType: data,calculateResult: this.inputRef.value,});};

mapDispatchToProps的简写形式

对于有很多dispatch的容器组件来说,很容易出现以下的代码

 const mapDispatchToProps = (dispatch) => ((value) => ({jia:dispatch(createAddAction(value)),jian:dispatch(createJianAction(value)),cheng:dispatch(createJianAction(value)),chu:dispatch(createChuAction(value)),...
}))

而使用mapDispatchToProps的简写形式会使得代码简洁许多。
简写形式是一个简单的对象

 const mapDispatchToProps = {jia:createAddAction,jian:createJianAction,cheng:createJianAction,chu:createChuAction}

redux模块化

当redux仅为counter一个组件服务的时候,在store.js中当然可以这样写

 const store = createStore(countReducer, applyMiddleware(thunk));

当项目中出现多个reducer时。就需要用到另一个api combainReducers。举个实例??。当项目中新增一个user的reducer

 import { createStore, applyMiddleware, combineReducers } from "redux";import countReducer from "./reducers/count_reducer";import userReducer from "./reducers/user_reducer";// 引入redux-thunk,支持异步actionimport thunk from "redux-thunk";const allReducers = combineReducers({countReducer,userReducer,});const store = createStore(allReducers, applyMiddleware(thunk));export default store;

react-redux的优化

在上例我们在项目中直接使用redux时,如果不在index.js中实施监听,那么各组件是无法监听到store更新的。而react-redux在内部就对各组件进行的监听,所以我们无需在index.js中再次监听

 // 可删除store.subscribe(() => {ReactDOM.render(<BrowserRouter><React.StrictMode><App /></React.StrictMode></BrowserRouter>,document.getElementById("root"));});

我们在对组件提供store对象时,目前的写法是

 <Counter  store={store} />

但是如果有很多个组件都需要像这样提供store对象的话,代码就会变得冗余。在react-redux中提供了一个providerApi,它会自动分析项目内对store的使用情况,并把store传递给该组件。使用如下
第一步,注释掉store的局部引入

 // 修改之前<Counter  store={store} />// 修改之后<Counter  />

第二步,全局引入store对象,在index.js文件夹下修改代码

 import store from "./store/store";import { Provider } from "react-redux";// 将app组件用Provider包起来<Provider store={store}><BrowserRouter><React.StrictMode><App /></React.StrictMode></BrowserRouter></Provider>

最后一步,将result组件修改成容器组件查看页面效果

 // 在container文件夹中新建Result文件夹,在其中新建index.js文件编辑如下import Result from "../../components/Result/Result";import { connect } from "react-redux";const mapStateToProps = (state) => state;const mapDispatchToProps = {};export default connect(mapStateToProps, mapDispatchToProps)(Result);// 在app.js中修改引入方式import Result from "../src/container/Result/index";

查看页面效果

额外的优化方案。
从上例中看出,我们使用的Counter组件和Result组件分为UI组件和容器组件分别在components文件夹中和container文件夹中。

react从入门到入魔相关推荐

  1. React.js入门笔记

    # React.js入门笔记 核心提示 这是本人学习react.js的第一篇入门笔记,估计也会是该系列涵盖内容最多的笔记,主要内容来自英文官方文档的快速上手部分和阮一峰博客教程.当然,还有我自己尝试的 ...

  2. React学习:入门实例-学习笔记

    文章目录 React学习:入门实例-学习笔记 React的核心 需要引入三个库 什么是JSX react渲染三步骤 React学习:入门实例-学习笔记 React的核心 1.组件化:把整一个网页的拆分 ...

  3. React.js 入门与实战课程思维导图

    原文发表于我的技术博客 我在慕课网的「React.js 入门与实战之开发适配PC端及移动端新闻头条平台」课程已经上线了,在这里分享了课程中的思维导图,供大家参考. 原文发表于我的技术博客 此导图为课程 ...

  4. React个人入门总结《五》

    简介 这一次总结的是 React-redux 的实现,可以参考一下 大佬的文章 . 首先要知道 redux 的基本使用: 创建一个 Store . <!-- store --> funct ...

  5. React Router入门指南

    by Nader Dabit 纳德·达比特(Nader Dabit) React Router入门指南 (Beginner's Guide to React Router) Or what I wis ...

  6. React.js 入门与实战之开发适配PC端及移动端新闻头条平台课程上线了

    原文发表于我的技术博客 我在慕课网的「React.js 入门与实战之开发适配PC端及移动端新闻头条平台」课程已经上线了,文章中是目前整个课程的大纲,以后此课程还会保持持续更新,此大纲文档也会保持更新, ...

  7. FFmpeg从入门到入魔(2):保存流到本地MP4

    1 . FFmpeg裁剪移植 之前我们简单地讲解了下如何在Linux系统中编译FFmpeg,但是编译出来的so体积太大,而且得到的多个so不便于使用.本节在此基础上,将详细讲解在编译FFmpeg时如何 ...

  8. React Native 入门实战视频教程(36 个视频)

    React Native 入门实战视频教程(36 个视频) #1 React Native 课程介绍「02:14」 #2 搭建 React Native 开发与运行环境跑起来「05:07」 #3 演示 ...

  9. web前端高级React - React从入门到进阶之React条件渲染

    系列文章目录 第一章:React从入门到进阶之初识React 第一章:React从入门到进阶之JSX简介 第三章:React从入门到进阶之元素渲染 第四章:React从入门到进阶之JSX虚拟DOM渲染 ...

  10. python从入门到入魔第八天——turtle库使用(含玫瑰花绘制实例)

    turtle库的作用 python 提供画图工具标准库:turtle库包绘图坐标体系.画笔控制函数和形状绘制函数,用来绘制想要的图画,turtle库的调用方式参考Python从入门到入魔第五天--ji ...

最新文章

  1. 《软件工程方法与实践》—— 导读
  2. 小米宣布加入鸿蒙,中兴和OPPO抵制后,第一个宣布加入鸿蒙阵营的果然是魅族...
  3. appserv 安装后phpmyadmin 密码问题 win7
  4. 算法杂货铺——分类算法之贝叶斯网络(Bayesian networks)
  5. 长话短说,阿里云原生团队招人,急
  6. JBPM学习(六):详解流程图
  7. CSP-S2019游记
  8. vscode设置终端字体大小
  9. php实现多字段unique验证,Laravel实现用户多字段认证的解决方法
  10. Firefox 检测到该服务器正在将此地址的请求循环重定向。     此问题可能是因为禁用或拒绝 Cookie 导致。...
  11. 苹果cms V10模板 手机端模板粉红色模板带会员中心
  12. 动态ip软件win7_IPXE+ISCSI Target安装WIN7
  13. linux编辑查看命令,Linux的文本编辑和文本内容查看命令
  14. vue 中 v-if 和 v-for 混用时应该注意的事项
  15. cmd命令提示符配置切换IP地址
  16. 服务器cmd升级系统命令,02-软件升级操作指导(命令行版)
  17. win10创建新的计算机用户名和密码,win10如何新建一个账号用户
  18. 毕业实习大作业(Android-Spring Boot-MySQL 前后端分离项目 快速上手实例)
  19. ALtera DE2开发板学习
  20. 朱刘算法 有向图的最小生成树

热门文章

  1. Kmeans 算法 收敛
  2. python判断图像是否为灰度图
  3. Jekyll 语句语法、功能的实现方法和结构介绍小手册
  4. 马哥教育SRE第五周作业
  5. 群晖NAS同步阿里云盘
  6. conan-transit服上的库列表
  7. nginx的配置优化
  8. ogre研究之第一个程序
  9. 【转】最落魄的日子你是怎样熬过来的?
  10. 纽约大学文科学院计算机,2016年美国大学文科排名大全