React笔记

403 阅读28分钟

React 笔记

react的开发依赖

  • react:包含react所必须的核心代码
  • react-dom:react渲染在不同平台所需要的核心代码
  • babel: 将jsx转化为react代码的工具

在script标签中编写react代码

<script type='text/babel'>
    let message = 'hello world'
    ReactDom.render(<h2>{message}</h2>, document.getElementById('#app'))
</script>

类封装组件

// 组件名称必须是大写,小写会被认为是HTML元素
class App extends React.Component {
    constructor() {
        super()
        this.state = {
            message: 'hello world',
            movies: [1, 2, 3]
        }
    }
    render() {
        {/* jsx的 */}
        return (
            <h2>{this.state.message}</h2>
            <ul>
                {
                    this.state.movies.map((item, index) => {
                        return (<li>{item}</li>)
                    })
                }
            </ul>
        )
    }
}
ReactDom.render(<App />, document.getElementById('#app'))

认识jsx

  • jsx是一种JavaScript的语法扩展(extension),也在很多地方称之为JavaScript XML
  • 用于描述我们的UI界面,并且jsx完全可以和JavaScript融合在一起使用

jsx的书写规范:

  • jsx的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素
  • jsx中的标签可以是单标签,也可以是双标签,但是如果是单标签,则必须以 /> 结尾

jsx的使用

  • 当变量是number,string,array类型时,可以直接显示内容
  • 当变量是null,undefined,boolean类型时,内容为空。如果希望在页面中显示,可以将这些数据转化为字符串
  • jsx可以嵌入js表达式

jsx绑定属性

{/* 绑定普通属性 */}
<h2 title={title}></h2>
{/* 绑定class, 注意这里的class要用className, label中的for要用htmlFor */}
<div className={'box title ' + (active ? 'active' : '')}></div>
{/* 绑定style */}
<div style={{color: 'red', fontSize: '50px'}}></div>

jsx绑定事件

我们定义的回调函数,react不会帮助我们绑定this,所以是undefined

constructor() {
    this.btnClick = this.btnClick.bind(this)
}
render() {
    return (
        <div>
            {/* 方案一 */}
            <button onClick={this.btnClick}>按钮1</button>
            {/* 方案二: 定义函数时, 使用箭头函数 */}
            <button onClick={this.increment}>+1</button>
            {/* 方案三(推荐): 直接传入一个箭头函数, 在箭头函数中调用需要执行的函数*/}
            <button onClick={(e) => { this.decrement("why") }}>-1</button>
        </div>
    )
}
btnClick() {
    console.log(this.state.message);
}
increment = () => {
    console.log(this.state.counter);
}
decrement(name) {
    console.log(this.state.counter, name);
}

条件渲染

render() {
    const { isLogin } = this.state
    // 方案一 通过if判断
    let welcome = null
    let btnText = null
    if (isLogin) {
        welcome = <h2>欢迎回来</h2>
        btnText = '退出'
    } else {
        welcome = <h2>请先登录</h2>
        btnText = '登录'
    }
    return (
        <div>
            { welcome }
            <button>{btnText}</button>
            {/* 2.方案二: 三元运算符 */}
            <button>{ isLogin ? '退出' : '登录' }</button>
            {/* 2.方案三: 逻辑与&& */}
            { isLogin && <h2>你好</h2> }
        </div>
    )
}

jsx的本质

jsx仅仅只是React.createElement(component, props, ...children) 函数的语法糖

所有的jsx最终都会被转换成React.createElement的函数调用

createElement需要传递三个参数:

  • 参数一type: 如果是标签元素,直接使用标签名的字符串,如果是组件元素,就直接使用组件的名称
  • 参数二config:所有jsx中的属性都在config中以对象的属性和值的形式存储
  • 参数三children:存放标签中的内容,以children数组的方式进行存储

React.createElement最终会创建一个ReactElement对象

  • React利用ReactElement对象组成了一个JavaScript的对象树
  • JavaScript对象树就是虚拟DOM(Virtual DOM)

为什么要使用虚拟DOM,而不是直接修改真实DOM?

  • 原有的开发模式很难跟踪到状态发生的改变,不方便我们对应用程序进行调试
  • 操作真实DOM的性能较低,而且通过document.createElement创建出来的对象是非常复杂的,并且操作DOM会引起浏览器的回流和重绘。

React的脚手架

  • React脚手架本身需要依赖node,所以需要安装node环境,在node官网下载,然后在cmd中通过 node--version 查看node版本
  • 通过npm安装yarn npm install yarn -g
  • 安装脚手架 , npm install create-react-app -g

创建React项目

create-react-app 项目名称(注意项目名称不能包含大写字母)

脚手架中的webpack

如果我们希望看到webpack的配置信息

  • 可以执行package.json文件中的一个脚本: "eject": "react-scripts eject"
  • 在命令行里 yarn eject 这个操作是不可逆的

重新搭建在脚手架创建出来项目的文件结构

// 先将src下的文件都删除
// 将public文件下除了favicon.ico 和 index.html之外的文件都删除掉
// 在src目录下创建一个index.js文件,因为这是webpack打包的入口
// index.js 文件
import React from 'react'
import ReactDOM from 'react-dom'import App from 'App.js'ReactDOM.render(<App />, document.getElementById('root'))
// 如果不希望在ReactDOM.render中编写过多的代码,可以单独抽取一个组件APP.js
// App.js 文件
import React, { Component } from 'react'export default class App extends Component {
    render() {
        return <h2>Hello React</h2>
    }
}

组件化

类组件

// 组件的名称必须是大写字母开头,如果是小写会被当成普通的html标签
// 类组件需要继承自React.Component
// 类组件必须实现render函数export default class App extends React.Component {
    constructor() {  // 可选的,通常在constructor中初始化数据,记得调用super()
        this.state = {}  // 保存组件的数据
    }
    render() {  // render方法是class组件中唯一必须实现的方法
        return [
            <div></div>
            <div></div>
            <div></div>
        ]
    }
    
}
​
// 函数组件
// 函数组件有自己的特点(以下特点是在没有hook的情况下)
// 没有生命周期,也会被更新并挂载,但是没有生命周期 
// 没有this(组件实例)
// 没有内部状态(state)
export default function App() {
    return (
        <div>Hello World</div>
    )
}

基本生命周期函数

  • constructor

    • 如果不进行初始化state或不进行方法绑定,则不需要为React组件实现构造函数
    • constructor通常只做两件事:初始化state,为事件绑定实例(this)
  • componentDidMount: 会在组件挂载后(插入DOM数中)立即调用

    • 依赖于DOM的操作可以在这里进行、
    • 在此发送网络请求就是最好的地方(官方建议)
    • 可以在此处添加一些订阅
  • componentDidUpdate:会在更新后被立即调用,首次渲染不会执行此方法

  • componentWillUnmount:会在组件卸载及销毁之前直接调用

    • 在此方法中执行必要的清理操作
  • 还有一些其他的生命周期可以查看官方文档:zh-hans.reactjs.org/docs/react-…

组件间通信

// 父子组件通信
// 父组件通过 属性=值 的形式来传递给子组件数据
// 子组件通过props参数获取父组件传递的数据    在class组件中 通过this.props// props 参数类型限制
import PropTypes from 'prop-types'
class App extends React.Component {
    render() {
        <h1>hello, { this.props.name }</h1>
    }
}
// 设置App组件的props参数的类型
App.propTypes = {
    name: PropTypes.string.isRequired   // 字符串类型,并且是必传的
}
// 设置App组件的props参数的默认值
App.defaultProps = {
    name: 'aaa'
}
// 函数组件和类组件设置props参数方法是一样的// 子组件传递父组件
// 还是通过props传递消息,只是让父组件传递一个回调函数,在子组件中调用这个函数即可
import React, { Component } from 'react';
​
// 子组件
class CounterButton extends Component {
  render() {
    const {onClick} = this.props;
    return <button onClick={onClick}>+1</button>
  }
}
​
export default class App extends Component {
  constructor(props) {
    super(props);
​
    this.state = {
      counter: 0
    }
  }
    
  render() {
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+</button>
            {/* 向子组件传递一个函数,子组件调用这个函数,即可 */}
        <CounterButton onClick={e => this.increment()} name="why"/>
      </div>
    )
  }
​
  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

实现vue中的slot效果

import React, { Component } from 'react'export default class App extends Component {
    render() {
        return (
            <div>
                <NavBar>
                    <div>aaa</div>
                    <div>bbb</div>
                    <div>ccc</div>
                </NavBar>
                <NavBar2 leftSlot={<div>aaa</div>}
                         centerSlot={<div>bbb</div>}
                         rightSlot={<div>ccc</div>}
                    />
            </div>
        )
    }
}
​
export default class NavBar extends Component {
  render() {
    // 写在组件标签中间的内容会被放到 this.props.children ;
    return (
      <div className="nav-item nav-bar">
        <div className="nav-left">
          {this.props.children[0]}
        </div>
        <div className="nav-item nav-center">
          {this.props.children[1]}
        </div>
        <div className="nav-item nav-right">
          {this.props.children[2]}
        </div>
      </div>
    )
  }
}
​
export default class NavBar2 extends Component {
  render() {
    const {leftSlot, centerSlot, rightSlot} = this.props;
​
    return (
      <div className="nav-item nav-bar">
        <div className="nav-left">
          {leftSlot}
        </div>
        <div className="nav-item nav-center">
          {centerSlot}
        </div>
        <div className="nav-item nav-right">
          {rightSlot}
        </div>
      </div>
    )
  }
}
​

context应用场景

非父子组件数据的共享:

  • 比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递
  • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
  • 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
// 在中间层的组件可以通过{...props}将父组件传递的数据传递个子组件// 在类组件中使用context
import React, { Component } from 'react'// 创建context对象,并且设置默认值
const UserContext = React.createContext({
    nickname: 'aaa',
    level: -1
})
​
class ProfileHeader extends Component {
    render() {
        return (
            <div>
                <h2>用户昵称: {this.context.nickname}</h2>
                <h2>用户等级: {this.context.level}</h2>
            </div>
        )
    }
}
// 设置ProfileHeader的contextType
ProfileHeader.contextType = UserContextfunction Profile(props) {
  return (
    <div>
      <ProfileHeader />
      <ul>
        <li>设置1</li>
        <li>设置2</li>
        <li>设置3</li>
        <li>设置4</li>
      </ul>
    </div>
  )
}
​
export default class App extends Component {
    constructor(props) {
        super(props)
        
        this.state = {
            nickname: 'aaa',
            level: 99
        }
    }
    
    render() {
        return (
            <div>
                <UserContext.Provider value={this.state}>
                    <Profile />
                </UserContext.Provider>
            </div>
        )
    }
}
​
// 在函数式组件中使用context,这种写法在类组件中也可以使用,而且不用设置ProfileHeader的contextType
import React, { Component } from 'react'// 创建context对象
const UserContext = React.createContext({
    nickname: 'aaa',
    level: -1
})
​
function ProfileHeader {
    // 函数式组件没有this,通过UserContext.Consumer获取传递的值
    return (
        <UserContext.Consumer>
            {
                value => {
                    return (
                        <div>{value.nickname  value.level}</div>
                    )
                }
            }
        </UserContext.Consumer>
    )
}
​
function Profile(props) {
  return (
    <div>
      <ProfileHeader />
      <ul>
        <li>设置1</li>
        <li>设置2</li>
        <li>设置3</li>
        <li>设置4</li>
      </ul>
    </div>
  )
}
​
class App extends Component {
    constructor() {
        super()
        this.state = {
            nickname: 'aaa',
            level: 'bbb'
        }
    }
    render() {
        return (
            <UserContext.Provider value={this.state}>
                <Profile />
            </UserContext.Provider>
        )
    }
}

为什么修改数据要使用setState?

  • 如果我们不通过setState修改state里的数据,React并不知道数据发生了改变,React没有实现类似于vue3的方法来监听数据的变化,所以我们必须通过setState方法来告诉React数据发生了改变,重新渲染页面

为什么在组件内部没有实现setState的方法但是却可以调用?

  • 原因很简单,setState方法是从Component中继承过来的

setState异步更新

changeText() {
    this.setState({
        message: 'aaa'
    })
    console.log(this.state.message) // 打印的结果是aaa修改之前的结果
}

为什么setState要设成异步的?

  • setState设计为异步,可以显著的提升性能

    • 如果每次调用setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
    • 最好的办法应该是获取到多个更新,之后批量进行更新
  • 如果同步了state,但是还没有执行render函数,那么state和props不能保持同步

    • state和props不能保持一致性,会在开发中产生很多的问题

如何获取更新后的值?

// 方式一:setState的回调
// setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后执行
changeText() {
    this.setState({
        message: 'aaa'
    }, () => {
        console.log(this.state.message)
    })
}
​
// 方式二:在生命周期中获取
componentDidUpdate(prevProps, prevState, snapshot) {
    console.log(this.state.message)
}

setState一定是异步吗?

  • 组件生命周期或者React合成事件中,setState是异步
  • setTimeout或者原生DOM事件中,setState是同步
// 在setTimeout中的更新
changeText() {
    setTimeout(() => {
        this.setState({
            message: 'aaa'
        })
        console.log(this.state.message) // 'aaa'
    }, 0)
}
// 在原生DOM事件中更新
componentDidMount() {
    const btnEl = document.getElementById('btn')
    btnEl.addEventListener('click', () => {
        this.setState({
            message: 'aaa'
        })
        console.log(this.state.message) // 'aaa'
    })
}

数据合并

当通过setState修改state对象时,react内部会调用Object.assign({}, this.state, 传入setState的对象)

Object.assign(target, ...sources): 该方法只会拷贝源对象自身的并且可枚举的属性到目标对象,如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖,后面的源对象的属性会类似的覆盖前面源对象的属性

setState本身合并

// 调用increment  counter只会加 1
increment() {
    this.setState({
        counter: this.state.counter + 1
    })
    
    this.setState({
        counter: this.state.counter + 1
    })
    
    this.setState({
        counter: this.state.counter + 1
    })
}
​
// 调用increment  counter 会加 3
increment() {
    this.setState((prevState, props) => ({
        counter: prevState.counter + 1
    }))
    
    this.setState((prevState, props) => ({
        counter: prevState.counter + 1
    }))
    
    this.setState((prevState, props) => ({
        counter: prevState.counter + 1
    }))
}

React更新流程

props/state改变 ---> render函数重新执行 ---> 产生新的DOM数 ---> 新旧DOM数进行diff ---> 计算出差异进行更新 ---> 更新到真实的DOM

新旧DOM树对比情况

keys的优化

在遍历列表时,总是会提示一个警告,让我们加入key属性,而加入一个唯一的key可以让react插入或者删除列表时的效率更高,性能更好

向列表中插入数据的方法

  • 方式一:在最后位置插入数据

    • 这种情况,有无key意义并不大
  • 方式二:在前面插入数据

    • 这种做法在没有key的情况下,所有项都需要修改
    • 当有唯一key时,有key的项只需要进行位移,将新的元素插入到前面即可

key的注意事项

  • key应该是惟一的
  • key不要使用随机数(随机数在下一次render时,会重新生成一个数字)
  • 使用index作为key,对性能是没有优化的

shouldComponentUpdate(控制render方法是否调用)

在一个嵌套组件中,如果父组件的render函数被调用时,所有的子组件的render函数都会被重新调用

事实上,很多的组件没有必要重新render,它们调用render应该有一个前提,就是依赖的数据(state,props)发生改变时,再调用自己的render函数

React给我们提供了一个生命周期方法shouldComponentUpdate(很多时候我们简称为scu),这个方法接收参数,并且需要有返回值

// 参数一:nextProps修改之后,最新的props
// 参数二:nextState修改之后,最新的state
// 该方法返回值是一个boolean类型
// 返回值为true(默认),那么就调用render方法
// 返回值为false,那么就不调用render方法
​
shouldComponentUpdate(nextProps, nextState) {
    if (this.state.counter !== nextState.counter) {
        return true
    }
    return false
}

PureComponent

如果所有的类,我们都需要手动来实现shouldComponentUpdate,那么会给我们开发者增加非常多的工作量

将class 继承自PureComponent即可帮助我们实现手动调用shouldComponentUpdate的效果

这个方法会调用shallowEqual方法,shallowEqual方法就是进行浅层比较

memo

类组件可以通过继承自PureComponent来实现性能优化,而函数组件可以通过memo这个高阶函数来实现和PureComponent相同的效果

import React, { memo } from 'react'
const MemoCpn = memo(function Cpn() {
 return (
    <div>memo</div>
 )   
})

不可变数据的力量

insertData() {
   // 1.在开发中不要这样来做
   // 这种写法页面不会重新渲染,也就是说没有响应式
   // 这种写法中feiends的内存地址不会改变,react会认为数据没有改变也就不会重新渲染界面
   const newData = {name: "tom", age: 30}
   this.state.friends.push(newData);
   this.setState({
     friends: this.state.friends
   });
​
   // 2.推荐做法
   // 这种做法页面是响应式的
   // 这种做法先浅拷贝了一个新对象,在新对象的基础上添加数据,最后在通过setState修改数据,由于friends对象和newFriends对象的内存地址不相同,所以页面会重新渲染
    const newFriends = [...this.state.friends];
    newFriends.push({ name: "tom", age: 30 });
    this.setState({
      friends: newFriends
    })
}

事件总线

在开发中如果有跨组件之间的事件传递,通过事件总线传递

// 第一步 安装 events  脚手架好像默认会帮我们安装好这个库
yarn add events
​
import { EventEmitter } from 'events'
const emiter = new EventEmitter()
// 发出事件
emitter.emit('事件名称',参数列表)
// 监听事件
emitter.on('事件名称', 监听函数)
// 移除事件
emitter.removeListener('事件名称',监听函数)

使用ref

// 给标签或者类组件添加ref
import React, { createRef, PureComponent } from 'react'
class App extends PureComponent {
    constructor() {
        super()
        this.titleRef = createRef()
        this.titleEl = null
    }
    
    render() {
        return (
            <div>
                <h2 ref={this.titleRef}>ref对象写法</h2>
                <h2 ref={e => this.titleEl = e}>ref函数写法</h2>
                <button onClick={e => changeText()}>改变文本</button>
            </div>
        )
    }
    changeText() {
        // 推荐使用对象写法
        this.titleRef.current.innerHTML = '对象写法'
        this.titleEl.innerHTML = '函数写法'
    }
}
​
// 给函数添加ref
// ref不能应用于函数式组件,因为函数式组件没有实例,所以不能获取到对应的组件对象
// 通过forwardRef高阶函数来实现给函数式组件添加ref
import React, {PureComponent, forwardRef, createRef} from 'react'
const Profile = forwardRef(function(props, ref) {
    return <p ref={ref}>Profile</p>
})
​
class App extends PureComponent {
    constructor() {
        super()
        this.profileRef = createRef()
    }
    render() {
        return (
            <div>
                <Profile ref={this.profileRef} />
            </div>
        )
    }
}

高阶组件(Higher-Order Components, 简称HOC)

高阶组件是一个参数为组件返回值为新组件函数,memo函数就是一个高阶组件

组件在浏览器控制台的展示名可以通过 组件名.displayName 来修改

Portals

某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载DOM元素中

Portals提供了一种将子节点渲染到存在于父节点以外的DOM节点的优秀的方案

import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'
​
class Modal extends PureComponent {
    render() {
        return ReactDom.createPortal(
            this.props.children,
            document.getElementById('#modal')
        )
    }
}
​
class Home extends PureComponent {
  render() {
    return (
      <div>
        <h2>Home</h2>
        <Modal>
          <h2>Title</h2>
        </Modal>
      </div>
    )
  }
}
​
export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home/>
      </div>
    )
  }
}

fragment

在开发中,我们总是在一个组件返回内容时包裹一个div元素,如果我们不希望渲染出这个div元素

可以通过使用Fragment,React提供了Fragment的短语法:<></>,看起来像空标签,但是如果需要在Fragment中添加key,则不能使用短语法

严格模式

包裹在<React.StrictMode></React.StrictMode>标签内的组件会开启严格模式
严格模式会检查什么?
  • 识别不安全的生命周期

  • 使用过时的ref API

  • 使用废弃的findDOMNode方法

  • 检查意外的副作用

    • 开启严格模式下,这个组件的constructor会被调用两次
    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
    • 在生产环境中,是不会被调用两次的
  • 检测过时的context API

React中的css

内联样式:
  • 内联样式是官方推荐的一种css样式的写法
  • style接受一个采用小驼峰命名属性的JavaScript对象,而不是css字符串
  • 并且可以引用state中的状态来设置相关的样式

内联样式的优点:

  • 内联样式不会有冲突
  • 可以动态获取当前state中的状态

内联样式的缺点:

  • 写法上都需要驼峰标识
  • 某些样式没有提示
  • 大量的样式,代码混乱
  • 某些样式无法编写(比如伪类/伪元素)
// 内联样式
import React, { PureComponent } from 'react'class App extends PureComponent {
    constructor() {
        super()
        this.state = {
            color: 'pink'
        }
    }
    render() {
        const pStyle = {
            color: this.state.color,
            textDecoration: 'underline'
        }
        return (
            <div>
                <h2 style={{fontSize: '20px', color: 'red'}}></h2>
                <p style={pStyle}></p>
            </div>
        )
    }
}
普通的css
  • 普通的css我们通常会编写到一个单独的文件,然后再进行引入
  • 在组件化开发中,我们总是希望组件是一个独立的模块,即使是样式也只是在自己内部生效,不会相互影响
  • 但是普通的css都属于全局的css,样式之间会相互影响

css modules(解决了局部作用域的问题)

css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用

React的脚手架已经内置了css modules的配置

cssmodule的缺点

  • 引用的类名,不能使用连接符(.home-title),在JavaScript总是不识别的
  • 所有的className都必须使用{ style.className }的形式来编写
  • 不方便动态来修改某些样式,依然需要使用内联样式的方式
// css modules
// 编写的css文件要以 .modules.css 作为文件名的结尾
import style from './style.modules.css'
export default class Profile extends PureComponent {
  constructor(props) {
    super(props);
​
    this.state = {
      color: "purple"
    }
  }
​
  render() {
    return (
      <div className="profile">
        <h2 className={style.title} style={{color: this.state.color}}>我是Profile的标题          </h2>
        <ul className={style.settings}>
          <li className={style.settingItem}>设置列表1</li>
          <li>设置列表2</li>
          <li>设置列表3</li>
        </ul>
      </div>
    )
  }
}
​
css in js
  • "Css in JS" 是指一种模式,其中css由JavaScript生成而不是在外部文件中定义
  • 注意此功能并不是React的一部分,而是有第三方库提供的

目前比较流行的Css in JS 的库

  • styled-components
  • emotion
  • glamorous

使用styled-components(通过ES6标签模板字符串实现的)

  • 安装styled-components yarn add styled-components
import styled from 'styled-components'
const CFInput = styled.input.arrts({
    placeholder: 'cf',
    
})`
 color: red;
 border-color: ${props => props.bColor}
`
<CFInput bColor='red' />
    
// 还支持将一个组件作为参数,这样新生成的就会包含这个组件的样式
    const Input2 = styled(CFInput)``
    
// 支持嵌套// styled设置主体
import {ThemeProvider} from 'styled-components'
<ThemeProvider theme={{color: 'red', fontSize: '30px'}}>
    <Home></Home>
</ThemeProvider>

修改React的webpack配置

  • 可以通过yarn run eject 来暴露出来对应的配置信息进行修改
  • 但是对于webpack并不熟悉的人来说,直接修改不是很好
Craco
  • 安装craco yarn add @craco/craco
  • 修改package.json文件 将原本是 react-scripts 改成craco

image-20210905094333713.png

  • 在根目录下创建craco.config.js用于修改默认配置
// 配置别名
// 在craco.config.js文件中
module.exports = {
    webapck: {
        alias" {
            // 在这里进行配置即可,注意路径要用绝对路径
        }
    }
}

Redux

纯函数

  • 确定是输入一定会产生确定的输出
  • 执行过程中不会产生副作用(修改形参等等)

React中就要求我们无论是函数还是class声明一个组件,这个组件都必须向纯函数一样,保护它们的props不被修改

Redux是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理

Redux的核心理念
  • store

  • action:Redux要求我们通过action来更新数据

    • 所有的数据变化都必须通过派发(dispatch)action来更新
    • action是一个普通的JavaScript对象,用来描述这次更新的type和content
    • 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可追踪的
Redux的使用过程
  • 创建一个对象,作为我们要保存的状态

  • 创建store来存储这个state

    • 创建store时必须创建reducer
    • 可以通过store.getState来获取当前的state
  • 通过action来修改state

    • 通过dispatch来派发action
    • 通常action中都会有type属性,也可以携带其他的数据
  • 修改reducer中的处理代码

    • 这里一定要注意,reducer是一个纯函数,不需要直接修改state
// 首先在命令行安装redux
//yarn add redux
import { createStore } from 'redux'
const defaultState = {
    num: 0
}
const store = createStore(reducer)
function reducer(state = defaultState, action) {
    switch (action.type) {
        case 'add1':
        case 'add2':
            return { ...state, num: action.num }
        default:
            return state
    }
}
// 多次dispatch只会有一次效果,因为num的值在定义时已经确定了
const action1 = {
    type: 'add1',
    num: store.getState().num + 1
}
// 多次dispatch会有多次效果
const action2 = state = > ({
    type: 'add2',
    num: state.num + 2
})
// 这个函数可以监听dispatch
store.subscribe(() => {
    console.log('num:' + store.getState().num);
})
store.dispatch(action1)
store,dispatch(action2(store.getStore))
​
自定义React-Redux的connect函数
// 创建一个context对象
import React from 'react';
​
const StoreContext = React.createContext(); 
​
export {
  StoreContext
}
​
// 在index.js中
import { StoreContext } from './utils/context'
import store from '....'ReactDOM.render(
  <StoreContext.Provider value={store}>
    <App />
  </StoreContext.Provider>,
  document.getElementById('root')
);
​
// connect 函数
import React, { PureComponent } from "react";
​
import { StoreContext } from './context';
​
export function connect(mapStateToProps, mapDispachToProp) {
  return function enhanceHOC(WrappedComponent) {
    class EnhanceComponent extends PureComponent {
      constructor(props, context) {
        super(props, context);
​
        this.state = {
          storeState: mapStateToProps(context.getState())
        }
      }
​
      componentDidMount() {
        this.unsubscribe = this.context.subscribe(() => {
          this.setState({
            storeState: mapStateToProps(this.context.getState())
          })
        })
      }
​
      componentWillUnmount() {
        this.unsubscribe();
      }
​
      render() {
        return <WrappedComponent {...this.props}
          {...mapStateToProps(this.context.getState())}
          {...mapDispachToProp(this.context.dispatch)} />
      }
    }
​
    EnhanceComponent.contextType = StoreContext;
​
    return EnhanceComponent;
  }
}
react-redux的使用
  • 安装react-redux yarn add react-redux

    // 在组件中
    import { connect } from 'react-redux'
    export default connect(mapStateToProps, mapDispatchToProps)(Home)
    ​
    // 在 index.js中
    import { Provider } from 'react-redux'
    import { Store } from './store'ReactDOM.render(
        <Provider store={store}>
            <App />
        </Provider>,
        document.getElementById('root')
    )
    
redux-thunk
  • 在之前的简单案例中,redux保存的counter是一个本地定义的数据
  • 但是真实开发中,redux保存的很多数据可能是来自服务器,我们需要进行异步请求,然后将数据保存到redux中
  • 我们可以将网络请求的异步代码放到组件的生命周期中,等到返回数据后,再将数据存到redux中
  • 事实上,网络请求的数据也属于我们状态管理的一部分,更好的一种方式应该是将异步代码也交给redux管理
redux中如何可以进行异步操作?
  • 使用中间件
  • 这个中间件的目的是在dispatch的action在最终到达reducer之间,扩展一些自己的代码
  • 我们这里要做的事情就是发送异步的网络请求
如何使用redux-thunk
  1. 安装redux-thunk yarn add redux-thunk

  2. 在创建store时传入应用了middleware的enhance函数

    • 通过applyMiddleware来结合多个Middleware,返回一个enhancer
    • 将enhancer作为第二个参数传入到createStore中
    • import { applyMiddleware } from 'redux'
      import thunkMiddleware from 'redux-thunk'
      const storeEnhancer = applyMiddleware(thunkMiddleware)
      const store = createStore(reducer, storeEnhancer)
      ​
      
  3. 定义返回一个函数的action

    • 注意:这里不是返回一个对象了,而是一个函数
    • 该函数会在dispatch之后会被执行
    • const action = (dispatch, getstate) => {
          // 在这里进行异步操作
          // 等到异步操作结果返回时,在通过dispatch将数据保存到redux中
      }
      
redux-devtools

redux可以方便的让我们对状态进行跟踪和调试,那么如何做到?

  • redux官网为我们提供了redux-devtools的工具,利用这个工具我们就可以知道每次状态是如何被修改的,修改前后的状态变化等等

安装该工具的步骤:

  1. 在浏览器中安装相关的插件redux-devtools

  2. 在redux中继承devtools的中间件

    import { createStore, applyMiddleware, compose } from 'redux';
    const composeEnhancers = 
          window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
    const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware);
    const store = createStore(reducer, composeEnhancers(storeEnhancer));
    
redux-sage

redux-saga是另外一个比较常用在redux发送异步请求的中间件,它的使用更加灵活。

reducer的拆分

在一个项目中,我们会有多个页面,如果将多个页面的状态都放到一个reducer中进行管理,随着项目的庞大,必然会造成代码臃肿,难以维护。所以我们需要对reducer进行拆分

redux给我们提供了一个将多个reducer合并成一个reducer的函数combineReducers

import {combineReducers} from 'redux'import { reducer as counterReducer } from './counter';
import { reducer as homeReducer } from './home';
​
const reducer = combinereducers({
    counterInfo: counterReducer,
    homeInfo: homeReducer
})

react-router

安装react-router:

  • 安装react-router-dom会自动帮助我们安装react-router的依赖
  • yarn add react-router-dom
Router的基本使用

react-router最主要的API是给我们提供的一些组件

  • BrowserRouter或HashRouter

    • router中包含了对路径改变的监听,并且会将相应的路径传递给子组件
    • BrowserRouter使用history模式
    • HashRouter使用
  • Link和NavLink

    • 通常路径的跳转是使用Link组件,最终会被渲染成a元素

    • NavLink是在Link的基础上增加了一些样式属性: 默认情况下,使用NavLink组件当路径选中时会添加class,可以通过activeClassName属性来修改class名,可以向activeStyle属性传递一个css对象​修改样式

      to属性:Link中最重要的属性,用于设置跳转到的路径

  • Route

    • Route用于路径的匹配
    • path属性:用于设置匹配到的路径
    • component属性:设置匹配到路径后,渲染的组件
    • exact:精确匹配,只有精确匹配到完全一致的路径,才会渲染对应的组件
import { BrowserRouter, Route, Link } from 'react-router-dom'
import Home from './home'
import About from './about'
import Profile from './profile'// 在render函数中使用
render() {
    return (
        <BrowserRouter>
            <Link to='/'>首页</Link>
            <Link to='/about'>关于</Link>
            <Link to='profile'>我的</Link>
            
            <Route exact path='/' component={Home} />
            <Route path='/about' component={About} />
            <Route path='/profile' component={Profile} />
        </BrowserRouter>
    )
}
Switch的作用

默认情况下,只要路径匹配,对应的组件就会被渲染,但是在实际开发中往往希望有一种排他的思想,只要匹配到了第一个,那么后面的就不应该继续匹配了,这个时候用组件将Route标签包裹起来即可

参数问题

查看react笔记2

手动跳转路由

向手动跳转路由就必须获取到history对象

如何可以获取到history对象?

  • 如果该组件是通过路由直接跳转过来的,那么就可以直接从props中获取history,location,match对象
  • 如果该组件是一个普通渲染的组件,那么要通过一个高阶组件withRouter包裹 withRouter(App)
react-router-config
  • 目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的
  • 但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理
  • 安装react-router-config yarn add react-router-config
  • 配置路由映射的关系数组
  • 使用renderRoutes函数完成配置
// 配置路由映射关系数组文件
// 导入组件
import { Redirect } from 'react-router-dom'
const routes = [
    {
        path: '/',
        exact: true,
        render: () => (   // 重定向
            <Redirect to='/home' />
        )
    },
    {
        path: '/about',
        component: About,
        routes: [
            // 子路由
        ]
    }
]
export default routes
​
// 在App组件中
import { renderRoutes } from 'react-router-config'import routes from './router'
render() {
    return (
        { renderRoutes(routes) }
    )
}

React Hooks

简单总结Hook:
  • 它可以让我们在不编写class的情况下使用state和其他React的特性
Hook的使用场景:
  • Hook的出现基本上可以代替我们之前所有使用class组件的地方(除了一些非常不常用的场景)
  • 如果是一个旧项目,并不需要将所有的代码重构,因为它完全向下兼容,可以渐进式的使用它
  • Hook只能在函数式组件中使用,不能再函数组件之外的地方使用
  • 只能在函数最外层调用Hook,不能再循环,条件判断或者子函数中调用
useState
import {useState} from 'react'// 解构出来的第一个参数是变量,
// 第二个参数是修改该变量的方法,它接收一个新的state值,并将组件的一次渲染加入队列
const [变量名, set变量名] = useState()  // 括号里可以添加默认值

与class组件中的setState不同,useState不会自动合并更新对象。可以用函数式的setState结合展开运算符来达到合并更新对象的效果

const [state, setState] = useState({})
setState((prevState) => {
    return {...prevState, ...updateValues}
})
useEffect

我们可以在useEffect函数中传入一个回调参数,当react渲染到某一阶段该回调会被调用,我们可以在useEffect中进行网络请求,手动更新DOM,一些事件的监听

// 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个回调函数
useEffect(() => {
    console.log("订阅一些事件");
    // 当组件被销毁时,会调用的个return 的回调函数
    return () => {
      console.log("取消订阅事件")
    }
  });

使用多个useEffect: useEffect这个Hook可以多次使用,在不同的useEffect完成不同的操作,执行代码的顺序和书写代码的顺序一致

Effect性能优化:默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是有些时候我们只希望他执行一次,或者某些数据改变了才重新执行。这个时候我们可以通过useEffect的第二个参数来决定

useEffect(() => {
    console.log("修改DOM", count);
  }, [count]); // 只有当count发生变化时,才会重新执行
​
  useEffect(() => {
    console.log("订阅事件"); 
  }, []); // 只有在第一次进入组件时,才会执行一次
​
  useEffect(() => {
    console.log("网络请求");
  }, []);// 只有在第一次进入组件时,才会执行一次
useContext

在之前开发中,我们要在组件中使用共享的Context有两种方式

  • 类组件可以通过类名.contextType = MyContext方式,在类中获取context
  • 多个Context或者在函数式组件中通过MyContext.Consumer 方式共享context
// user 和 theme就是从最近一层的Provider提供的数据  
const user = useContext(UserContext);//这里的参数必须是通过createContext()创建出来的context对象
const theme = useContext(ThemeContext);
useReducer

useReducer仅仅是useState的一种替代方案:

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分

  • 或者这次的修改需要依赖之前的state,也可以使用

    const initialState = {count: 0};
    ​
    function reducer(state, action) {
      switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
          return {count: state.count - 1};
        default:
          throw new Error();
      }
    }
    ​
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
      return (
        <>
          Count: {state.count}
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
      );
    }
    
useCallback

useCallback实际的目的是为了进行性能优化

如何进行性能优化?

  • useCallback会返回一个函数的memoized(记忆的)值
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
const increment2 = useCallback(() => {
    console.log("执行increment2函数");
    setCount(count + 1);
  }, [count]);

通常使用useCallback的目的是不希望子组件多次渲染,并不是为了函数进行缓存。

当父组件向子组件传递一个函数时,这个时候使用useCallbac返回的函数,可以让组件减少渲染次数。

useMemo

useMemo实际的目的是也为了进行性能优化

如何进行性能优化?

  • useMemo返回的也是一个memoized(记忆的)值
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
// 案例一:进行大量计算,是否有必要每次渲染时都重新计算
import React, {useState, useMemo} from 'react';
​
function calcNumber(count) {
  console.log("calcNumber重新计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}
​
export default function MemoHookDemo01() {
  const [count, setCount] = useState(10);
  const [show, setShow] = useState(true);
​
  // const total = calcNumber(count); // 如果采用这种做法,每次修改show都会重新计算
  const total = useMemo(() => { // 采用这种方式,只有count发生变化时才会重新计算
    return calcNumber(count);
  }, [count]);
​
  return (
    <div>
      <h2>计算数字的和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}
// 案例二 向子组件传递相同内容的对象时
import React, { useState, memo, useMemo } from 'react';
​
const HYInfo = memo((props) => {
  console.log("HYInfo重新渲染");
  return <h2>名字: {props.info.name} 年龄: {props.info.age}</h2>
});
​
export default function MemoHookDemo02() {
  console.log("MemoHookDemo02重新渲染");
  const [show, setShow] = useState(true);
​
 //const info = { name: "why", age: 18 };采用这种info,修改show时,父组件和HYInfo都会重新渲染
  const info = useMemo(() => { // 采用这种info,修改show时,父组件会重新渲染,HYInfo不会
    return { name: "why", age: 18 };
  }, []);
​
  return (
    <div>
      <HYInfo info={info} />
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}
useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变

最常用的ref是两种用法:

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素
  • 用法二:保存一个数据,这个对象咋整个生命周期可以保持不变
// 引用DOM
class TestCpn extends React.Component {
  render() {
    return <h2>TestCpn</h2>
  }
}
​
// 函数式组件要使用forwardRef包裹
const TestCpn2 = forwardRef(function TestCpn2(props, ref) {
  return <h2 ref={ref}>TestCpn2</h2>
})
​
export default function RefHookDemo01() {
​
  const titleRef = useRef();
  const inputRef = useRef();
  const testRef = useRef();
  const testRef2 = useRef();
​
  function changeDOM() {
    titleRef.current.innerHTML = "Hello World";
    inputRef.current.focus();
    console.log(testRef.current);
    console.log(testRef2.current);
  }
​
  return (
    <div>
      <h2 ref={titleRef}>RefHookDemo01</h2>
      <input ref={inputRef} type="text"/>
      <TestCpn ref={testRef}/>
      <TestCpn2 ref={testRef2}/>
​
      <button onClick={e => changeDOM()}>修改DOM</button>
    </div>
  )
}
// 保存一个数据
export default function RefHookDemo02() {
  const [count, setCount] = useState(0);
​
  const numRef = useRef(count);
  console.log(numRef.current);
  useEffect(() => {
    numRef.current = count;
  }, [count])
​
  return (
    <div>
      {/* <h2>numRef中的值: {numRef.current}</h2>
      <h2>count中的值: {count}</h2> */}
      <h2>count上一次的值: {numRef.current}</h2>
      <h2>count这一次的值: {count}</h2>
      <button onClick={e => setCount(count + 10)}>+10</button>
    </div>
  )
}
useImperativehandle
// 使用useImperativehandle可以指定暴露给父组件的方法
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
​
const CFInput = forwardRef((props, ref) => {
    const inputRef = useRef()
    
    useImperativeHandle(ref, () => {
        focus: () => {
            inputRef.current.focus()
        }
    }, [inputRef])
    return <input ref={inputRef} type='text'></input>
})
​
function UseImperativeHandleHookDemo() {
  const inputRef = useRef();
  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e => inputRef.current.focus()}>聚焦</button>
    </div>
  )
}
​
useLayoutEffect

useLayoutEffect和useEffect非常相似,事实上他们只有一点的区别

  • useEffect会在渲染的内容更新到DOM后执行,不会阻塞DOM的更新
  • useLayoutEffect会在渲染内容更新到DOM上之前执行,会阻塞DOM的更新
import React, { useState, useEffect } from 'react'export default function EffectCounterDemo() {
  const [count, setCount] = useState(10);
​
  useEffect(() => { // 界面会有一闪而过的0
    if (count === 0) {
      setCount(Math.random() + 200)
    }
  }, [count]);
   useLayoutEffect(() => { // 界面不会有一闪而过的0
    if (count === 0) {
      setCount(Math.random() + 200)
    }
  }, [count]);
​
  return (
    <div>
      <h2>数字: {count}</h2>
      <button onClick={e => setCount(0)}>修改数字</button>
    </div>
  )
}

redux-hook

useSelector的作用是将state映射到组件中
import { useSelector, shallowEqual, useDispatch } from 'react-redux'
const { 数据名 } = useSelector(state => ({
    数据名: 通过state或去你想要的redux数据
}), shallowEqual) // shallowEqual 用于浅层比较,性能优化const dispatch = useDispatch() // dispatch 可以用于派发action

immutable js