react学习

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的特性

  1. 声明式设计-React采用声明范式(只关心数据层改变,无需考虑网页如何渲染),轻松描述应用
  2. 高效-React通过对DOM的模拟(V-DOM),最大幅度的减少与DOM的交互
  3. 灵活-React可以与已知的库或者框架很好的配合
  4. JSX-JSX是javascript语法的拓展
  5. 组件- 通过react构建组件,使代码很容易得到复用,能很好的应用在大项目的开发中去
  6. 单向的响应数据流-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 #开发用的源码代码目录

注意这一步会有依赖下载过慢的清况,可以切换到淘宝镜像,具体步骤如下:

  1. 执行 npm i -g nrm 下载nrm切换镜像工具
  2. 执行nrm ls 查看所有镜像源
  3. 执行nrm use taobao 切换到淘宝源

编写第一个react app

  1. 首先删除src目录下的所有文件

  2. 在src目录下新建一个index.js文件

  3. 在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")
    )
    
  4. 执行命令npm start,会打开浏览器,显示出hello react文字

JSX语法与组件

1.JSX语法

JSX将HTML语法直接加入到JavaScript代码中,再通过翻译器转换到纯JavaScript后由浏览器运行。在实际开发中,JSX在产品打包阶段都已经编译成纯JavaScript代码,不会带来任何副作用,编译的过程由Babel的JSX编译器实现

Https://reactjs.org/docs/hello-world.html

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采取驼峰式写法onClickReact事件不是原生事件,是合成事件

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.stopPropagationevent.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更新,都具有确定性(状态/属性相同,结果相同)

不同点:

  1. 属性能从父组件获取,状态不能
  2. 属性可以由父组件修改,状态不能
  3. 属性能在内部设置默认值,状态也可以,设置方式不一样
  4. 属性不在组件内部修改,状态要在组件内部修改
  5. 属性能设置自组件初始值,状态不能
  6. 属性可以修改自组件的值,状态不能

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:在删除组件之前进行清理操作,比如计时器和事件监听

老生命周期的问题

  1. componentWillMount,在ssr中这个方法会被多次调用,所以会重复触发多次,同时在这里如果绑定事件,将无法解绑,导致内存泄漏,变得不够安全和高效,从而逐步废弃
  2. componentWillReceiveProps,外部组件多次频繁更新传入多次不同的props,会导致不必要的异步请求
  3. componentWillUpdate,更新前记录DOM状态,可能会做一些处理,与componentDidUpdate相隔时间过长,会导致状态不太可信

新生命周期的替代

  1. getDerivedStateFromProps,第一次组件初始化,以及后续组件更新过程中(包括自身状态更新以及父传子),返回一个对象作为新的state,返回null说明不需要在这里更新state
  2. 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的理由

  1. 高阶组件为了复用,导致代码层级复杂
  2. 生命周期复杂
  3. 写成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>

注意⚠️:

  1. 默认模糊匹配
  2. 加上exact精准匹配
  3. 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>

反向代理

  1. npm i http-proxy-middleware安装代理插件

  2. 在src目录下新建setupProxy.js

  3. 在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工作流

redux工作流

原图地址:https://redux.js.org/tutorials/essentials/part-1-overview-concepts#:~:text=on%20the%20screen-,Here%27s,-what%20that%20data

React-redux

1. 介绍

https://github.com/reactjs/react-redux

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底层中的应用

  1. connect 是HOC,高阶组件
  2. Provider组件,可以让容器组件拿到state,使用了context

2.5 高阶组件构建与应用

HOC不仅仅是一个方法,确切说应该是一个组件工厂,获取低阶组件,生产高阶组件。

  1. 代码复用,代码模块化
  2. 增删查改props
  3. 渲染劫持
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

https://github.com/rt2zz/redux-persist

UI组件库

Ant Design是一个致力于提升【用户】和【设计者】使用体验的预言;旨在统一中台项目中的前端UI设计,屏蔽不必要的设计差异和实现成本,解放设计和前端的的研发资源;包含很多设计原则和配套的组件库。

1. ant-design(PC端)

https://ant.design/index-cn

2. antd-mobile(移动端)

https://mobile.ant.design

Immutable

var obj = { /* 一个十分复杂的对象 */ }
doSomething(obj)
//上面的函数执行完之后,此时的obj还是原来的obj吗?如果函数内部发生deepCopy那么就不是,否则还是原来的地址

//deepCopy?

1. Immutable.js介绍

地址: https://github.com/immutable-js/immutable-js

每次修改一个immutable对象时都会创建一个新的不可变对象,在新对象的操作并不会影响原始对象的数据。

2. 深拷贝与浅拷贝

  1. var arr = {}; var arr2 = arr; 浅拷贝
  2. Object.assign()只是一级属性复制,比浅拷贝多拷贝了一层而已
  3. 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