React学习
介绍
1.React起源
React起源facebook内部项目,该公司对市场上的JavaScript MVC框架不满意,决定自己写一套,用于架设Instagram网站。做出来之后,发现很好用,就于2013年5月开源了
2.React与传统MVC关系
轻量级视图层库。A javascript library for building user interfaces
React是构建页面元素UI的库,可以简单理解为,React将页面分成各个独立的块,每一个块儿就是一个独立组件,各个组件之间可以进行组合、嵌套,就形成了我们的页面。
3.React的特性
- 声明式设计-React采用声明范式(只关心数据层改变,无需考虑网页如何渲染),轻松描述应用
- 高效-React通过对DOM的模拟(V-DOM),最大幅度的减少与DOM的交互
- 灵活-React可以与已知的库或者框架很好的配合
- JSX-JSX是javascript语法的拓展
- 组件- 通过react构建组件,使代码很容易得到复用,能很好的应用在大项目的开发中去
- 单向的响应数据流-React实现了单向响应的数据流,减少重复代码,比传统数据绑定更加简单
4.虚拟DOM
传统DOM更新场景下,每一个页面对应一个DOM树,每次需要更新页面的时候,需要手动操作DOM去更新。
虚拟DOM更新场景下,React把真实的DOM树转换成JavaScript对象树,也就是VDOM。通过DIFF算法,将虚拟DOM的change更新到真实DOM中去。
create-react-app
全局安装create-react-app
$ npm install -g create-react-app
创建一个项目
$ create-react-app your-app-name
如果不想全局安装,可以使用npx
$ npx create-react-app your-app-name
生成的项目目录结构如下:
|--README.md #使用方法文档
|--ndoe_modules #所有依赖安装的目录
|--package-lock.json #锁定安装时的包版本号,保证团队的依赖一致
|--package.json #
|--public #静态公共目录
|--src #开发用的源码代码目录
注意这一步会有依赖下载过慢的清况,可以切换到淘宝镜像,具体步骤如下:
- 执行 npm i -g nrm 下载nrm切换镜像工具
- 执行nrm ls 查看所有镜像源
- 执行nrm use taobao 切换到淘宝源
编写第一个react app
首先删除src目录下的所有文件
在src目录下新建一个index.js文件
在index文件中写入如下代码
//从react的包中引入React。 //只要你写react,这个包必须引入,因为react的jsx语法,需要React解析 import React from 'react' //ReacDOM可以帮助我们把React组件渲染到页面上去 import ReactDOM from 'react-dom' ReactDOM.render( //JSX语法标签 <h1>hello react</h1>, //确定要渲染的地方 document.getElementById("root") )
执行命令
npm start
,会打开浏览器,显示出hello react文字
JSX语法与组件
1.JSX语法
JSX将HTML语法直接加入到JavaScript代码中,再通过翻译器转换到纯JavaScript后由浏览器运行。在实际开发中,JSX在产品打包阶段都已经编译成纯JavaScript代码,不会带来任何副作用,编译的过程由Babel的JSX编译器实现
2.Class组件
ES6的加入让JavaScript直接支持使用Class来定义一个类,react创建组件的方式就是使用类的继承,ES6 class
是目前官方推荐的使用方式,它使用ES6的标准语法构建,看一下代码:
import React from 'react'
import ReactDOM from 'react-dom'
//创建一个组件,组件必须继承React.Component类
//注意:首字母大写
class App extends React.Component{
render(){
//这个节点不能有任何兄弟节点,必须是根节点
//如果要换行可以使用()包裹,成为一个整体,否则报错
return <h1>hello react component</h1>
}
}
ReactDOM.render(
<App/>,
document.getElementById("root")
)
3.函数式组件
import React from 'react'
import ReactDOM from 'react-dom'
//函数式组件,16.8之前函数式组件无法包含状态,所以使用类组件多;之后能够包含状态,推荐函数式组件
//1.和类组件一样,div必须是根节点
//2.和类组件一样函数名称必须大写
//3.和类组件一样JSX如果换行,必须要()包裹成一个整体
function App(){
return (
<div>
hello function component
</div>
)
}
ReactDOM.render(
<App/>,
document.getElementById("root")
)
组件的嵌套
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
//类组件
class Navibar extends Component {
render() {
return (
<div>
Navibar
</div>
)
}
}
//普通函数组件
function Swiper(){
return (
<div>Swiper</div>
)
}
//箭头函数组件
const Tabbar = ()=>{
return (
<div>
Tabbar
</div>
)
}
//箭头函数另一种写法
const Tabbar2 = ()=><div>Tabbar2</div>
//App组件,包含四个子组件
class App extends Component {
render() {
return (
<div>
<Navibar />
<Swiper />
<Tabbar />
<Tabbar2 />
</div>
)
}
}
ReactDOM.render(
<App/>,
document.getElementById("root")
)
4.组件样式
在react中{}
包裹的是表达式,页面展示的是里面表达式计算的值。
react注释写法优点特殊,就是js的注释,在用{}包裹起来
{//} 或者{/**/}
行内样式
想给虚拟DOM添加行内样式,需要使用表达式传入样式对象的方式来实现:
//注意:这里的两个花括号,外层表示我们要在JSX插入JS,内层表示一个js对象 //html里面font-size属性,在这类转成fontSize这种驼峰风格的,这个需要注意一下 <p style={{color:'red', fontSize:'14px'}}></p>
行内样式需要写入一个样式对象,这个样式对象的位置可以放在很多地方,例如
render
函数里,组件原型上,外链的js上使用Class
其实我们大多数情况还是在大量的为元素添加类名,需要注意的是,
class
需要专门写成className
(因为是写在js代码中的,class在js是关键字;同样的label标签的for属性也是一样)<p className="hello">hello react</p>
注意:
class ===》className
for===》htmlFor
react中比较推荐使用行内样式,因为react觉得每个组件是一个独立的整体
5.事件处理
5.1 绑定事件
采用on+事件名的方式来绑定事件。注意,这里和原生的事件是有区别的,原生的事件全是小写onclick
,React采取驼峰式写法onClick
,React事件不是原生事件,是合成事件
5.2 事件handler写法
- 直接在render中写行内的箭头函数(不推荐)
- 在组件内使用箭头函数定义一个方法(推荐)
- 直接在组件内定义一个非箭头函数的方法,然后再render里直接使用
onClick={this.handlerFuntioin.bind(this)}
(不推荐) - 直接在组件定义一个非箭头函数方法,然后再construct方法里
bind(this)
(推荐)
import React, { Component } from 'react'
export default class App extends Component {
a = 1000
render() {
return (
<div>
<input></input>
{//不推荐}
<button onClick={()=>{
console.log(1111111, this.a)
}}>add1</button>
{//不推荐}
<button onClick={this.Add2.bind(this)}>add2</button>
{//推荐}
<button onClick={this.Add3}>add3</button>
{//推荐这种方式}
<button onClick={()=>{
this.Add4()
}}>add4</button>
</div>
)
}
Add2(){
console.log(2222, this.a)
}
//箭头函数的this和App一致
Add3 = ()=>{
console.log(3333,this.a)
}
Add4(){
console.log(4444,this.a)
}
}
5.3 Event 对象
和浏览器一样,事件handler会被传入一个event
对象,这个对象和普通浏览器对象event
对象所包含的方法属性基本一致。不同的是,React中的event
对象不是浏览器提供的,而是它内部自己构建的,它同样具有event.stopPropagation
、event.preventDefault
这种常用的方法。
6.Ref的应用
6.1 给标签设置ref=”username”
通过this.refs.username, ref可以获得应用的真实dom,this.refs.username.value获取对象的值
6.2 给组件设置ref=”username”
通过this.refs.username, ref可以获得组件的对象
6.3 新的写法
myref = React.createRef()
<div ref={myref}>hello</div>
//访问this.myref.current获取到真实dom节点对象
组件数据挂载方式
1. 状态
状态就是组件描述某种显示的数据,由组件自己设置和更改,也就是说组件自己维护,使用状态的目的就是为了在不同状态下使组件的显示不同(自己设置的)。
定义state
import React, { Component } from 'react'
export default class App extends Component {
//state这个名称是固定的
//方法一:通过构造器构造state
constructor(){
super()
this.state={
showLabel:true
}
}
// 方法二:直接通过对象修改
// state ={
// showLabel:true
// }
render() {
return (
<div>
<h1>欢迎来到react世界1</h1>
<button >{this.state.showLabel?"收藏":"取消收藏"}</button>
</div>
)
}
}
修改state
import React, { Component } from 'react'
export default class App extends Component {
state ={
showLabel:true
}
render() {
return (
<div>
<h1>欢迎来到react世界1</h1>
<button onClick={()=>{
//state的修改只能通过setState函数修改,其他方式无效
this.setState({
showLabel: !this.state.showLabel
},()=>{
console.log("这个是回调函数,表明state同步完成了,渲染出了dom,后续如果有需要操作dom的可以在这里进行")
})
if(this.state.showLabel){
console.log("收藏逻辑")
}else{
console.log("取消收藏逻辑")
}
}}>{this.state.showLabel?"收藏":"取消收藏"}</button>
</div>
)
}
}
2. 属性(props)
props
是正常外部传入的,组件内部也可以通过一些方式来初始化设置,属性不能被组件自己更改,但是你可以通过父组件主动重新渲染的方式来传入新的props
。
属性是描述性质、特点的,组件自己不能随意更改。
之前的代码里有props
的简单使用,总而言之,在使用一个组件的时候,可以把参数放在标签属性中,所有的属性都会作为组件props
对象的键值。通过箭头函数创建的组件,需要通过函数的参数来接收props
:
1)在组件上通过key=valuex 写属性,通过this.props获取属性,这样组件的可复用性提高了。
2)注意在传参数时候,如果写成isShow=”true” 那么这是一个字符串。 如果写成isShow={true},这个世布尔值
3){…对象} 展开写法,
<Navibar title={obj.title} leftShow={obj.leftShow}>
和<Navibar {...obj}/>
等价4)默认属性值
//通过设置类属性设置属性的默认值 //第一种方式直接在组件外面使用类名.defaultProps方式 *.defaultProps = { name:"1231" } //或,类组件推荐这种方式,方法组件推荐上面的方式 //第二种方式,在组件内部使用 static defaultProps = { name:"1231" }
4)prop-types 属性验证
//通过设置类属性设置属性的验证 //属性从组件传入的时候需要对类型进行验证,默认的类型react已经提供好了 import PropTypes from 'prop-types' //类属性,在组外部使用 *.propTypes = { title: PropTypes.string, showLeft: PropTypes.bool, } //或,类组件推荐这种方式,方法组件推荐上面的方式 //类属性,在组件内部使用 static propTypes = { title: PropTypes.string, showLeft: PropTypes.bool, }
⚠️插槽:是属性的特殊属性。
作用:
**1.一定程度上可以减少父子通信 **
**2.复用(比如说轮播内容,第一个地方次轮播图片,第二个地方轮播视频,这时候就可以把轮播内容放到轮播组件插槽中,轮播组件只需要控制轮播方式而不关心DOM结构) **
import React, { Component } from 'react'
class Child extends Component{
render() {
return (
<div>
Child
{/* 插槽 */}
{/* 固定写法,含义是将组件内包裹的内容挂载到this.props.children属性上 */}
{this.props.children}
</div>
)
}
}
export default class App extends Component {
render() {
return (
<div>
App
<Child>
<div>1111</div>
</Child>
</div>
)
}
}
3. 属性VS状态
相似点:都是纯js对象,都会触发render更新,都具有确定性(状态/属性相同,结果相同)
不同点:
- 属性能从父组件获取,状态不能
- 属性可以由父组件修改,状态不能
- 属性能在内部设置默认值,状态也可以,设置方式不一样
- 属性不在组件内部修改,状态要在组件内部修改
- 属性能设置自组件初始值,状态不能
- 属性可以修改自组件的值,状态不能
state
的主要作用是用于组件保存、控制、修改自己的可变状态。state
在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。你可以认为state
是一个局部、只能被组件自身控制的数据源。state
中状态可以通过this.setState
方法进行更新,setState
会导致组件的重新渲染
props
主要作用是让使用该组件的父组件可以传入参数来配置该组件。它是外部传进来的配置参数,组件内部无法控制也无法修改。除非,外部组件主动传入新的props
,否则组件的props
永远不变。
没有state
的组件叫做无状态组件(stateless component),设置了state
的组件叫做有状态组件(stateful component)。因为状态回带来管理的复杂性,我们尽量多写无状态组件,尽量少些有状态的组件。这样回降低代码维护的难度,也会在一定程度上增强组件的可复用性。
4. 渲染数据
4.1 列表渲染
import React, { Component } from 'react'
export default class App extends Component {
constructor(){
super()
this.state = {
list:[{
id:1,
name:"1231"
},{
id:2,
name:"eee"
},{
id:3,
name:"rrr"
},]
}
}
render() {
return (
<div>
//通过js的方式渲染列表,key是唯一键,必须要加
{this.state.list.map(item=><li key={item.id}>{item.name}</li>)}
</div>
)
}
}
React的高效依赖于Virtual-DOM,尽量不操作DOM。对于列表元素来说会有一个问题:元素可能在一个列表中改变位置,值不变。要实现这个操作,只需要交换一下DOM位置就行了,但是React并不知道我们只是改变了元素位置,所以它会重新渲染元素(再执行Virtual-DOM),这样会大大增加DOM操作。但是,如果给每个元素加上标识,React就可以知道这两个元素只是交换了位置,这个标识就是key,这个key必须是唯一的标识
小技巧:
在列表渲染的时候,我们需要把要渲染的数据备份一份,然后每次操作副本不动原始数据,这样原始数据我们就不会修改,深拷贝推荐下面的方式:
let list=[1,2,3,4]
let newList=list.slice()//副本
4.2 条件渲染
//三目运算符
{this.state.dataList.length===0 ? <div>暂</div> : null}
//断路法
{this.state.dataList.length===0 && <div>暂</div>}
4.3 dangerouslySetInnerHtml
对于富文本创建的内容,后台拿到的数据是这样的:
content = "<p>Hello React</p>"
处于安全考虑,React当中所有的表达式都会被转义,如果直接输入,标签会被当成文本。这个时候就需要使用dangerouslySetInnerHtml
属性,它允许我们动态设置innerHTML
<div dangerouslySetInnerHTML={{
//__html固定,
__html: "Hello React
"
}}>
表单中受控组件与非受控组件
1. 非受控组件
React要编写一个非受控组件,可以使用ref
来从DOM节点中获取表单数据,就是非受控节点。
例如:下面代码使用非受控组件接受一个表单的值
import React, { Component } from 'react'
export default class App extends Component {
username = React.createRef()
render() {
return (
<div>
<h1>登录页</h1>
{/* 非受控的组件,value不受控制,需要修改成defaultValue */}
<input type='text' ref={this.username}
// value="默认值"
defaultValue={"默认值"} ></input>
<button onClick={()=>{
console.log(this.username.current.value)
}}>登录</button>
<button onClick={()=>{
this.username.current.value=""
}}>重置</button>
{/* 非受控组件在这里会有问题,只有第一次能传值,后续值不会改变,不会触发render更新 */}
{/* <ChileComponent myvalue={this.username.current.value}></ChileComponent> */}
</div>
)
}
}
因为非受控组件将真实数据存储在DOM节点中,所以在非受控组件中,有时候反而更容易集成React和非React代码。如果你不介意代码美观性,并希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
默认值的问题
在React渲染生命周期的时候,表单元素上的
value
将会覆盖DOM节点中的值,在非受控组件中,你经常希望React能赋予组件一个初始值,但是不去控制后续的更新。在这种情况下,你可以指定一个defaultValue
属性,而不是value
属性。
2. 受控组件
在HTML中,表单元素(如input、textarea和select)通常自己维护state
,并根据用户输入进行更新。而在React中,可变状态(mutable state)通常保存在组件state属性中,并且只能通过使用setState
来更新。
我们可以把两者结合起来,使React的state成为唯一的数据源,渲染表单的React组件还控制着用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做受控组件。
下面就是一个受控组件的案例:
import React, { Component } from 'react'
export default class App extends Component {
state={
username:"react"
}
render() {
return (
<div>
<h1>登录页</h1>
{/* input是一个受控组件 */}
<input type='text'
value={this.state.username}
onChange={(event)=>{
// console.log(event.target.value)
this.setState({
username: event.target.value
})
}}></input>
<button onClick={()=>{
console.log(this.state.username)
}}>登录</button>
<button onClick={()=>{
this.setState({
username:''
})
}}>重置</button>
{/* 方便传递状态给子组件 */}
<ChileComponent myvalue={this.username.current.value}></ChileComponent>
</div>
)
}
}
注意:
广义范围内,React组件的数据渲染是否被调用者传递的
props
完全控制,控制则为受控组件,否则为非受控组件。
组件通信方式
1. 父子组件通信
1.1 传递数据(父传子)和传递回调方法(子传父)
import React, { Component } from 'react'
class Navibar extends Component {
render() {
return (
<div style={{background:"red"}}>
<button onClick={()=>{
// 获取自定义的属性
//这个获取的是一个函数,如果需要执行,需要在后面加上()
let cc = this.props.cc
console.log("点击之后通知父组件App,让App改变isShow的状态,从而改变Sidebar组件的显隐",cc )
//子传父,通过父组件属性传递过来的回调函数调用
cc()//执行父组件传过来的函数
}}>子组件click</button>
<span>Navibar</span>
</div>
)
}
}
class Sidebar extends Component {
render() {
return (
<div style={{background:"yellow", width:"200px"}}>
<ul>
<li>11111</li>
<li>11111</li>
<li>11111</li>
<li>11111</li>
<li>11111</li>
<li>11111</li>
<li>11111</li>
</ul>
</div>
)
}
}
export default class App extends Component {
state={
isShow: true
}
render() {
return (
<div>
{/* 自定义一个属性,但是不执行,执行时机是子组件自己决定的 */}
{/* 父传子,属性传递 */}
<Navibar cc={()=>{
console.log("父组件中自定义一个属性,传入一段函数定义")
this.changeSideBar()
}}/>
{/* <button onClick={()=>{
this.changeSideBar()
}} >父组件click</button> */}
{this.state.isShow && <Sidebar />}
</div>
)
}
changeSideBar = ()=>{
this.setState({
isShow: !this.state.isShow
})
}
}
1.2 ref标记(父组件拿到子组件的引用,从而调用子组件的方法)
在父组件中清楚子组件的input输入框的value值。this.refs.form.reset()
2. 非父子组件通信方式
2.1 状态提升(中间人模式)
React中状态提升意思是,多个组件需要共享的状态提升到它最近的父组件上,在父组件上改变这个状态,然后在通过props分发给子组件
2.2 发布订阅模式
import React, { Component } from 'react'
export default class App extends Component {
render() {
return (
<div>App</div>
)
}
}
//处理中心
var bus = {
list:[],
//订阅
subscribe(callback){
console.log(callback)
this.list.push(callback)
},
//发布
publish(text){
//遍历所有的list,执行回调函数
console.log(this.list)
this.list.forEach(callback=>{
callback && callback(text)
})
},
}
//订阅
bus.subscribe((value)=>{
console.log("第一次订阅", value)
})
bus.subscribe((value)=>{
console.log("第二次订阅", value)
})
//发布
bus.publish("发布的时候传的形参")
2.3 context状态树传参
import React, { Component } from 'react'
//1.先定义全局的context对象
const GlobalContext = React.createContext()
//2.设置生产者
export default class App extends Component {
state={
name:"张三"
}
render() {
return (
//包裹根节点
<GlobalContext.Provider value={{
call: "打电话",
phone: "手机号",
name: this.state.name,
//只能通过这种方式让父组件更新,这样才能重新渲染,重新执行render函数
//在消费组件内直接修改,不能重新渲染,不能出发页面更新
changeName:(value)=>{
this.setState({
name: value
})
},
}}>
<div>
App
<Child></Child>
<Child2></Child2>
</div>
</GlobalContext.Provider>
)
}
}
//3.设置消费者
class Child extends Component{
render() {
return (
<GlobalContext.Consumer>
{
(value)=>{
return (
//这里才是根节点内容
<div style={{
background:"yellow"
}}>
Child组件1
<button onClick={()=>{
value.changeName("李四")
}}>改变name值为李四</button>
<p>{value.name}</p>
<p>{value.phone}</p>
<p>{value.call}</p>
</div>
)
}
}
</GlobalContext.Consumer>
)
}
}
class Child2 extends Component{
render() {
return (
<GlobalContext.Consumer>
{
(value)=>{
return (
//这里才是根节点内容
<div style={{
background:"red"
}}>
Child组件2
<p>{value.name}</p>
<p>{value.phone}</p>
<p>{value.call}</p>
</div>
)
}
}
</GlobalContext.Consumer>
)
}
}
React生命周期
1. 初始化阶段
componentWillMount:render之前最后一次修改状态的机会 新版本已废弃,直接替换为使用consructor
render:只能访问this.props和this.state,不允许修改状态和DOM输出
componentDidMount:成功render并渲染完成真实DOM之后触发,可以修改DOM
componentDidMount(){ console.log("mount") //数据请求axios //订阅函数的调用 // setInterval // 基于创建完的DOM进行一些初始化,比如说一些初始化类库之类的,例如:BetterScroll库 }
2. 运行中阶段
componentWillReceiveProps:父组件修改属性的时候触发 新版本已废弃
shouldComponentUpdate:返回false会阻止render调用
//优化组件更新操作 shouldComponentUpdate(nextProps, nextState) { //return true 应该更新 //return false 阻止更新 // this.state 老状态 // nextState 新状态 if(JSON.stringify(this.state) !== JSON.stringify(nextState)){ return true } return false }
componentWillUpdate:不能修改属性和状态 新版本已废弃
render:只能访问this.props和this.state,不允许修改状态和DOM输出
componentDidUpdate:能访问之前的props和state,可以修改DOM
componentDidUpdate(preProps, preState){ //preProps 老的属性 //preState 老的状态 }
3. 销毁阶段
- componentWillUnmount:在删除组件之前进行清理操作,比如计时器和事件监听
老生命周期的问题
- componentWillMount,在ssr中这个方法会被多次调用,所以会重复触发多次,同时在这里如果绑定事件,将无法解绑,导致内存泄漏,变得不够安全和高效,从而逐步废弃
- componentWillReceiveProps,外部组件多次频繁更新传入多次不同的props,会导致不必要的异步请求
- componentWillUpdate,更新前记录DOM状态,可能会做一些处理,与componentDidUpdate相隔时间过长,会导致状态不太可信
新生命周期的替代
- getDerivedStateFromProps,第一次组件初始化,以及后续组件更新过程中(包括自身状态更新以及父传子),返回一个对象作为新的state,返回null说明不需要在这里更新state
- getSnapshotBeforeUpdate,取代了componentWillUpdate,触发事件为update发生的时候,在render之后,DOM渲染之前返回一个值,作为componentDidUpdate的第三个参数。
4. react中性能优化
4.1 shouldComponentUpdate
控制组件自身或者子组件是否需要更新,尤其在子组件非常多的情况下,需要进行优化
4.2 PureComponent
PureComponent会帮你比较新props和旧的props,新的state和旧的state(值相等、或者对象含有相同的属性、且属性值相等),决定shouldComponentUpdate返回true或者false,从而决定是否要呼叫render函数。
注意⚠️:
如果你的state或者props永远都在变,那么PureComponent并不会更快,因为shallowEqual需要花时间。
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
render() {
console.log("render")
return (
<div>App</div>
)
}
}
React Hooks
1. 使用Hooks的理由
- 高阶组件为了复用,导致代码层级复杂
- 生命周期复杂
- 写成function组件,无状态组件,因为需要状态,又改成了class,成本高
2. useState(保存组件状态)
const [state, setState] = useState(initialState)
一个小demo:
import React, { useState } from 'react'
export default function App() {
var obj = useState("zzz")
//是一个数组,第一个参数是初始化的值,第二个参数是一个函数
console.log(obj)
//这个写法就是es6数组解构赋值,和上面等价
const [name, setName] = useState("zzz")
const [age, setAge] = useState(28)
return (
<div>
<button onClick={()=>{
// console.log(name)
setName("修改的新name值")
setAge(18)
}}>click</button>
App-{name}-{age}
</div>
)
}
3. useEffect(处理副作用)和useLayoutEffect(同步执行副作用)
Function Component不存在生命周期,所以不要把Class Component的生命周期搬过来对号入坐
useEffect(()=>{
//effect
return ()=>{
//cleanup
}
},[依赖的状态;空数组,表示不依赖])
不要对Dependencies撒谎,如果你明确使用了某个变量,却没有声明在依赖中,你等于向React撒谎了。后果就是,当依赖的变量改变,useEffect不会再次执行,eslint也会警告
有依赖的demo:
import React, { useEffect, useState } from 'react'
export default function App() {
const [name, setName] = useState("asda")
//这里使用了name,那么依赖必须要传,不然只有第一次首字母大写,后续修改name值都不会变
useEffect(()=>{
//首字母大写
var newName = name.substring(0,1).toUpperCase()+name.substring(1)
setName(newName)
}, [name])
return (
<div>
<button onClick={()=>{
setName("zzzzada")
}}>click</button>
App-{name}
</div>
)
}
没有依赖的demo:
import React, { useEffect, useState } from 'react'
export default function App() {
const [isCreated, setIsCreated] = useState(true)
return (
<div>
App
<button onClick={()=>{
setIsCreated(false)
}}>点击销毁子组件</button>
{isCreated}
{isCreated && <Child />}
</div>
)
}
function Child() {
useEffect(() => {
//监听resize事件
window.onresize = ()=>{
console.log("resize")
}
//启动定时器
var timer = setInterval(()=>{
console.log("1111")
}, 1000)
return () => {
console.log("组件销毁的时候执行")
//为什么要这么处理,因为react在组件销毁的时候绑定在组件上的事件,react并不会为我们清楚,所以需要自己手动清理
//去掉监听
window.onresize=""
//清楚定时器
clearInterval(timer)
}
}, [])
return (
<div>
child
</div>
)
}
useEffect和useLayoutEffect有啥区别?
简单来说就是调用时机不同,useLayoutEffect
和原来的componentDidMount和componentDidUpdate
一致,在React完成DOM更新之后马上同步调用代码,会阻塞页面渲染。而useEffect
是会在整个页面渲染完才会调用代码。
官方建议优先使用useEffect。
在实际使用中如果想避免页面抖动(在useEffect中修改DOM很可能出现)的话,可以把需要操作DOM的代码放在useLayoutEffect里。在这里做DOM操作,这些DOM修改会和React做出的更改一起被一次性渲染到屏幕上,只有一次回流、重绘的代价。
4. useCallBack(记忆函数)
防止因为组件重新渲染,导致方法重新创建,起到缓存的作用;只有第二个参数变化了,才重新声明一次
var handleClick = useCallBack(()=>{
console.log(name)
}, [name])
<buttton onClick={()=>handleClick()}> hello </button>
//只有name改变后,这个函数才会重新声明一次
//如果传入空数组,那么就是第一次创建就被缓存,如果name后期变化了,拿到的还是老的name
//如果不传第二个参数,每次都会重新声明一次,拿到的就是最新的name
5. useMemo(记忆组件)
useCallBack的功能完全可以用useMemo取代,如果你想通过useMemo返回一个记忆函数也是完全可以的。
useCallBack(fn, inputs) 等价于 useMemo( ()=>fn, inputs)
唯一的区别是:useCallBack不会执行第一个参数函数,只是将它返回给你,而useMemo会执行第一个函数,并将结果返回给你。
所以,useCallBack常用与记忆事件函数,生产记忆后的事件函数并传递给子组件使用。而useMemo更适合经过函数计算的到一个确认的值,比如记忆组件,理解为计算属性。
6. useRef(保存引用值)
const myswiper = useRef(null)
<Swiper ref={myswiper} />
7. useReducer和useContext(减少组件层级)
import React, { useContext, useState } from 'react'
//1.先定义全局的context对象
const GlobalContext = React.createContext()
//2.设置生产者
export default function App(){
const [name, setName] = useState("张三")
return (
//包裹根节点
<GlobalContext.Provider value={{
call: "打电话",
phone: "手机号",
name: name,
//只能通过这种方式让父组件更新,这样才能重新渲染,重新执行render函数
//在消费组件内直接修改,不能重新渲染,不能出发页面更新
changeName:(value)=>{
setName(value)
},
}}>
<div>
App
<Child></Child>
<Child2></Child2>
</div>
</GlobalContext.Provider>
)
}
//3.设置消费者
function Child(){
//class组件的写法
return (
//直接使用标签版本
<GlobalContext.Consumer>
{
(value)=>{
return (
//这里才是根节点内容
<div style={{
background:"yellow"
}}>
Child组件1
<button onClick={()=>{
value.changeName("李四")
}}>改变name值为李四</button>
<p>{value.name}</p>
<p>{value.phone}</p>
<p>{value.call}</p>
</div>
)
}
}
</GlobalContext.Consumer>
)
}
//useContext简化代码
function Child2(){
//hooks写法,大大减化代码
const context = useContext(GlobalContext)
console.log("context", context)
return (
//这里才是根节点内容
<div style={{
background:"red"
}}>
Child组件2
<p>{context.name}</p>
<p>{context.phone}</p>
<p>{context.call}</p>
</div>
)
}
userReducer将组件状态转移到外部,使得组件无状态
import React,{useReducer} from 'react'
//useReducer处理函数
const reducer = (preState, action)=>{
console.log("reducer...", preState, action)
//这里是一个技巧,操作状态不应该对原状态操作,需要深拷贝一份,对新状态操作,这样不会污染原来状态
let newState = {...preState}
switch(action.type){
case "value-minus":
newState.count--
return newState
case "value-add":
newState.count++
return newState
default:
return preState
}
}
//useReducer外部的状态
const initialState={
count: 0
}
export default function App() {
// 这样做的好处就是将状态转移到外部,组件变成无状态了,类似后端开发redis作用
// const [state, dispatch] = useReducer(first, second, third)
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<button onClick={()=>{
dispatch({
//type固定不可变,对应的值可以修改
type:"value-minus"
})
}}>-</button>
{state.count}
<button onClick={()=>{
dispatch({
//type固定不可变,对应的值可以修改
type:"value-add"
})
}}>+</button>
</div>
)
}
案例:点击Child1组件的按钮,能够分别修改Child2和Child3组件的内容
import React,{useReducer, useContext} from 'react'
//将状态转移到外部
const initialValue = {
text1:"aaaa",
text2:"bbbb"
}
//定义状态发生改变的回调函数,也就是具体修改状态的地方就在这
const reducer = (preState, action)=>{
const newState = {...preState}
switch(action.type){
case "click2":
newState.text1 = action.value
return newState
case "click3":
newState.text2 = action.value
return newState
default:
return preState
}
}
//帮助在子节点同步状态
const GlobalContext = React.createContext()
export default function App() {
const [state, dispatch] = useReducer(reducer, initialValue)
return (
//父组件将状态传递到子组件
<GlobalContext.Provider value={{
state,
dispatch
}}>
<div>
APP
<Child1></Child1>
<Child2></Child2>
<Child3></Child3>
</div>
</GlobalContext.Provider>
)
}
function Child1(){
//子组件拿到状态,这里是解构赋值的方式,只用了一个,所以只写一个
//完整的是 const {state, dispatch} = useContext(GlobalContext)
const {dispatch} = useContext(GlobalContext)
return (
<div>
<button onClick={()=>{
dispatch({
type:"click2",
value:"点击了click2"
})
}}>click2</button>
Child1
<button onClick={()=>{
dispatch({
type:"click3",
value:"点击了click3"
})
}}>click3</button>
</div>
)
}
function Child2(){
//子组件拿到状态,这里是解构赋值的方式,只用了一个,所以只写一个
//完整的是 const {state, dispatch} = useContext(GlobalContext)
const {state} = useContext(GlobalContext)
console.log(state)
return (
<div>
Child2-{state.text1}
</div>
)
}
function Child3(){
//子组件拿到状态,这里是解构赋值的方式,只用了一个,所以只写一个
//完整的是 const {state, dispatch} = useContext(GlobalContext)
const {state} = useContext(GlobalContext)
return (
<div>
Child3-{state.text2}
</div>
)
}
8. 自定义hooks(相当于抽出公共组件)
当我们想在两个函数之间共享逻辑的时候,我们会把它提到第三个函数中。
必须以“use”开头吗?必须如此。这个约定非常重要,不遵循的话,由于无法判断某个函数是否包含其对内部的Hook的调用,React将无法检查你的Hook是否违反了Hook的规则
import React, { useEffect } from 'react'
import axios from 'axios'
//抽出到自定义hooks
function useGetListFromServce(){
const [list, setList] = useState([])
useEffect(() => {
axios.get("xxxx").then(res=>{
setList(res.data)
})
return () => {
list
}
}, [])
}
export default function App() {
//原始逻辑
// const [list, setList] = useState([])
// const getListFromServce = ()=>{
// axios.get("xxxx").then(res=>{
// setList(res.data)
// })
// }
//从自定义hooks拿出list
const {list} = useGetListFromServce()
return (
<div>App
{
list.length
}
</div>
)
}
React路由
1. 什么是路由
路由是根据不同的url地址展示不同的内容或者页面
一个针对React设计的路由解决方案,可以友好的帮你解决React Components 到URL之间的同步映射关系
2. 路由安装
npm install react-router-dom@5
3. 路由使用
3.1 路由方法导入
import {HashRouter,Route} from 'react-router-dom'
3.2 定义路由以及重定向
<HashRouter>
{/* switch只陪到到找到的第一个,然后就退出。
刷新之后如果匹配到,立即退出,就不会Redirect,否则继续跳到Redirect */}
{/* 类似switch case语句 */}
<Switch>
<Route path="/films" component={Films}></Route>
<Route path="/cinemas" component={Cinemas}></Route>
<Route path="/center" component={Center}></Route>
{/* 模糊匹配,所以每次刷新都会匹配到Redirect这里 */}
{/* <Redirect from="/" to="/films" /> */}
{/* 精确匹配 */}
<Redirect from="/" to="/films" exact />
{/* 放到想要做的效果就是上面都不匹配,跳到这个组件,展示404 */}
{/* 但是实际效果来说,会被上面的Redirect拦截掉,
因为默认的是模糊匹配,因此需要让Redirect变成精准匹配,只需要在Redirect加上exact属性即可 */}
<Route component={NotFound} />
</Switch>
</HashRouter>
注意⚠️:
默认模糊匹配 - 加上exact精准匹配
- Warning :Hash history cannot PUSH the same path;a new entry will not be added to the history stack,这个警告只有hash模式会出现
3.3 嵌套路由
<Switch>
<Route path="/films/nowplay" component={Nowplaying}></Route>
<Route path="/films/commingsoon" component={CommingSoon}></Route>
<Redirect from="/films" to="/films/nowplay" ></Redirect>
</Switch>
3.4 路由跳转方式
3.4.1 声明式导航
<NavLink to="/films" activeClassName="zactiv">电影</NavLink>
3.4.2 编程式导航
//方式一
window.location.href="#/detail/"+id
//方式二
//方法组件中, `/detail/${id}` 字符串模版
props.history.push(`/detail/${id}`)
3.5 路由传参
//一、query传参数
<Route paht="/detail" component={Detail} ></Route>
//传参数
props.history.push({pathname:'/detail', query:{id:1}})
//取参数
props.location.query.id
//二、动态路由传参, 推荐这种
<Route paht="/detail/:myid" component={Detail} ></Route>
//传参数
props.history.push(`/detail/${id}`)
//取参数
props.match.params.myid
3.6 路由拦截
//加个redirect是为了保持地址栏和组件一致
<Route path="/center" render={ (props)=> isAuth()?<Center {...props} />: <Redirect to="/login" /> } />
<Route path="/login" component={Login}></Route>
反向代理
npm i http-proxy-middleware
安装代理插件在src目录下新建setupProxy.js
在setupProxy.js复制如下代码
//这段代码含义, //用户发送一个以/api开头的请求,node服务器会把这个请求拦截下来, //并由node服务器去发送请求,然后把数据给我们的代码 //这样做是为了绕过,浏览器的跨域限制,由服务器去请求到数据在给到我们自己写的代码 const {createProxyMiddleware} = require('http-proxy-middleware') module.exports = function(app){ app.use( '/api', //以api开头的接口 createProxyMiddleware({ target:'http://www.newbaiduy.com', //目标请求的域名 changeOrigin: true, }) ) }
Flux与Redux
Redux主要是用于应用状态管理。简而言之,Redux用一个单独的常量状态树(state对象)保存整个应用的状态,这个对象不能被直接改变。当一些数据发生变化了,一个新的对象就会被创建(使用action和reducer),这样就可以进行数据追踪,实现时光旅行。
1. redux介绍及设计和使用的三大原则
- state 以单一对象存储在store对象中
- state 只读(每次都返回一个新对象)
- 使用纯函数reducer执行state更新
2. redux工作流
React-redux
1. 介绍
2. 容器组件与UI组件
2.1 UI组件
- 只负责UI的呈现,不带有任何业务
- 没有状态(即不使用this.state这个变量)
- 所有数据都有this.props提供
- 不使用任何redux的api
2.2 容器组件
- 负责管理数据和业务逻辑,不负责UI的呈现
- 带有内部状态
- 使用Redux的API
2.3 Provider与connect
2.4 HOC与context通信在react-redux底层中的应用
- connect 是HOC,高阶组件
- Provider组件,可以让容器组件拿到state,使用了context
2.5 高阶组件构建与应用
HOC不仅仅是一个方法,确切说应该是一个组件工厂,获取低阶组件,生产高阶组件。
- 代码复用,代码模块化
- 增删查改props
- 渲染劫持
import React from 'react'
function NotFound() {
return (
<div>404 not found</div>
)
}
function myHOC(func, obj) {
//执行传入的函数
var value = func()
return (MyComponent)=>{ //这里返回一个函数,入参是一个组件,对应下面的 myHOC(...)(NotFound) 这一部分
//执行上面定义的函数,返回一个组件
return (props)=>{
//劫持原组件,然后渲染一下
return (<div style={{background: "red"}}>
{/* 将value, 之前的props和obj绑定到新组件的props中去 */}
<MyComponent {...value} {...props} {...obj} ></MyComponent>
</div>)//括号包裹的就是组件内容
}
}
}
//自定义的高阶组件
export default myHOC(()=>{
return {
a:1,
b:2
}
}, {
f1(){},
f2(){},
})(NotFound)
2.6 Redux持久化
redux-persis
UI组件库
Ant Design是一个致力于提升【用户】和【设计者】使用体验的预言;旨在统一中台项目中的前端UI设计,屏蔽不必要的设计差异和实现成本,解放设计和前端的的研发资源;包含很多设计原则和配套的组件库。
1. ant-design(PC端)
2. antd-mobile(移动端)
Immutable
var obj = { /* 一个十分复杂的对象 */ }
doSomething(obj)
//上面的函数执行完之后,此时的obj还是原来的obj吗?如果函数内部发生deepCopy那么就不是,否则还是原来的地址
//deepCopy?
1. Immutable.js介绍
每次修改一个immutable对象时都会创建一个新的不可变对象,在新对象的操作并不会影响原始对象的数据。
2. 深拷贝与浅拷贝
- var arr = {}; var arr2 = arr; 浅拷贝
- Object.assign()只是一级属性复制,比浅拷贝多拷贝了一层而已
- const obj1 = JSON.parse(JSON.stringfy(obj));数组对象都好用(缺点:不能有undefined)
3. immutable优化性能的方式
immutable实现的原理是Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时,为了避免deepCopy把所有节点都复制一遍带来的性能损耗,immutable使用了Structural Sharing(结构共享)。即,对象树的一个节点发生变化,只修改这个节点和它影响的父节点,其他节点共享。
4. Immutable中常用的类型(Map, List)
Map
import {Map} from 'immutable' const m1 = Map({a:1,b:2,c:3}) const m2 = map1.set('b',50) m1.get('b')+" vs "+m2.get('b'); //2 vs 50
List
import {List} from 'immutable' const list = List([1,2,3]) const list2 = list1.push(3,4,5) //添加 const list3 = list2.unshift(0) //移除 list2.toJs() //123345 list3.toJs() // 23345