介绍
用于动态构建用户界面的 JavaScript 库(只关注于视图)
- 发送请求获取数据
- 处理数据(过滤,整理格式等)
- 操作DOM呈现页面(React做的事情) React是一个将数据渲染为HTML视图的开源JavaScript库
1.2 原生JavaScript的缺点
- 原生JavaScript操作DOM繁琐,效率低(DOM-API操作UI)
- 使用JavaScript直接操作DOM,浏览器会进行大量的重绘重排
- 原生JavaScript没有组件化编码方案,代码复用率很低
【补充】浏览器重绘重排
浏览器重绘(repaint)重排(reflow)与优化[浏览器机制]
- 重绘(repaint):当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘
- 重排(reflow):当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排
【补充】模块化与组件化
模块
理解:向外提供特定功能的js程序, 一般就是一个js文件 为什么要拆成模块:随着业务逻辑增加,代码越来越多且复杂。 作用:复用js, 简化js的编写, 提高js运行效率 组件 理解:用来实现局部功能效果的代码和资源的集合(html/css/js/image等等) 为什么要用组件: 一个界面的功能更复杂 作用:复用编码, 简化项目编码, 提高运行效率
- 模块化:当应用的js都以模块来编写的, 这个应用就是一个模块化的应用
- 组件化:当应用是以多组件的方式实现, 这个应用就是一个组件化的应用
React的特点
采用组件化模式、声明式编码,提高开发效率及组件复用率 在 React Native中可以使用React语法进行移动端开发 使用虚拟DOM+Diff算法,尽量减少与真实DOM的交互
React高效的原因
使用虚拟(virtual)DOM, 不总是直接操作页面真实DOM。 DOM Diffing算法, 最小化页面重绘。 相关库介绍 旧版本 16.8.4 (March 5, 2019) 新版本 有不一样的会说明
react.js:React核心库。 react-dom.js:提供操作DOM的React扩展库。 babel.min.js:解析JSX语法代码转为JS代码的库。
【补充】babel.js的作用
浏览器不能直接解析JSX代码, 需要babel转译为纯JS的代码才能运行
只要用了JSX,都要加上type="text/babel", 声明需要babel来处理
使用JSX创建虚拟DOM
const VDOM = <h1>Hello,React</h1>
渲染虚拟DOM(元素)
- 语法: ReactDOM.render(virtualDOM, containerDOM)
- 作用: 将虚拟DOM元素渲染到页面中的真实容器DOM中显示 参数说明
- 参数一: 纯js或jsx创建的虚拟dom对象
- 参数二: 用来包含虚拟DOM元素的真实dom元素对象(一般是一个div)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello_react</title>
</head>
<body>
<!-- 准备好一个“容器” -->
<div id="test"></div>
<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel"> /* 此处一定要写babel */
//1.创建虚拟DOM
const VDOM = <h1>Hello,React</h1> /* 此处一定不要写引号,因为不是字符串 */
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
</body>
</html>
创建虚拟DOM的两种方式
3.1 纯JS方式(一般不用)
<div id="test"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" >
//1.创建虚拟DOM
const VDOM = React.createElement('h1',{id:'title'},React.createElement('span',{},'Hello,React'))
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
JSX方式
JSX方式就是js创建虚拟DOM的语法糖
<div id="test"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel" > /* 此处一定要写babel */
//1.创建虚拟DOM
const VDOM = ( /* 此处一定不要写引号,因为不是字符串 */
<h1 id="title">
<span>Hello,React</span>
</h1>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
虚拟DOM与真实DOM
打印输出虚拟DOM和真实DOM进行比较
const VDOM = ( /* 此处一定不要写引号,因为不是字符串 */
<h1 id="title">
<span>Hello,React</span>
</h1>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.getElementById('test'))
const TDOM = document.getElementById('demo')
console.log('虚拟DOM',VDOM);
console.log('真实DOM',TDOM);
debugger;
- 虚拟DOM本质是Object类型的对象(一般对象)
- 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性
- 虚拟DOM最终会被React转化为真实DOM,呈现在页面上
JSX入门
概述 全称: JavaScript XML React定义的一种类似于XML的JS扩展语法: JS + XML本质是React.createElement(component, props, ...children)方法的语法糖
作用: 用来简化创建虚拟DOM
写法:var ele =
Hello JSX!
注意1:它不是字符串, 也不是HTML/XML标签
注意2:它最终产生的就是一个JS对象
标签名任意: HTML标签或其它标签
标签属性任意: HTML标签属性或其它 基本语法规则
- 定义虚拟DOM时,不要写引号。
- 标签中混入JS表达式时要用 { }。
- 样式的类名指定不要用 class,要用 className。(因为class是ES6中类的关键字,所以不让用)
- 内联样式,要用 style={{ key:value }} 的形式去写。
- 只有一个根标签
- 标签必须闭合
- 标签首字母
- (1). 若小写字母开头,则将该标签转为html中同名元素,若html中无该标签对应的同名元素,则报错。
- (2). 若大写字母开头,React就去渲染对应的组件,若组件没有定义,则报错。 补充】 区分js表达式与js语句 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方 下面这些都是表达式:
- a
- a+b
- demo(1) // 函数调用表达式
- arr.map()
- function test () {}
- 语句(代码):
- 下面这些都是语句(代码):
- if(){ }
- for(){ }
- switch( ){case:xxxx}
总结
- 遇到
<开头的代码, 以标签的语法解析: html同名标签转换为html同名元素, 其它标签需要特别解析 - 遇到以
{开头的代码,以JS语法解析: 标签中的js表达式必须用{ }包含
基本理解和使用
函数式组件
// 1. 创建函数式组件
function MyComponent(){
console.log(this); //此处的this是undefined,因为babel编译后开启了严格模式
return <h2>我是用函数定义的组件(适用于简单组件的定义)</h2>
}
// 2. 渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test'))
执行了
ReactDOM.render(<MyComponent/>.......之后,发生了什么?
- React解析组件标签,找到了MyComponent组件。
- 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
补充】严格模式中的this
function sayThis() {
console.log(this)
}
sayThis() // Window {...}
function sayThis2() {
'use strict'
console.log(this)
}
sayThis2() // undefined
如果不开启严格模式,直接调用函数,函数中的this指向window如果开启了严格模式,直接调用函数,函数中的this是undefined
类式组件
//1.创建类式组件
class MyComponent extends React.Component {
render(){
//render是放在哪里的?—— MyComponent的原型对象上,供实例使用。
//render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。
console.log('render中的this:',this);
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent/>,document.getElementById('test'))
执行了ReactDOM.render(.......之后,发生了什么?
React解析组件标签,找到了MyComponent组件。 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。 将render返回的虚拟DOM转为真实DOM,随后呈现在页面中。
【补充】关于ES6中类的注意事项
- 类中的构造器不是必须要写的,要对实例进行一些初始化的操作,如添加指定属性时才写。
- 如果A类继承了B类,且A类中写了构造器,那么A类构造器中的
super是必须要调用的。 - 类中所定义的方法,都放在了类的原型对象上,供实例去使用。
注意
- 组件名必须首字母大写
- 虚拟DOM元素只能有一个根元素
- 虚拟DOM元素必须有结束标签
- 重点关注下渲染类组件标签的基本流程
- React内部会创建组件实例对象
- 调用
render()得到虚拟DOM, 并解析为真实DOM - 插入到指定的页面元素内部
组件实例的三大核心属性1: state 状态
理解
state是组件对象最重要的属性, 值是对象(可以包含多个key-value的组合)- 组件被称为"状态机", 通过更新组件的
state来更新对应的页面显示(重新渲染组件)
补充】类中方法的this指向问题
- 类中定义的方法,在内部默认开启了局部的严格模式\
- 开启严格模式,函数如果直接调用,this不会指向window,而是undefined
class Person {
constructor(name,age){
this.name = name
this.age = age
}
study(){
//study方法放在了哪里?——类的原型对象上,供实例使用
//通过Person实例调用study时,study中的this就是Person实例
console.log(this);
}
}
const p1 = new Person('tom',18)
p1.study() //通过实例调用study方法 Person
const x = p1.study
x() // 直接调用 undefined
注意
- 组件中render方法中的this为组件实例对象
- 组件自定义的方法中this为undefined,如何解决?
- a) 强制绑定this: 通过函数对象的bind()
- b) 箭头函数 + 赋值语句
- 状态数据state,不能直接修改或更新
- 状态必须通过setState()进行更新, 且更新是一种合并,不是替换。
组件实例的三大核心属性2: props
理解
- 每个组件对象都会有
props(properties的简写)属性 - 组件标签的所有属性都保存在
props中
作用
- 通过标签属性从组件外向组件内传递变化的数据
- 注意: 组件内部不可修改
props数据,是只读的
使用指南
内部读取某个属性值
this.props.name
扩展属性: 将对象的所有属性通过props传递(批量传递标签属性)
ReactDOM.render(<Person name="yk" age="18" sex="男"/>, document.getElementById('test'))
const person = {name: 'yk', age: 18, sex: '男'} ReactDOM.render(<Person { ...person }/>, document.getElementById('test'))
【补充】展开运算符
let arr1 = [1, 3, 5, 7, 9]
let arr2 = [2, 4, 6, 8, 10]
// 1. 展开一个数组
console.log(...arr1); // 1 3 5 7 9
// 2. 连接数组
let arr3 = [...arr1, ...arr2]
// 3. 在函数中使用
function sum(...numbers) {
return numbers.reduce((preValue, currentValue) => {
return preValue + currentValue
})
}
console.log(sum(1, 2, 3, 4)); // 10
// 4. 构造字面量对象时使用展开语法
let person = {
name: 'tom',
age: 18
}
// console.log(...person); // 报错,展开运算符不能展开对象
console.log({...person}) // {name: "tom", age: 18}
let person2 = { ...person } // 可以拷贝一个对象
person.name = 'jerry'
console.log(person2); // {name: "tom", age: 18}
console.log(person); // {name: "jerry", age: 18}
// 5. 合并对象
let person3 = {
...person,
name: 'jack',
address: "地球"
}
console.log(person3); // {name: "jack", age: 18, address: "地球"}
对props中的属性值进行类型限制和必要性限制
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
//对标签属性进行类型、必要性的限制
Person.propTypes = {
name:PropTypes.string.isRequired, // 限制name必传,且为字符串
sex:PropTypes.string, // 限制sex为字符串
age:PropTypes.number, // 限制age为数值
speak:PropTypes.func, // 限制speak为函数
}
可以写在类的里面,前面加static关键字
默认属性值
//指定默认标签属性值
Person.defaultProps = {
sex:'男', // sex默认值为男
age:18 //age默认值为18
}
组件类的构造函数
constructor(props){ super(props) console.log(this.props)//打印所有属性 }
构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
很多同学不知道这个为什么药使用super,是为了传递属性
函数式组件使用props
//创建组件
function Person (props){
const {name,age,sex} = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
Person.propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string, //限制sex为字符串
age:PropTypes.number, //限制age为数值
}
// 指定默认标签属性值
Person.defaultProps = {
sex:'男',// sex默认值为男
age:18 // age默认值为18
}
//渲染组件到页面
ReactDOM.render(<Person name="jerry"/>,document.getElementById('test1'))
refs与事件处理
理解
组件内的标签可以定义ref属性来标识自己
编码
字符串形式的ref(新版本不推荐使用了)
<input ref="input1"/>
this.refs.input1
示例
//创建组件
class Demo extends React.Component{
//展示左侧输入框的数据
showData = ()=>{
const {input1} = this.refs
alert(input1.value)
}
//展示右侧输入框的数据
showData2 = ()=>{
const {input2} = this.refs
alert(input2.value)
}
render(){
return(
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Demo />,document.getElementById('test'))
回调形式的ref
<input ref={(currentNode)=>{this.input1 = currentNode}} />
//简写
<input ref={ c => this.input1 = c } />
示例
//创建组件
class Demo extends React.Component{
//展示左侧输入框的数据
showData = ()=>{
const {input1} = this
alert(input1.value)
}
//展示右侧输入框的数据
showData2 = ()=>{
const {input2} = this
alert(input2.value)
}
render(){
return(
<div>
<input ref={ c => this.input1 = c } type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input onBlur={this.showData2} ref={c => this.input2 = c } type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Demo />,document.getElementById('test'))
回调执行次数
内联的回调,渲染时调用一次,每次更新都会执行两次
类绑定的回调,就在初始渲染时调用一次
影响不大,日常开发基本都用内联,方便一点
createRef创建ref容器
// React.createRef调用后可以返回一个容器 // 该容器可以存储被ref所标识的节点,该容器是“专人专用”的
myRef = React.createRef() <input ref={this.myRef}/>
使用
this.myRef.current
//创建组件
class Demo extends React.Component{
myRef = React.createRef()
myRef2 = React.createRef()
//展示左侧输入框的数据
showData = ()=>{
alert(this.myRef.current.value);
}
//展示右侧输入框的数据
showData2 = ()=>{
alert(this.myRef2.current.value);
}
render(){
return(
<div>
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Demo />,document.getElementById('test'))
React中的事件处理
- 通过onXxx属性指定事件处理函数(注意大小写)
- React使用的是自定义(合成)事件, 而不是使用的原生DOM事件----为了更好的兼容性
- React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ----为了的高效
- 通过event.target得到发生事件的DOM元素对象----不要过度使用ref
收集表单数据
理解
- 受控组件
- 非受控组件
非受控组件
页面中所有输入类DOM的值,都是现用现取的
// 创建组件
class Login extends React.Component {
handleSubmit = (event) => {
event.preventDefault() // 阻止表单提交
const {username, password} = this
alert(`您输入的用户名是 ${username.value},您输入的密码是:${password.value}`)
}
render() {
return (
<form action="https://www.baidu.com/" 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>
)
}
}
// 渲染组件
ReactDOM.render(<Login />, document.getElementById('test'))
受控组件
页面中输入类的DOM,随着输入的过程,将数据存储在状态state中,需要用的时候在从状态state中取(有点类似Vue中的双向数据绑定)
// 创建组件
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 action="https://www.baidu.com/" onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username" />
密码:<input onChange={this.savePassword} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
// 渲染组件
ReactDOM.render(<Login />, document.getElementById('test'))
高阶函数与函数的柯里化
高阶函数
- 高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
- 若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
- 若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
- 常见的高阶函数有:Promise、setTimeout、arr.map()等等
函数的柯里化
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
function sum1(a, b, c){
return a + b + c;
}
sum1(1, 2, 3)
// 柯里化后
function sum(a){
return(b)=>{
return (c)=>{
return a+b+c
}
}
}
sum(1)(2)(3)
//创建组件
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>
)
}
}
//渲染组件
ReactDOM.render(<Login/>,document.getElementById('test'))
组件的生命周期
理解
- 组件从创建到死亡它会经历一些特定的阶段。
- React组件中包含一系列勾子函数(生命周期回调函数), 会在特定的时刻调用。
- 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。
引入案例
需求:定义组件实现以下功能:
- 让指定的文本做显示 / 隐藏的渐变动画
- 从完全可见,到彻底消失,耗时2S
- 点击“不活了”按钮从界面中卸载组件
//创建组件
//生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子
class Life extends React.Component{
state = {opacity:1}
death = ()=>{
//卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
//组件挂完毕
componentDidMount(){
console.log('componentDidMount');
this.timer = setInterval(() => {
//获取原状态
let {opacity} = this.state
//减小0.1
opacity -= 0.1
if(opacity <= 0) opacity = 1
//设置新的透明度
this.setState({opacity})
}, 200);
}
//组件将要卸载
componentWillUnmount(){
//清除定时器
clearInterval(this.timer)
}
//初始化渲染、状态更新之后
render(){
console.log('render');
return(
<div>
<h2 style={{opacity:this.state.opacity}}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
//渲染组件
ReactDOM.render(<Life/>,document.getElementById('test'))
生命周期的三个阶段(旧)
初始化阶段
- 由ReactDOM.render()触发—初次渲染
- constructor() —— 类组件中的构造函数
- componentWillMount() —— 组件将要挂载 【即将废弃】
- render() —— 挂载组件
- componentDidMount() —— 组件挂载完成 比较常用
- 一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
class Count extends React.Component{
// 构造器
constructor(props){
alert('constructor')
console.log('Count---constructor');
super(props)
//初始化状态
this.state = {count:0}
}
add = () => {
const {count} = this.state
this.setState({count: count+1})
}
//组件将要挂载的钩子
componentWillMount(){
alert('componentWillMount')
console.log('Count---componentWillMount');
}
render(){
alert('render')
console.log('Count---render');
const {count} = this.state
return(
<div>
<h1>当前计数值:{count}</h1>
<button onClick={this.add}>点我+1</button>
</div>
)
}
//组件挂载完毕的钩子
componentDidMount(){
alert('componentDidMount')
console.log('Count---componentDidMount');
}
}
ReactDOM.render(<Count/>, document.getElementById('test'))
更新阶段【第一种情况】父组件重新render触发
componentWillReceiveProps() —— 接收属性参数(非首次)【即将废弃】 然后调用下面的钩子函数
【第二种情况】由组件内部this.setSate()
shouldComponentUpdate() —— 组件是否应该被更新(默认返回true) 然后调用下面的钩子函数
【第三种情况】强制更新 forceUpdate()
componentWillUpdate() ——组件将要更新 【即将废弃】 render() —— 组件更新 componentDidUpdate() —— 组件完成更新
卸载组件
由ReactDOM.unmountComponentAtNode()触发
componentWillUnmount()—— 组件即将卸载
生命周期的三个阶段(新)
更新阶段
- 由组件内部this.setSate()或父组件重新render触发或强制更新forceUpdate()
- getDerivedStateFromProps() —— 从props得到一个派生的状态 【新增】
- shouldComponentUpdate() —— 组件是否应该被更新(默认返回true)
- render() —— 挂载组件
- getSnapshotBeforeUpdate() —— 在更新之前获取快照【新增】
- componentDidUpdate(prevProps, prevState, snapshotValue) —— 组件完成更新
- 卸载组件
- 由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount() —— 组件即将卸载
重要的勾子
render:初始化渲染或更新渲染调用componentDidMount:开启监听, 发送ajax请求componentWillUnmount:做一些收尾工作, 如: 清理定时器
即将废弃的勾子
componentWillMountcomponentWillReceivePropscomponentWillUpdate
现在使用会出现警告,下一个大版本需要加上UNSAFE_前缀才能使用,以后可能会被彻底废弃,不建议使用。
虚拟DOM与DOM Diffing算法
基本原理图
关于key的经典面试题
react/vue中的key有什么作用?(key的内部原理是什么?) 为什么遍历列表时,key最好不要用index? 虚拟DOM中key的作用 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用。
详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】, 随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
- 旧虚拟DOM中找到了与新虚拟DOM相同的key:
- (1).若虚拟DOM中内容没变, 直接使用之前的真实DOM
- (2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM
- 旧虚拟DOM中未找到与新虚拟DOM相同的key
- 根据数据创建新的真实DOM,随后渲染到到页面
用index作为key可能会引发的问题
若对数据进行:逆序添加、逆序删除等破坏顺序操作: 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低
如果结构中还包含输入类的DOM: 会产生错误DOM更新 ==> 界面有问题
注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作, 仅用于渲染列表用于展示,使用index作为key是没有问题的
SPA的理解
- 单页Web应用(single page web application,
SPA) - 整个应用只有一个完整的页面。
- 点击页面中的链接不会刷新页面,只会做页面的局部更新。
- 数据都需要通过
ajax请求获取, 并在前端异步展现。
路由的理解
什么是路由?
- 一个路由就是一个映射关系(
key: value) key为路径,value可能是function或component
路由分类
后端路由
- 理解:
value是function, 用来处理客户端提交的请求。 - 注册路由:
router.get(path, function(req, res)) - 工作过程:当
node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
前端路由
- 浏览器端路由,value是
component,用于展示页面内容。 - 注册路由:
<Route path="/test" component={Test}> - 工作过程:当浏览器的
path变为/test时, 当前路由组件就会变为Test组件
react-router-dom 的理解
React的一个插件库。- 专门用来实现一个SPA应用。
- 基于
React的项目基本都会用到此库。
react-router-dom相关API
内置组件
<BrowserRouter><HashRouter><Route><Redirect><Link><NavLink><Switch>
其它
history对象match对象withRouter函数
基本路由使用
效果
准备
- 下载react-router-dom:
npm install react-router-dom
- 引入bootstrap.css:
<link rel="stylesheet" href="/css/bootstrap.css">
路由的基本使用
明确好界面中的导航区、展示区
导航区的a标签改为Link标签
<Link to="/xxxxx">Demo</Link>
展示区写Route标签进行路径的匹配
<Route path='/xxxx' component={Demo}/>
<App>的最外侧包裹了一个<BrowserRouter>或<HashRouter>
实现
index.js
这里用一个标签将整个App包起来,保证使用的是同一个路由器
//引入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')
)
App.jsx
import React, { Component } from 'react'
import { Link,Route } from 'react-router-dom'
import Home from './Home'
import About from './About'
export default class App extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 原生html中,靠<a>跳转不同的页面 */}
{/* <a className="list-group-item" href="./about.html">About</a>
<a className="list-group-item active" href="./home.html">Home</a> */}
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Route path='/about' component={About} />
<Route path='/home' component={Home} />
</div>
</div>
</div>
</div>
</div>
)
}
}
路由组件与一般组件的区别
-
写法不同: 一般组件: 路由组件:
-
存放位置不同: 一般组件:components
路由组件:pages
-
接收到的props不同: 一般组件:写组件标签时传递了什么,就能收到什么
路由组件:接收到三个固定的属性
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
NavLink与封装NavLink NavLink可以实现路由链接的高亮,通过activeClassName属性指定样式名,默认是"active"
<NavLink activeClassName="demo" className="list-group-item" to="/home">Home</NavLink>
可以自己封装一个NavLink【一般组件】
import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
// console.log(this.props);
return (
<NavLik activeClassName="demo" className="list-group-item" {...this.props} />
)
}
}
标签体内容是特殊的标签属性通过this.props.children可以获取标签体内容
使用
<MyNavLink to="/about">About</MyNavLink> <MyNavLink to="/home">Home</MyNavLink>
Switch的使用
- 通常情况下,path和component是一一对应的关系。
- Switch可以提高路由匹配效率(单一匹配)。
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Test}/>
</Switch>
这样只要匹配到了第一个就不会再往下匹配了
解决多级路径刷新页面样式丢失的问题
- public/index.html 中 引入样式时不写
./写/(常用)【绝对路径】 - public/index.html 中 引入样式时不写
./写%PUBLIC_URL%(常用) - 使用
HashRouter
路由的严格匹配与模糊匹配
- 默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
- 开启严格匹配:
<Route exact={true} path="/about" component={About}/> - 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
Redirect的使用 【重定向】
- 一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
- 具体编码:
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Redirect to="/about"/>
</Switch>
嵌套路由使用
效果
注意
- 注册子路由时要写上父路由的path值
- 路由的匹配是按照注册路由的顺序进行的
实现
Home/index.jsx
import React, { Component } from 'react'
import { Route, NavLink,Redirect,Switch } from 'react-router-dom'
import News from './News'
import Message from './Message'
export default class Home extends Component {
render() {
return (
<div>
<h3>我是Home的内容</h3>
<div>
<ul className="nav nav-tabs">
<li>
<NavLink className="list-group-item" to="/home/news">News</NavLink>
</li>
<li>
<NavLink className="list-group-item" to="/home/message">Message</NavLink>
</li>
</ul>
<Switch>
<Route path='/home/news' component={News} />
<Route path='/home/message' component={Message} />
<Redirect to='/home/news' />
</Switch>
</div>
</div>
)
}
}
向路由组件传递参数数据
效果
具体方法
方法1. params参数
路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>
注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
接收参数:<Route path="/demo/test/:name/:age" component={Test}/>
Message/index.jsx
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom';
import Detail from './Detail';
export default class Message extends Component {
state = {
messageArr: [
{ id: '01', title: '消息1' },
{ id: '02', title: '消息2' },
{ id: '03', title: '消息3' },
]
}
render() {
const { messageArr } = this.state;
return (
<div>
<ul>
{
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
{/* 向路由组件传递params参数 */}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
{/* 声明接收params参数 */}
<Route path="/home/message/detail/:id/:title" component={Detail}/>
</div>
)
}
}
Detail/index.jsx
import React, { Component } from 'react'
export default class Detail extends Component {
state = {
detailData : [
{ id: '01', content: '你好啊' },
{ id: '02', content: '还不错鸭' },
{ id: '03', content: '显示我吧' }
]
}
render() {
console.log(this.props)
// 接收params参数
const { id, title } = this.props.match.params
const findResult= this.state.detailData.find((dataObj) => {
return dataObj.id === id
})
return (
<div>
<ul>
<li>ID: {id }</li>
<li>Title: {title }</li>
<li>Content: { findResult.content}</li>
</ul>
</div>
)
}
}
方法2. search参数 路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情 注册路由(无需声明,正常注册即可): 接收参数:this.props.location.search 备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
import qs from 'querystring'
let obj = {name:'tom', age:18}
console.log(qs.stringify(obj)) // name=tom&age=18
let str = 'carName=Benz&price=199'
console.log(qs.parse(str)) // {carName: 'Benz', price: 199}
方法3. state参数 路由链接(携带参数):<Link to={{ pathname:'/demo/test', state:{name:'tom',age:18} }}>详情 注册路由(无需声明,正常注册即可): 接收参数:this.props.location.state 备注:刷新也可以保留住参数【history对象记录着在】
Message/index.jsx
export default class Message extends Component {
render() {
const {messageArr} = this.state
return (
<div>
<ul>
{
messageArr.map((msgObj)=>{
return (
<li key={msgObj.id}>
{/* 向路由组件传递params参数 */}
{/* <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> */}
{/* 向路由组件传递search参数 */}
{/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */}
{/* 向路由组件传递state参数 */}
<Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr/>
{/* 声明接收params参数 */}
{/* <Route path="/home/message/detail/:id/:title" component={Detail}/> */}
{/* search参数无需声明接收,正常注册路由即可 */}
{/* <Route path="/home/message/detail" component={Detail}/> */}
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
</div>
)
}
}
Detail/index.jsx
import React, { Component } from 'react'
// import qs from 'querystring'
export default class Detail extends Component {
render() {
console.log(this.props);
// 接收params参数
// const {id,title} = this.props.match.params
// 接收search参数
// const {search} = this.props.location
// const {id,title} = qs.parse(search.slice(1))
// 接收state参数
const {id,title} = this.props.location.state || {}
const findResult = DetailData.find((detailObj)=>{
return detailObj.id === id
}) || {}
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
多种路由跳转方式
编程式路由导航
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
- this.prosp.history.push()
- this.prosp.history.replace()
- this.prosp.history.goBack()
- this.prosp.history.goForward()
- this.prosp.history.go()
withRouter的使用
export default withRouter(Header)
withRouter可以加工一般组件,让一般组件具备路由组件所特有的API
withRouter的返回值是一个新组件
注意
BrowserRouter与HashRouter的区别
底层原理不一样:
- BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
- HashRouter使用的是URL的哈希值。
- path表现形式不一样
- BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
- HashRouter的路径包含#,例如:localhost:3000/#/demo/test
- 刷新后对路由state参数的影响
- BrowserRouter没有任何影响,因为state保存在history对象中
- HashRouter刷新后会导致路由state参数的丢失!!!
- 备注:HashRouter可以用于解决一些路径错误相关的问题。
redux理解
相关文档
中文文档: www.redux.org.cn/
Github: github.com/reactjs/red…
先把它装上
npm i redux
redux是什么
- redux是一个专门用于做
状态管理的JS库(不是react插件库)。 - 可以用在react, angular, vue等项目中, 但基本与react配合使用。
- 作用: 集中式管理react应用中多个组件
共享的状态
什么情况下需要使用redux
- 某个组件的状态,需要让其他组件可以随时拿到(共享)
- 一个组件需要改变另一个组件的状态(通信)
- 总体原则:能不用就不用, 如果不用比较吃力才考虑使用
redux工作流程
redux的三个核心概念
action
-
动作的对象
-
包含 2 个属性
type:标识属性, 值为字符串, 唯一, 必要属性data:数据属性, 值类型任意, 可选属性
-
例子:
{ type: 'ADD_STUDENT',data:{name: 'tom',age:18} }
reducer
- 用于初始化状态、加工状态。
- 加工时,根据旧的
state和action, 产生新的state的纯函数。
store
将state、action、reducer联系在一起的对象
如何得到此对象?
import {createStore} from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
此对象的功能?
- getState(): 得到state
- dispatch(action): 分发action, 触发reducer调用, 产生新的state
- subscribe(listener): 注册监听, 当产生了新的state时, 自动调用
API
createStore()
作用:创建包含指定reducer的store对象
store对象
作用: redux库最核心的管理对象 它内部维护着:
- state
- reducer
核心方法:
getState()dispatch(action)subscribe(listener)具体编码:
store.getState()
store.dispatch({type:'INCREMENT', number})
store.subscribe(render)
applyMiddleware()
作用:应用上基于redux的中间件(插件库)
combineReducers()
作用:合并多个reducer函数
使用redux编写应用
效果
实现
redux/store.js
该文件专门用于暴露一个store对象,整个应用只有一个store对象
/ 引入createStore,专门用于创建redux中最为核心的store对象
import { createStore } from "redux";
// 引入为Count组件服务的reducer
import countReducer from "./count_reducer.js";
// 暴露store
export default createStore(countReducer);
redux/count_reducer.js
该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
import {INCREMENT,DECREMENT} from './constant'
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
// console.log(preState);
//从action对象中获取:type、data
const {type,data} = action
//根据type决定如何加工数据
switch (type) {
case INCREMENT: //如果是加
return preState + data
case DECREMENT: //若果是减
return preState - data
default:
return preState
}
}
redux/count_action.js
该文件专门为Count组件生成action对象
import {INCREMENT,DECREMENT} from './constant'
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})
redux/constant.js
该模块是用于定义action对象中type类型的常量值,目的只有一个:便于管理的同时防止程序员单词写错
export const INCREMENT = 'increment' export const DECREMENT = 'decrement'
Count/index.jsx
import React, { Component } from 'react'
//引入store,用于获取redux中保存状态
import store from '../../redux/store'
//引入actionCreator,专门用于创建action对象
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
export default class Count extends Component {
/* componentDidMount(){
//检测redux中状态的变化,只要变化,就调用render
store.subscribe(()=>{
this.setState({})
})
} */
//加法
increment = ()=>{
const {value} = this.selectNumber
store.dispatch(createIncrementAction(value*1))
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
store.dispatch(createDecrementAction(value*1))
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const count = store.getState()
if(count % 2 !== 0){
store.dispatch(createIncrementAction(value*1))
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch(createIncrementAction(value*1))
},500)
}
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
ReactDOM.render(<App/>,document.getElementById('root'))
// 监测redux中状态的改变,如redux的状态发生了改变,那么重新渲染App组件
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
总结
store.js:
1). 引入redux中的createStore函数,创建一个store 2). createStore调用时要传入一个为其服务的reducer 3). 记得暴露store对象
count_reducer.js:
- reducer的本质是一个函数,接收:preState,action,返回加工后的状态
- reducer有两个作用:初始化状态,加工状态
- reducer被第一次调用时,是store自动触发的,
- 传递的preState是undefined,
- 传递的action是:{type:'@@REDUX/INIT_a.2.b.4}
- 在index.js中监测store中状态的改变,一旦发生改变重新渲染
- 备注:redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。
完整版新增文件:
- count_action.js 专门用于创建action对象
- constant.js 放置容易写错的type值
redux异步编程
理解
- redux默认是不能进行异步处理的,
- 某些时候应用中需要在redux中执行异步任务(ajax, 定时器)
使用异步中间件
npm install redux-thunk
count_action.js
该模块专门为Count组件生成action对象 异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的
import {INCREMENT,DECREMENT} from './constant'
//同步action,就是指action的值为Object类型的一般对象
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})
//异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的
export const createIncrementAsyncAction = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(createIncrementAction(data))
},time)
}
}
store.js
该模块专门用于暴露一个store对象,整个应用只有一个store对象
在这里引入中间件
//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'
//暴露store
export default createStore(countReducer,applyMiddleware(thunk))
总结
- 明确:延迟的动作不想交给组件自身,想交给action
- 何时需要异步action:想要对状态进行操作,但是具体的数据靠异步任务返回。
- 具体编码:
- npm install redux-thunk,并配置在store中
- 创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。
- 异步任务有结果后,分发一个同步的action去真正操作数据。
- 备注:异步action不是必须要写的,完全可以自己等待异步任务的结果了再去分发同步action。
react-redux
理解
- 一个React插件库
- 专门用来简化React应用中使用redux
react-redux将所有组件分成两大类
UI组件 1. 2. 只负责 UI 的呈现,不带有任何业务逻辑 3. 通过props接收数据(一般数据和函数) 4. 不使用任何 Redux 的 API 5. 一般保存在components文件夹下 容器组件
- 负责管理数据和业务逻辑,不负责UI的呈现
- 使用 Redux 的 API
- 一般保存在containers文件夹下
相关API
Provider:让所有组件都可以得到state数据connect:用于包装 UI 组件生成容器组件mapStateToprops:将外部的数据(即state对象)转换为UI组件的标签属性mapDispatchToProps:将分发action的函数转换为UI组件的标签属性
基本使用
安装
npm install react-redux
components/Count/index.jsx
UI组件
import React, { Component } from 'react'
export default class Count extends Component {
//加法
increment = ()=>{
const {value} = this.selectNumber
this.props.jia(value*1)
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
this.props.jian(value*1)
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
if(this.props.count % 2 !== 0){
this.props.jia(value*1)
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
this.props.jiaAsync(value*1,500)
}
render() {
//console.log('UI组件接收到的props是',this.props);
return (
<div>
<h1>当前求和为:{this.props.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
containers/Count/index.jsx
容器组件
//引入Count的UI组件
import CountUI from '../../components/Count'
//引入action
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction
} from '../../redux/count_action'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'
/*
1.mapStateToProps函数返回的是一个对象;
2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
3.mapStateToProps用于传递状态
*/
function mapStateToProps(state){
return {count:state}
}
/*
1.mapDispatchToProps函数返回的是一个对象;
2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
3.mapDispatchToProps用于传递操作状态的方法
*/
function mapDispatchToProps(dispatch){
return {
jia:number => dispatch(createIncrementAction(number)),
jian:number => dispatch(createDecrementAction(number)),
jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
}
}
//使用connect()()创建并暴露一个Count的容器组件
export default connect(mapStateToProps,mapDispatchToProps)(CountUI)
App.jsx
给容器组件传递store
import React, { Component } from 'react'
import Count from './containers/Count'
import store from './redux/store'
export default class App extends Component {
render() {
return (
<div>
{/* 给容器组件传递store */}
<Count store={store} />
</div>
)
}
}
总结 明确两个概念:
- UI组件:不能使用任何 redux 的api,只负责页面的呈现、交互等。
- 容器组件:负责和 redux 通信,将结果交给UI组件。
- 如何创建一个容器组件————靠 react-redux 的 connect 函数
- connect(mapStateToProps,mapDispatchToProps)(UI组件)
- mapStateToProps: 映射状态,返回值是一个对象
- mapDispatchToProps: 映射操作状态的方法,返回值是一个对象
- 备注1:容器组件中的store是靠props传进去的,而不是在容器组件中直接引入
- 备注2:mapDispatchToProps,也可以是一个对象【优化1】
优化
优化1 简写mapDispatchToProps
//使用connect()()创建并暴露一个Count的容器组件
export default connect(
state => ({count:state}),
//mapDispatchToProps的一般写法
/* dispatch => ({
jia:number => dispatch(createIncrementAction(number)),
jian:number => dispatch(createDecrementAction(number)),
jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
}) */
//mapDispatchToProps的简写
{
jia:createIncrementAction,
jian:createDecrementAction,
jiaAsync:createIncrementAsyncAction,
}
)(CountUI)
**容器组件可以检测redux中的状态改变,并渲染页面,所以不需要在index.js中检测了 不要在App.jsx中给子组件传递store了 **
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
import {Provider} from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
)
优化3 整合UI组件和容器组件
每个组件两个文件夹太麻烦了,直接整合在一起就好了~
containers/Count/index.jsx
import React, { Component } from 'react'
//引入action
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction
} from '../../redux/count_action'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'
//定义UI组件
class Count extends Component {
state = {carName:'奔驰c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
this.props.jia(value*1)
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
this.props.jian(value*1)
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
if(this.props.count % 2 !== 0){
this.props.jia(value*1)
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
this.props.jiaAsync(value*1,500)
}
render() {
//console.log('UI组件接收到的props是',this.props);
return (
<div>
<h1>当前求和为:{this.props.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
使用connect()()创建并暴露一个Count的容器组件
export default connect(
state => ({count:state}),
//mapDispatchToProps的一般写法
/* dispatch => ({
jia:number => dispatch(createIncrementAction(number)),
jian:number => dispatch(createDecrementAction(number)),
jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
}) */
//mapDispatchToProps的简写
{
jia:createIncrementAction,
jian:createDecrementAction,
jiaAsync:createIncrementAsyncAction,
}
)(Count)
总结
- 容器组件和UI组件整合一个文件
- 无需自己给容器组件传递store,给包裹一个
<Provider store={store}>即可。 - 使用了react-redux后也不用再自己检测redux中状态的改变了,容器组件可以自动完成这个工作。
- mapDispatchToProps也可以简单的写成一个对象
- 一个组件要和redux“打交道”要经过哪几步?
- 定义好UI组件—不暴露
- 引入connect生成一个容器组件,并暴露,写法如下:
connect(
state => ({key:value}), //映射状态
{key:xxxxxAction} //映射操作状态的方法
)(UI组件)
- 在UI组件中通过this.props.xxxxxxx读取和操作状态
store.js
/*
该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import { createStore, applyMiddleware, combineReducers } from "redux";
//引入为Count组件服务的reducer
import countReducer from "./reducers/count";
//引入为Count组件服务的reducer
import personReducer from "./reducers/person";
//引入redux-thunk,用于支持异步action
import thunk from "redux-thunk";
// 汇总所有的reducer变为一个总的reducer
const allReducer = combineReducers({
he: countReducer,
rens: personReducer,
});
//暴露store
export default createStore(allReducer, applyMiddleware(thunk));
总结
- 定义一个Pserson组件,和Count组件通过redux共享数据。
- 为Person组件编写:reducer、action,配置constant常量。
- 重点:Person的reducer和Count的reducer要使用combineReducers进行合并,合并后的总状态是一个对象!!!
- 交给store的是总reducer,最后注意在组件中取出状态的时候,记得“取到位”。 纯函数和高阶函数 reducer要求是一个纯函数,所以操作数组的时候,不能用push之类的方法
纯函数
- 一类特别的函数: 只要是同样的输入(实参),必定得到同样的输出(返回)
- 必须遵守以下一些约束
- 不得改写参数数据
- 不会产生任何副作用,例如网络请求,输入和输出设备
- 不能调用Date.now()或者Math.random()等不纯的方法
- redux的reducer函数必须是一个纯函数 高阶函数
- 理解: 一类特别的函数
- 情况1: 参数是函数
- 情况2: 返回是函数
- 常见的高阶函数:
- 定时器设置函数
- 数组的forEach()/map()/filter()/reduce()/find()/bind()
- promise
- react-redux中的connect函数
- 作用: 能实现更加动态, 更加可扩展的功能
最终版
- 所有变量名字要规范,尽量触发对象的简写形式。
- reducers文件夹中,编写index.js专门用于汇总并暴露所有的reducer
containers/Count/index.jsx
import React, { Component } from 'react'
//引入action
import {
increment,
decrement,
incrementAsync
} from '../../redux/actions/count'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'
//定义UI组件
class Count extends Component {
//加法
increment = ()=>{
const {value} = this.selectNumber
this.props.increment(value*1)
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
this.props.decrement(value*1)
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
if(this.props.count % 2 !== 0){
this.props.increment(value*1)
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
this.props.incrementAsync(value*1,500)
}
render() {
//console.log('UI组件接收到的props是',this.props);
return (
<div>
<h2>我是Count组件,下方组件总人数为:{this.props.renshu}</h2>
<h4>当前求和为:{this.props.count}</h4>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
//使用connect()()创建并暴露一个Count的容器组件
export default connect(
state => ({
count:state.count,
personCount:state.persons.length
}),
{increment,decrement,incrementAsync}
)(Count)
containers/Person/index.jsx
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import {connect} from 'react-redux'
import {addPerson} from '../../redux/actions/person'
class Person extends Component {
addPerson = ()=>{
const name = this.nameNode.value
const age = this.ageNode.value*1
const personObj = {id:nanoid(),name,age}
this.props.addPerson(personObj)
this.nameNode.value = ''
this.ageNode.value = ''
}
render() {
return (
<div>
<h2>我是Person组件,上方组件求和为{this.props.count}</h2>
<input ref={c=>this.nameNode = c} type="text" placeholder="输入名字"/>
<input ref={c=>this.ageNode = c} type="text" placeholder="输入年龄"/>
<button onClick={this.addPerson}>添加</button>
<ul>
{
this.props.persons.map((p)=>{
return <li key={p.id}>{p.name}--{p.age}</li>
})
}
</ul>
</div>
)
}
}
export default connect(
state => ({
persons:state.persons,
count:state.count
}),//映射状态
{addPerson}//映射操作状态的方法
)(Person)
redux/actions/count.js
/*
该文件专门为Count组件生成action对象
*/
import { INCREMENT, DECREMENT } from "../constant";
//同步action,就是指action的值为Object类型的一般对象
export const increment = (data) => ({ type: INCREMENT, data });
export const decrement = (data) => ({ type: DECREMENT, data });
//异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的。
export const incrementAsync = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(increment(data));
}, time);
};
};
redux/action/person.js
import { ADD_PERSON } from "../constant";
//创建增加一个人的action动作对象
export const addPerson = (personObj) => ({ type: ADD_PERSON, data: personObj });
redux/reducers/count.js
/*
1.该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
2.reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
*/
import { INCREMENT, DECREMENT } from "../constant";
const initState = 0; //初始化状态
export default function countReducer(preState = initState, action) {
// console.log('countReducer@#@#@#');
//从action对象中获取:type、data
const { type, data } = action;
//根据type决定如何加工数据
switch (type) {
case INCREMENT: //如果是加
return preState + data;
case DECREMENT: //若果是减
return preState - data;
default:
return preState;
}
}
redux/reducers/person.js
import { ADD_PERSON } from "../constant";
//初始化人的列表
const initState = [{ id: "001", name: "tom", age: 18 }];
export default function personReducer(preState = initState, action) {
const { type, data } = action;
switch (type) {
case ADD_PERSON: //若是添加一个人
//preState.unshift(data) //此处不可以这样写,这样会导致preState被改写了,personReducer就不是纯函数了。
return [data, ...preState];
default:
return preState;
}
}
redux/reducers/index.js
/*
该文件用于汇总所有的reducer为一个总的reducer
*/
//引入combineReducers,用于汇总多个reducer
import { combineReducers } from "redux";
//引入为Count组件服务的reducer
import count from "./count";
//引入为Person组件服务的reducer
import persons from "./person";
//汇总所有的reducer变为一个总的reducer
export default combineReducers({
count,
persons,
});
redux/constant.js
/*
该模块是用于定义action对象中type类型的常量值,目的只有一个:便于管理的同时防止程序员单词写错
*/
export const INCREMENT = "increment";
export const DECREMENT = "decrement";
export const ADD_PERSON = "add_person";
redux/store.js
/*
该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import { createStore, applyMiddleware } from "redux";
//引入汇总之后的reducer
import reducer from "./reducers";
//引入redux-thunk,用于支持异步action
import thunk from "redux-thunk";
//引入redux-devtools-extension
import { composeWithDevTools } from "redux-devtools-extension";
//暴露store
export default createStore(
reducer,
composeWithDevTools(applyMiddleware(thunk))
);
App.jsx
import React, { Component } from 'react'
import Count from './containers/Count' //引入的Count的容器组件
import Person from './containers/Person' //引入的Person的容器组件
export default class App extends Component {
render() {
return (
<div>
<Count/>
<hr/>
<Person/>
</div>
)
}
}
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./redux/store";
import { Provider } from "react-redux";
ReactDOM.render(
/* 此处需要用Provider包裹App,目的是让App所有的后代容器组件都能接收到store */
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
【React】拓展
- setState
- 路由组件懒加载
- Hooks
- Context
- PureComponent
- 插槽
- 错误边界
- 组件通信方式
- setState
setState
setState更新状态其实有两种写法
setState这个方法在调用的时候是同步的,但是引起React的状态更新是异步的
【React状态更新是异步的】
对象式
setState(stateChange, [callback])
stateChange为状态改变对象 (该对象可以体现出状态的更改)callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用【异步更新后调用,可以拿到更新后的状态的值】 之前一直写的,就是直接传一个对象
this.setState({ count: this.state.count+1 })
函数式
setState(updater, [callback])
updater为返回stateChange对象的函数【返回值是对象】updater可以接收到state和propscallback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用【和对象式是一样的】 传的时候回调函数,可以接收到state和props,函数的返回值是设置状态的对象
setState(state => ({count: state.count+1}))
总结
- 对象式的setState是函数式的setState的简写方式【语法糖】
- 使用原则
- 如果新状态不依赖于原状态【使用对象方式】
- 如果新状态依赖于原状态 【使用函数方式】
- 如果需要在setState()执行后,获取最新的状态数据,可以在第二个callback函数中读取到异步更新的最新值
路由组件的懒加载 lazyLoad
用的时候才加载,一般是路由组件进行懒加载 如果不用路由懒加载,页面在第一次进入的时候,就请求了所有组件的数据,如果组件过多,过多的请求这就没有必要了,应该是用户按哪个链接再请求哪个组件
通过React的lazy函数配合import()函数动态加载路由组件【路由组件代码会被分开打包】 通过指定在加载得到路由打包文件前显示一个自定义loading界
import React, { Component, lazy, Suspense} from 'react'
import {NavLink,Route} from 'react-router-dom'
// import Home from './Home'
// import About from './About'
import Loading from './Loading'
const Home = lazy(()=> import('./Home') )
const About = lazy(()=> import('./About'))
export default class Demo extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
<NavLink className="list-group-item" to="/about">About</NavLink>
<NavLink className="list-group-item" to="/home">Home</NavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
<Suspense fallback={<Loading/>}>
{/* 注册路由 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</Suspense>
</div>
</div>
</div>
</div>
</div>
)
}
}
Hooks
React Hook/Hooks是什么? Hook 是 React 16.8 的新增特性。 它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。【函数式组件】
三个常用的Hook
- State Hook: React.useState()
- Effect Hook: React.useEffect()
- Ref Hook: React.useRef() State Hook
State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作 语法: const [xxx, setXxx] = React.useState(initValue)
useState()说明: 参数: 第一次初始化指定的值在内部作缓存 返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
setXxx()2种写法:
setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值 setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值
Effect Hook
Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
React中的副作用操作:
- 发ajax请求数据获取
- 设置订阅 / 启动定时器
- 手动更改真实DOM
语法和说明:
useEffect(() => {
// 在此可以执行任何带副作用操作【挂载+更新】
return () => { // 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等【卸载前】
}
}, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行
可以把 useEffect Hook 看做如下三个函数的组合
componentDidMount()
componentDidUpdate()
componentWillUnmount()
Ref Hook Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据 语法: const refContainer = useRef() 作用:保存标签对象,功能与React.createRef()一样 代码示例
import React from 'react'
import ReactDOM from 'react-dom'
//类式组件
/* class Demo extends React.Component {
state = {count:0}
myRef = React.createRef()
add = ()=>{
this.setState(state => ({count:state.count+1}))
}
unmount = ()=>{
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
show = ()=>{
alert(this.myRef.current.value)
}
componentDidMount(){
this.timer = setInterval(()=>{
this.setState( state => ({count:state.count+1}))
},1000)
}
componentWillUnmount(){
clearInterval(this.timer)
}
render() {
return (
<div>
<input type="text" ref={this.myRef}/>
<h2>当前求和为{this.state.count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.unmount}>卸载组件</button>
<button onClick={this.show}>点击提示数据</button>
</div>
)
}
} */
function Demo(){
//console.log('Demo');
const [count,setCount] = React.useState(0)
const myRef = React.useRef()
React.useEffect(()=>{
let timer = setInterval(()=>{
setCount(count => count+1 )
},1000)
return ()=>{
clearInterval(timer)
}
},[])
//加的回调
function add(){
//setCount(count+1) //第一种写法
setCount(count => count+1 )
}
//提示输入的回调
function show(){
alert(myRef.current.value)
}
//卸载组件的回调
function unmount(){
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return (
<div>
<input type="text" ref={myRef}/>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
<button onClick={unmount}>卸载组件</button>
<button onClick={show}>点我提示数据</button>
</div>
)
}
export default Demo
Fragment
文档碎片
使用
React中的副作用操作:
或者
<></>
作用 可以不用必须有一个真实的DOM根标签了
import React, { Component,Fragment } from 'react'
export default class Demo extends Component {
render() {
return (
<Fragment kye={1}>
<input type="text"/>
<input type="text"/>
</Fragment>
)
}
Context
上下文
理解 一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信
使用 创建Context容器对象 const XxxContext = React.createContext()
渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<xxxContext.Provider value={数据}>
子组件
</xxxContext.Provider>
后代组件读取数据:
第一种方式:仅适用于类组件
static contextType = xxxContext // 声明接收context
this.context // 读取context中的value数据
第二种方式: 函数组件与类组件都可以
<xxxContext.Consumer>
{
value => ( // value就是context中的value数据
要显示的内容
)
}
</xxxContext.Consumer>
注意 在应用开发中一般不用context, 一般都用它来封装react插件
import React, { Component } from 'react'
import './index.css'

//创建Context对象
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext
export default class A extends Component {
state = {username:'tom',age:18}
render() {
const {username,age} = this.state
return (
<div className="parent">
<h3>我是A组件</h3>
<h4>我的用户名是:{username}</h4>
<Provider value={{username,age}}>
<B/>
</Provider>
</div>
)
}
}
class B extends Component {
render() {
return (
<div className="child">
<h3>我是B组件</h3>
<C/>
</div>
)
}
}
/* class C extends Component {
//声明接收context
static contextType = MyContext
render() {
const {username,age} = this.context
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:{username},年龄是{age}</h4>
</div>
)
}
} */
function C(){
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:
<Consumer>
{value => `${value.username},年龄是${value.age}`}
</Consumer>
</h4>
</div>
)
}
组件优化
Component的问题
- 只要执行setState(),即使不改变状态数据, 组件也会重新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.carName === nextState.carName
}
办法2: 使用PureComponent PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
注意
只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false不要直接修改state数据, 而是要产生新数据项目中一般使用PureComponent来优化
import React, { PureComponent } from 'react'
import './index.css'
export default class Parent extends PureComponent {
state = {carName:"奔驰c36",stus:['小张','小李','小王']}
addStu = ()=>{
/* const {stus} = this.state
stus.unshift('小刘')
this.setState({stus}) */
const {stus} = this.state
this.setState({stus:['小刘',...stus]})
}
changeCar = ()=>{
this.setState({carName:'迈巴赫'})
// const obj = this.state
// obj.carName = '迈巴赫'
// console.log(obj === this.state);
// this.setState(obj)
}
/* shouldComponentUpdate(nextProps,nextState){
// console.log(this.props,this.state); //目前的props和state
// console.log(nextProps,nextState); //接下要变化的目标props,目标state
return !this.state.carName === nextState.carName
} */
render() {
console.log('Parent---render');
const {carName} = this.state
return (
<div className="parent">
<h3>我是Parent组件</h3>
{this.state.stus}
<span>我的车名字是:{carName}</span><br/>
<button onClick={this.changeCar}>点我换车</button>
<button onClick={this.addStu}>添加一个小刘</button>
<Child carName="奥拓"/>
</div>
)
}
}
class Child extends PureComponent {
/* shouldComponentUpdate(nextProps,nextState){
console.log(this.props,this.state); //目前的props和state
console.log(nextProps,nextState); //接下要变化的目标props,目标state
return !this.props.carName === nextProps.carName
} */
render() {
console.log('Child---render');
return (
<div className="child">
<h3>我是Child组件</h3>
<span>我接到的车是:{this.props.carName}</span>
</div>
)
}
}
render props
如何向组件内部动态传入带内容的结构(标签)?
类似于Vue中的插槽
Vue中:
React中:
- 使用children props: 通过组件标签体传入结构
- 使用render props: 通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性
children props
<A> <B>xxxx</B> </A>
使用
{this.props.children}
问题: 如果B组件需要A组件内的数据, ==> 做不到
render props
<A render={(data) => <C data={data}></C>}></A>
使用
A组件: `{this.props.render(内部state数据)}
C组件: 读取A组件传入的数据显示 `{this.props.data}
错误边界
理解
错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面
特点
只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式
getDerivedStateFromError配合componentDidCatch
// 生命周期函数,一旦后台组件报错,就会触发
static getDerivedStateFromError(error) {
console.log(error);
// 在render之前触发
// 返回新的state
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// 统计页面的错误。发送请求发送到后台去
console.log(error, info);
}
示例
import React, { Component } from 'react'
import Child from './Child'
export default class Parent extends Component {
state = {
hasError:'' // 用于标识子组件是否产生错误
}
//当Parent的子组件出现报错时候,会触发getDerivedStateFromError调用,并携带错误信息
static getDerivedStateFromError(error){
console.log('@@@',error);
return {hasError:error}
}
componentDidCatch(){
console.log('此处统计错误,反馈给服务器,用于通知编码人员进行bug的解决');
}
render() {
return (
<div>
<h2>我是Parent组件</h2>
{this.state.hasError ? <h2>当前网络不稳定,稍后再试</h2> : <Child/>}
</div>
)
}
}
组件通信方式总结
组件间的关系
- 父子组件
- 兄弟组件(非嵌套组件)
- 祖孙组件(跨级组件)
几种通信方式
- props: (1).children props (2).render props
- 消息订阅-发布: pubs-sub、event等等
- 集中式管理: redux、dva等等
- Context:
生产者-消费者模式
比较好的搭配方式
- 父子组件:props
- 兄弟组件:消息订阅-发布、集中式管理
- 祖孙组件(跨级组件):消息订阅-发布、集中式管理、Context(开发用的少,封装插件用的多)