React(复习)

181 阅读20分钟

React初体验

需要引入三个包:react、react-dom、babel

  • react :里面包含了react核心代码
  • react-dom:里面是用来实现跨平台的代码
  • babel:用来将 jsx 语法转换成 js 代码
  • 使用babel,需要将 script 的 type 属性设置为 text/babel
  • ReactDOM.createRoot 创建根节点,之后render后的内容包含在这个根中
  • root.render 渲染 jsx 文件
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

    <script type="text/babel">
      // react18 更新成这样渲染
      const root = ReactDOM.createRoot(document.getElementById('root'))
      root.render(<h2>hello react</h2>)
    </script>
  </body>
</html>

案例:更换文本 hello react

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- 引入 react -->
    <script src="../lib/react.js"></script>
    <!-- 引入 react-dom -->
    <script src="../lib/react-dom.js"></script>
    <!-- 引入 babel -->
    <script src="../lib/babel.js"></script>

    <script type="text/babel">
      // 1. 将文本定义成变量
      let message = 'hello react'

      // 2. 定义函数
      function btnClick() {
        message = '123456'
        rootRender()
      }

      const root = ReactDOM.createRoot(document.getElementById('root'))

      function rootRender() {
        root.render(
          <div>
            <h2>{message}</h2>
            <button onClick={btnClick}>修改文本</button>
          </div>
        )
      }

      rootRender()
    </script>
  </body>
</html>

类组件

  • 需要继承 React.Component 类
  • 需要提供 render() {} 函数

数据在哪里定义

参与界面更新的数据

  • 一般定义在 constructor 中
  • this.state = {定义的数据}
  • 当数据发生变化,需要调用 this.setState 更新数据,会自动调用 render 函数

不参与界面更新的数据

JSX的使用

书写规范

  • jsx 结构中只能有一个根元素
  • jsx 结构通常会被()包裹,形成整体,为了方便阅读
  • jsx 可以是双标签结构,也可以是单标签结构

注释

  • 可以使用 { /**/ } 来添加注释

JSX具体用法

JSX浅入变量作为子元素

  • 情况一:当变量是Number、String、Array类型时,可以直接显示
  • 情况二:当变量是null、undefined、boolean类型时,内容为空
  • 情况三:Object对象不能作为子元素
  • 可以插入表达式

JSX嵌入表达式

  • 三元运算符
  • 运算表达式
  • 执行一个函数

JSX绑定属性

  • title属性
  • 内联样式 style
  • img 的 src 属性等
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- 引入 react -->
    <script src="../lib/react.js"></script>
    <!-- 引入 react-dom -->
    <script src="../lib/react-dom.js"></script>
    <!-- 引入 babel -->
    <script src="../lib/babel.js"></script>


    <script type="text/babel">
      class App extends React.Component {
        constructor() {
          super()
          this.state = {
            message: 'Hello, World',
            title: '哈哈哈',
            imgUrl: 'https://pic35.photophoto.cn/20150511/0034034892281415_b.jpg',
            href: 'https://www.baidu.com',
            isActive: false,
            objStyle: { color: 'red', fontSize: '13px' }
          }
        }


        render() {
          const { message, title, imgUrl, href, isActive, objStyle } = this.state
          return (
            <div>
              {/* 绑定 title 属性 */}
              <h2 title={title}>h2</h2>
              {/* 绑定 src 属性 */}
              <img src={imgUrl} alt="" />
              {/* 绑定 href 属性 */}
              <a href={href}>百度一下</a>


              {/* 绑定 class 属性 */}
              <h2 className={isActive ? 'abc' : ''}>123</h2>
              {/* 绑定 style 属性 */}
              <h2 style={objStyle}>456</h2>
            </div>
          )
        }
      }
      const root = ReactDOM.createRoot(document.getElementById('root'))
      root.render(<App />)
    </script>
  </body>
</html>

JSX 转换过程

  • 所有的JSX都会转成 React.createElement() 函数

JSX首先通过 Babel 进行转换,每遇到一个标签,转换成 React.createElement('div', {class, id},[]) 进行函数调用,最后变成一个树结构(虚拟DOM)
image.png

React组件化开发

生命周期

image.png
image.png

案例:

import React from 'react'


export default class HelloWorld extends React.Component {
  // 1. 构造方法
  constructor() {
    console.log('hello world constructor')
    super()


    this.state = {
      message: 'hello world'
    }
  }


  changeTest() {
    this.setState({ message: '你好呀' })
  }
  // 2. render 方法
  render() {
    console.log('hello world render')
    const { message } = this.state
    return (
      <div>
        <div>{message}</div>
        <button onClick={e => this.changeTest()}>修改文本</button>
      </div>
    )
  }


  // 3. 组件被挂载到 DOM
  componentDidMount() {
    console.log('helloworld Mount')
  }


  // 4. 组件 DOM 更新完成 updated
  componentDidUpdate() {
    console.log('helloworld Updated')
  }


  // 5. 销毁组件
  componentWillUnmount() {
    console.log('helloworld Unmount')
  }
}

三个阶段

主要分为下面三个阶段:

  • 装载阶段(Mount)
  • 更新过程(Update)
  • 卸载过程(UnMount)

生命周期函数

主要分为下面几个函数:

  • componentDidMount :组件挂载到DOM上,触发回调
  • componentDidUpdate:组件发生更新时,触发回调
  • componentWillUnmount:组件即将被移除时,触发回调
  • shouldComponentUpdate: 要不要更新DOM (返false不执行) ,性能优化
  • getSnapshotBeforeUpdate:在DOM更新之前,提前保存数据
  • getDerivedStateFromProps:state的值在任何时候都依赖于props使用;该方法返回一个对象来更新state

Context 上下文

用来给子组件传递数据
使用步骤:

  1. 创建一个 Context(一般抽取成js文件)
  2. 用<ThemeContext.Provider value={{需要传递的数据}}> </ThemeContext.Provider> 包裹需要传递数据的子组件
  3. 在子组件中也需要引入 Context,然后用 组件名.contextType = Context名,即可用 this.context 取到传递的值

context.js

import React from 'react';

const ThemeContext = React.createContext();

export default ThemeContext

App.jsx

import React, { Component } from 'react'


import Home from './Home'
import ThemeContext from './context/theme-context'


export class App extends Component {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{ color: 'red', size: '30' }}>
          <Home />
        </ThemeContext.Provider>
      </div>
    )
  }
}


export default App

Home.jsx

import React, { Component } from 'react'


import ThemeContext from './context/theme-context'


export class Home extends Component {
  render() {
    console.log(this.context)
    return <div>Home</div>
  }
}


Home.contextType = ThemeContext


export default Home

组件传值

props-Types 参数类型限制

  • 还可以使用 Typescript、Flow 来进行限制
  • propsTypes 参数类型限制
  • defaultProps 添加默认值
import React, { Component } from 'react'
import PropTypes from 'prop-types'

class MainBanner extends Component {
  render() {
    const { banners } = this.props
    return <div>{banners}</div>
  }
}

// 参数限制
MainBanner.propsTypes = {
  // 类型为数组,必传项
  banners: PropTypes.array.isRequired
}
MainBanner.defaultProps = {
  banners: []
}

export default MainBanner

父传子:

  • 父组件通过
  • 子组件通过 props 接收,this.props

父组件:banner.jsx

import React, { Component } from 'react'
import MainBanner from './MainBanner'
import MainProductList from './MainProductList'


export default class Main extends Component {
  constructor() {
    super()


    this.state = {
      banners: ['歌曲', 'MV', '歌单'],
      productList: ['商品1', '商品2', '商品3']
    }
  }
  render() {
    const { banners, productList } = this.state
    return (
      <div>
        Main
        <MainBanner banners={banners} />
        <MainProductList productList={productList} />
      </div>
    )
  }
}

子组件:MainBanner.jsx

import React, { Component } from 'react'


export default class MainBanner extends Component {
  render() {
    const { banners } = this.props
    return <div>{banners}</div>
  }
}

子传父:

  • 也是通过prop的原理,只不过是传递了一个函数

父组件App.jsx:

import React, { Component } from 'react'
import AddCounter from './AddCounter'


export class App extends Component {
  constructor() {
    super()


    this.state = {
      counter: 100
    }
  }
  changeCount(count) {
    this.setState({ counter: this.state.counter + count })
  }
  render() {
    const { counter } = this.state
    return (
      <div>
        <h2>当前计数:{counter}</h2>
        <AddCounter addClick={count => this.changeCount(count)} />
      </div>
    )
  }
}


export default App

子组件AddCount.jsx:

import React, { Component } from 'react'


export class AddCounter extends Component {
  addCount(val) {
    console.log(val)
    this.props.addClick(val)
  }


  render() {
    return (
      <div>
        <button onClick={e => this.addCount(1)}>+1</button>
        <button onClick={e => this.addCount(5)}>+5</button>
        <button onClick={e => this.addCount(10)}>+10</button>
      </div>
    )
  }
}


export default AddCounter

案例:tabs切换

父组件:App.jsx

import React, { Component } from 'react'
import Tabs from './Tabs'


export class App extends Component {
  constructor() {
    super()


    this.state = {
      titles: ['流行', '新款', '精选'],
      currentIndex: 0
    }
  }
  changeParentIndex(index) {
    this.setState({ currentIndex: index })
  }
  render() {
    const { titles, currentIndex } = this.state
    return (
      <div className="app">
        <Tabs titles={titles} changeParentIndex={index => this.changeParentIndex(index)} />
        <h1>{titles[currentIndex]}</h1>
      </div>
    )
  }
}


export default App

子组件:Tabs.jsx

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './style.css'

export class Tabs extends Component {
  constructor() {
    super()

    this.state = {
      currentIndex: 0
    }
  }
  changeIndex(index) {
    this.props.changeParentIndex(index)
    this.setState({ currentIndex: index })
  }

  render() {
    const { titles } = this.props
    const { currentIndex } = this.state
    return (
      <div className="tabs">
        {titles.map((item, index) => {
        return (
          <div className={`item ${index === currentIndex ? 'active' : ''}`} key={item} onClick={e => this.changeIndex(index)}>
            <span className="text">{item}</span>
          </div>
        )
      })}
      </div>
    )
  }
}

Tabs.propTypes = {
  titles: PropTypes.array.isRequired
}

Tabs.defaultProps = {
  titles: []
}
export default Tabs

插槽 Slot

  • children有一个弊端,传入的元素很容易出错
  • 推荐用 props 的方式

使用children实现

  • 主要将内容放进子组件内, <子组件> content </子组件>
  • 在子组件中,使用 this.props.children(数组) 获取

App.jsx

import React, { Component } from 'react'
import NavBar from './nav-bar/NavBar'


export class App extends Component {
  render() {
    return (
      <div>
        <NavBar>
          <button>按钮</button>
          <h2>标题</h2>
          <i>123</i>
        </NavBar>
      </div>
    )
  }
}


export default App

NavBar.jsx

import React, { Component } from 'react'
import './style.css'


export class NavBar extends Component {
  render() {
    const { children } = this.props
    return (
      <div className="nav-bar">
        <div className="left">{children[0]}</div>
        <div className="center">{children[1]}</div>
        <div className="right">{children[2]}</div>
      </div>
    )
  }
}


export default NavBar

使用props实现

App.jsx

import React, { Component } from 'react'
import NavBar from './nav-bar/NavBar'
import NavBar2 from './nav-bar2/NavBar'


export class App extends Component {
  render() {
    return (
      <div>
        {/* 使用 children 方式 */}
        <NavBar>
          <button>按钮</button>
          <h2>标题</h2>
          <i>123</i>
        </NavBar>


        {/* 使用 props 的方式 */}
        <NavBar2
          leftSlot={<button>按钮2</button>}
          centerSlot={231}
          rightSlot={321} />
      </div>
    )
  }
}


export default App

Nav-Bar.jsx

import React, { Component } from 'react'
import './style.css'


export class NavBar2 extends Component {
  render() {
    const { leftSlot, centerSlot, rightSlot } = this.props
    return (
      <div className="nav-bar">
        <div className="left">{leftSlot}</div>
        <div className="center">{centerSlot}</div>
        <div className="right">{rightSlot}</div>
      </div>
    )
  }
}


export default NavBar2

根据组件定义方式分类

函数组件

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数
  • this 关键字不能指向组件实例(因为没有)
  • 没有内部状态(state)

类组件

  • 组件的名称是大写字符开头
  • 必须继承自 React.Component
  • 必须实现 render 函数

根据组件内部是否有状态需要维护分类

无状态组件

有状态组件

根据组件不同职责分类

展示型组件

容器型组件

EventBus

  • event.on 监听事件
  • event.emit 发出事件
  • event.off 取消事件

App.jsx

import React, { Component } from 'react'


import Home from './Home'
import eventBus from './utils/event-bus'
export class App extends Component {
  bannerPrevClick() {
    console.log('app 监听 bannerPrev')
  }
  componentDidMount() {
    eventBus.on('bannerPrev', this.bannerPrevClick)
  }


  componentWillUnmount() {
    eventBus.off('bannerPrev', this.bannerPrevClick )
  }


  render() {
    return (
      <div>
        <h2>App Component</h2>
        <Home />
      </div>
    )
  }
}


export default App

HomeBanner.jsx

import React, { Component } from 'react'
import eventBus from './utils/event-bus'


export class HomeBanner extends Component {
  prevClick() {
    console.log('上一个')
    eventBus.emit('bannerPrev', 10)
  }
  nextClick() {
    console.log('下一个')
    eventBus.emit('bannerNext', 9)
  }


  render() {
    return (
      <div>
        <h2>HomeBanner</h2>
        <button onClick={e => this.prevClick()}>上一个</button>
        <button onClick={e => this.nextClick()}>下一个</button>
      </div>
    )
  }
}


export default HomeBanner

获取 DOM

获取类组件

  • 通过 this.refs.ref名称
  • 通过 let titleRef = createRef()函数,然后通过 ref = {this.titleRef()} 获取
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {}
    this.hwRef = createRef()
  }


  getComponent() {
    console.log(this.hwRef.current.test())
  }


  render() {
    return (
      <div>
        <button onClick={e => this.getComponent()}> 获取组件实例 </button>
        <HelloWorld ref={this.hwRef} />
      </div>
    )
  }
}

获取函数式组件

  • 通过 React.forwardRef() 方法
import React, { PureComponent, createRef, forwardRef } from 'react'

const HelloWorld = forwardRef(function (props, ref) {
  return (
    <div>
      <h1 ref={ref}>hello world</h1>
      <p>哈哈哈</p>
    </div>
  )
})

React 更新机制

React 渲染流程

image.png

React 更新流程

  • React 在 props 或 state 发生改变时,会调用 React 的 render 方法,创建一棵不同的树
  • 如果按照下述过程进行比较的话,时间复杂度为O(n³)
  • 此时需要用改进的diff算法,只进行同层级比较

image.png
image.png

diff 算法

  • 同层节点之间相互比较,不会跨节点比较
  • 不同类型的节点,产生不同的树结构
  • 开发中,通过key指定那些节点在不同渲染下保持稳定

image.png

性能优化

render 函数

SCU 优化

  • 可以使用 shouldComponentUpdate(nextProps, nextState) 生命周期函数,在里面对数据进行对比,可以提高性能,因为有一些组件在其他文本被改变的时候,他也重新渲染了

PureComponent 组件(用于类组件)

  • 继承PureComponent组件,可以省去多个判断条件,是React本身提供的
import React, { Component, PureComponent } from 'react'

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      message: 'hello',
      counter: 0
    }
  }
  render() {
    const { message, counter } = this.state
    return (
      <div>
        <h2>
          App-{message}-{counter}
        </h2>
      </div>
    )
  }
}


export default App

Memo (函数组件)

  • 使用 memo 方法将组件进行包裹
import { memo } from 'react'

const Profile = memo(function (props) {
  console.log('profile render')
  return <h2>Profile: {props.message}</h2>
})

export default Profile

数据不可变的力量

扩展运算符

  • 在添加数据时,不用 array.push()方法,使用 ... 扩展运算符, const books = [...this.state.books]

shallowEqual() 方法

高阶组件

  • 英文名:Higher-Order Components,简称 HOC
  • 接收一个组件作为他的参数,返回一个新的组件作为返回值
  • 相当于对组件进行了一层拦截操作,对组件进行增强
  • 如果大量使用HOC,会产生很多嵌套,不利于调试
  • HOC可以劫持props,在不遵守约定的情况下可能造成冲突
import React, { PureComponent } from 'react'


// 1. 定义一个高阶函数
function hoc(Cpn) {
  // 1. 定义类组件
  class NewCpn extends PureComponent {
    render() {
      return <Cpn />
    }
  }
  return NewCpn
}


class HelloWorld extends PureComponent {
  render() {
    return (
      <div>
        <h1>hello world</h1>
      </div>
    )
  }
}


const HelloWorldHOC = hoc(HelloWorld)


export class App extends PureComponent {
  render() {
    return (
      <div>
        <HelloWorldHOC />
      </div>
    )
  }
}


export default App

案例 1

App.jsx

import React, { PureComponent } from 'react'
import enhancedUserInfo from './enhanced_props'


const userInfo = {
  name: 'codeWhy',
  level: 99
}


const Home = enhancedUserInfo(function (props) {
  return <h1>home:{props.name}</h1>
})
const Profile = enhancedUserInfo(function (props) {
  return <h1>Profile</h1>
})
const HelloFriend = enhancedUserInfo(function (props) {
  return <h1>hellofriend</h1>
})


export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home />
        <Profile />
        <HelloFriend />
      </div>
    )
  }
}


export default App

enhanced_props.js

import { PureComponent } from "react"


function enhancedUserInfo(OriginComponent) {
  class NewComponent extends PureComponent {
    constructor() {
      super()


      this.state = {
        userInfo: {
          name: 'codeWhy',
          level: 99
        }
      }
    }
    render() {
      return <OriginComponent {...this.props} {...this.state.userInfo} />
    }
  }
  return NewComponent
}


export default enhancedUserInfo

案例2:使用context+hoc增强

App.jsx

import React, { PureComponent } from 'react'
import ThemeContext from './context/theme_context'
import Product from './pages/Product'


export class App extends PureComponent {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{ color: 'red', size: 30 }}>
          <Product />
        </ThemeContext.Provider>
      </div>
    )
  }
}


export default App

Product.jsx

import React, { PureComponent } from 'react'
import withTheme from '../hoc/with_theme'


export class Product extends PureComponent {
  render() {
    const { color, size } = this.props
    return (
      <div>
        product: {color} {size}
      </div>
    )
  }
}


export default withTheme(Product)

with_theme.js

import ThemeContext from "../context/theme_context"


function withTheme(OriginComponent) {
  return (props) => {
    // function NewComponent(props) {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponent {...value} {...props} />
          }
        }
      </ThemeContext.Consumer>
    )
  }
}


export default withTheme

案例3:登录鉴权

App.jsx

login_auth.js

function loginAuth(OriginComponent) {
  return props => {
    // 从 localStorage 获取 token
    const token = localStorage.getItem("token")
    if (token) {
      return <OriginComponent {...props} />
    }
    else {
      return <h2>请先登录</h2>
    }
  }
}


export default loginAuth

cart.jsx

import React, { PureComponent } from 'react'
import loginAuth from '../hoc/login_auth'


export class Cart extends PureComponent {
  render() {
    return <div>Cart</div>
  }
}


export default loginAuth(Cart)

Portals 组件

  • 可以使组件不挂载到root中,让他挂载到指定元素上
  • 需要提供两个参数
    • 第一个:可以是任何可渲染的React子元素
    • 第二个:是一个DOM元素
  • import {createPortals} from 'react-dom'

Fragment 组件

  • 可以用Fragment包裹标签,不会生成额外标签
import React, { Fragment, PureComponent } from 'react'

export class App extends PureComponent {
  render() {
    return (
      <Fragment>
        <h2>我是App标题</h2>
        <p>我是App内容,哈哈哈哈</p>
      </Fragment>
    )
  }
}


export default App

StrictMode 严格模式

  • 会检测不安全的生命周期
  • 检测过时 Api
  • 检查意外的副作用
    • 组件的constructor会调用两次,这是故意进行的操作,让你查看你写的一些逻辑代码被调用多次是否有副作用
    • 在生产环境不会调用两次
  • 检测过时的 Context Api

React 过渡动画

react-transition-group库

  • Transition:该组件和平台无关,不一定要结合css
  • CSSTransition:在前端开发中,一般用这个完成过渡动画
  • SwitchTransition:两个组件显示和隐藏切换
  • TransitionGroup:将多个动画组件包裹,一般用于列表中元素动画

CSSTransition

image.png

React中如何编写CSS

内联样式

  • style接受一个采用小驼峰命名属性的 JavaScript对象,而不是CSS字符串
  • 并且可以引用state中的状态来设置相关样式
  • 内联样式之间不会有冲突
  • 某些样式无法编写(伪元素/伪类)

普通CSS

  • 会导致CSS影响全局,需要配置 css modules
  • 因为 React 脚手架内置了 css modules,将 .css/.less/.scss 等文件修改成 .module.css/.module.less
  • 通过在App.jsx引入 App.module.css 文件
  • 但是这种方法不太方便

App.jsx

import React, { PureComponent } from 'react'
import appStyle from './App.module.css'

export class App extends PureComponent {
  render() {
    return (
      <div>
        <h2 className={appStyle.title}>我是标题</h2>
      </div>
    )
  }
}


export default App

App.module.css

.title {
  font-size: 50px;
}

CSS in JS

  • 此功能是第三方提供
  • React 对样式如何定义没有明确态度
  • 目前流行的CSS in JS库有: styled-components(常用)、emotion、glamorous

styled-components

使用步骤

  1. npm install styled-components
  2. 新建 style.js 文件
  3. 编写 style.js
import styled from 'styled-components'
import { primaryColor } from './style/variables'

export const AppWrapper = styled.div`
  .section {
    border: 1px solid red;
    
  }

  .footer {
    border: 1px solid orange
  }
`


// 可以接收外部传入的 props
export const SectionWrapper = styled.div`
  .title {
    font-size: ${props => props.size}px;
    color: ${props => props.color};
  }
  .content {
    color: ${primaryColor}
  }
  
`
  1. 在App.jsx中引入
import React, { PureComponent } from 'react'
import { AppWrapper } from './style'


export class App extends PureComponent {
  render() {
    return (
      <AppWrapper className="app">
        <div className="section">
          <div className="title">我是标题</div>
          <div className="content">我是内容</div>
        </div>
        <div className="footer">
          <p>免责声明</p>
        </div>
      </AppWrapper>
    )
  }
}


export default App

高级特性

  • 通过 jsx 传入属性
// 可以接收外部传入的 props
export const SectionWrapper = styled.div.attrs(props => {return { tColor: props:color || 'blue'} })`
  .title {
    font-size: ${props => props.size}px;
    color: ${props => props.color};
  }
  
`
  • 当 js 里面的属性没有传入使,使用 attrs({ color:‘blue' }) 方法

classnames

Redux状态管理库

image.png

Redux 的三大原则

  • 单一数据源
    • 可以让整个应用程序的state变得方便维护、追踪、修改
  • State 是只读的
    • 唯一修改State的方法一定是派发action,不要再其他地方试图修改state
    • 这样确保了View或网络请求不能直接修改state,只能通过action来改
    • 这样可以保证所有的修改都能被集中化处理,并且按照严格的顺序来执行,不需要担心 race condition(竟态)问题
  • 使用纯函数来修改
    • 通过 reducer 将 旧state 和 actions 联系再一起,返回一个新的 State
    • 随着应用程序的复杂度增加,可以将 reducers 拆分成多个小的 reducers
    • 所有的 reducers 必须是纯函数

结构划分

如果把所有逻辑代码写在一起,那么当redux变得复杂时代码就非常难以维护,所以将redux进行拆解

  • store/index.js
  • store/reducer.js
  • store/actionCreators.js
  • store/constants.js

Store

  • 用来管理数据

Action

  • 所有数据的变化,都需要通过派发(dispatch)action来更新
  • action 是一个普通的 JS 对象,用来描述此次更新的 type 和 content

Reducer

  • 必须是一个纯函数
  • 将传入的 state 和 action 结合起来生成一个新的 state

案例 1

reducer

import { ADD_NUMBER } from './constant'


const initialState = {
  counter: 1
}
function reducer(state = initialState, action) {
  switch (action.type) {
    case ADD_NUMBER:
      return { ...state, counter: state.counter + action.num }
    default:
      return state
  }
}


export default reducer;

constant

export const ADD_NUMBER = "ADD_NUMBER" 

action_creators

import { ADD_NUMBER } from "./constant"
export function addNumberAction(number) {
  return { type: ADD_NUMBER, number }
}

index

import { createStore } from 'redux'
import reducer from './reducer'

export const store = createStore(reducer)

Home.jsx

import React, { PureComponent } from 'react'
import { store } from './store'
import { addNumberAction } from './store/action_creator'


export class Home extends PureComponent {
  constructor() {
    super()


    this.state = {
      counter: store.getState().counter
    }
  }


  componentDidMount() {
    store.subscribe(() => {
      const state = store.getState()
      this.setState({ counter: state.counter })
    })
  }


  componentWillUnmount() {}
  addNumber(number) {
    store.dispatch(addNumberAction(number))
  }


  render() {
    const { counter } = store.getState()
    return (
      <div>
        <h2>Home:</h2>
        <h2>当前计数:{counter}</h2>
        <button onClick={e => this.addNumber(1)}>+1</button>
        <button>+5</button>
      </div>
    )
  }
}


export default Home

react-redux库

  • 用来连接 react 和 redux 的一个库
  • 底层原理是使用:connect() 高阶组件实现

使用步骤(类组件中)

  1. 再 index.js 中将store全局引入,用Provider 包裹传递
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
  1. 在组件内使用
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'

export class about extends PureComponent {
  calcNumber(num) {
    this.props.addNumber(num)
  }

  render() {
    return (
      <div>
        <button onClick={e => this.calcNumber(88, true)}> +5</button>
      </div>
    )
  }
}

// 要将那些 store 里面的数据映射到 About 组件中
const mapStateToProps = state => ({
  counter: state.counter
})
// 内部其实是再做: <About {...this.props} {...obj}/>

// 该函数会映射到 props 中
const mapDistchToProps = dispatch => ({
  addNumber(num) {
    dispatch(this.addNumberAction(num))
  },
  subNumber() {}
})
// connext() 的返回值是一个高阶组件
export default connect(mapStateToProps, mapDistchToProps)(about)

案例:发送网络请求

  • 需要引入 redux-thunk 插件,才能 dispatch 函数
  • combineReducers() 组合多个 reducer

Category.jsx

import React, { PureComponent } from 'react'
import axios from 'axios'
import { connect } from 'react-redux'
import { fetchHomeMultidataAction } from '../store/action_creator'


export class category extends PureComponent {
  componentDidMount() {
    this.props.fetchHomeMultidata()
  }
  render() {
    return (
      <div>
        <h2>category page: </h2>
      </div>
    )
  }
}


const mapDispatchToProps = dispatch => ({
  fetchHomeMultidata() {
    dispatch(fetchHomeMultidataAction())
  }
})


export default connect(null, mapDispatchToProps)(category)

action.js

import { ADD_NUMBER, CHANGE_BANNERS, CHANGE_RECOMMENDS } from "./constant"
import axios from 'axios'
export function addNumberAction(number) {
  return { type: ADD_NUMBER, number }
}


export const changeBannersAction = (banners) => ({
  type: CHANGE_BANNERS, banners
})


export const changeRecommendsAction = (recommends) => ({
  type: CHANGE_RECOMMENDS, recommends
})


export const fetchHomeMultidataAction = () => {
  return function (dispatch, getState) {
    // 网络请求
    axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      dispatch(changeBannersAction(banners))
      dispatch(changeRecommendsAction(recommends))
    })
  }
}

index.js

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'

export const store = createStore(reducer, applyMiddleware(thunk))

redux Tookkit

  • 安装库:npm install @reduxjs/toolkit react-redux
  • 核心API
    • configureStore:包装 createStore 以提供简化的配置选项和良好的默认值。可以自动组合 slice reducer,添加你提供的任何 Redux 中间件,默认包含 redux-thunk
    • createSlice:接受 reducer 函数的对象、切片名称和初始状态值,并自动生成切片 reducer,并带有相应 actions
      • name:名称
      • initialState:初始化的数据
      • reducers:是一个对象,里面可以写函数
    • createAsyncThunk:接收一个动作类型字符串和一个返回承诺的函数

案例:减法

Profile.jsx

import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { subNumber } from '../store/features/counter'
export class Profile extends PureComponent {
  subNumber(num) {
    this.props.subNumber(num)
  }
  render() {
    const { counter } = this.props
    return (
      <div>
        <h2>Profile Counter: {counter}</h2>
        <button onClick={e => this.subNumber(5)}>-5</button>
        <button onClick={e => this.subNumber(8)}>-8</button>
      </div>
    )
  }
}


const mapStateToProps = state => ({
  counter: state.counter.counter
})
const mapDistchToProps = dispatch => ({
  subNumber(num) {
    dispatch(subNumber(num))
  }
})
export default connect(mapStateToProps, mapDistchToProps)(Profile)

counter.js

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: "counter",
  initialState: {
    counter: 888
  },
  reducers: {
    // 相当于每一个 case 语句,获取 action,修改 state
    subNumber(state, { payload }) {
      state.counter = state.counter - payload
    }
  }
})


export const { addNumber, subNumber } = counterSlice.actions
export default counterSlice.reducer

index.js

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter'

const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})


export default store

React-Route路由

  • 安装 npm install react-router-dom,如果安装react-router 会包含native相关

React-Route的基本使用

路由映射配置

  • Routes:包裹所有的 Route,在其中匹配一个路由
    • Router5.x 使用的是 Switch
  • Route:用于路径匹配
    • path 属性:用于设置匹配到的路径
    • element属性:设置匹配到路径后,渲染的组件
      • Router5.x 使用的是 component 属性
    • exact:精确匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
      • Router6.x 不再支持该属性
<div className="content">
  <Routes>
    <Route path="/home" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Routes>
</div>

路由配置和跳转

Link和NavLink

  • 通常路径的跳转是使用Link组件,最终会渲染成 a 元素
  • NavLink 是在 Link 的基础之上加了一些样式属性
  • to 属性:Link 中最重要的属性,用于设置跳转路径
  • replace属性:不保留历史记录
<div className="nav">
  <Link to="/home">首页</Link>
  <Link to="/about">关于</Link>
</div>

NavLink

  • style :需要传入一个函数
  • style={({ isActive }) => ({ color: isActive ? 'red' : '' })}
  • className:传入函数,函数接受一个对象,包括 isActive 属性
<NavLink to="/home" style={({ isActive }) => ({ color: isActive ? 'red' : '' })}>
  首页
</NavLink>
<NavLink to="/about">关于</NavLink>

Navigate跳转标签

<div>
  <h1>Login: page</h1>
  {!isLogin ? <button onClick={e => this.login()}>登录</button> : <Navigate to="/home" />}
</div>

<Routes>
  <Route path="/" element={<Navigate to="/home" />} />
    <Route path="/home" element={<Home />} />
      <Route path="/about" element={<About />} />
        <Route path="/login" element={<Login />} />
        </Routes>

NotFound 配置

  • <Route path="*" element={} />

路由嵌套

App.js

<Routes>
  <Route path="/" element={<Navigate to="/home" />} />
    <Route path="/home" element={<Home />}>
      <Route path="/home/recommend" element={<HomeRecommend />}></Route>
      <Route path="/home/ranking" element={<HomeRanking />}></Route>
    </Route>
    <Route path="/about" element={<About />} />
      <Route path="/login" element={<Login />} />
        <Route path="*" element={<NotFound />} />
        </Routes>

Home.jsx

import React, { PureComponent } from 'react'
import { Link, Outlet } from 'react-router-dom'
export class Home extends PureComponent {
  render() {
    return (
      <div>
        <h1>Home page</h1>
        <div className="home-nav">
          <Link to="/home/recommend">推荐</Link>
          <Link to="/home/ranking">排行榜</Link>
        </div>


        <Outlet />
      </div>
    )
  }
}


export default Home

HomeRecommend.jsx

import React, { PureComponent } from 'react'

export class HomeRecommend extends PureComponent {
  render() {
    return (
      <div>
        <h2>Banner</h2>
        <h2>歌单列表</h2>
      </div>
    )
  }
}


export default HomeRecommend

类组件使用 路由hook

  • 使用高阶组件进行封装
import { useNavigate } from "react-router-dom"
// 高阶组件:函数
export function withRouter(WrapperComponent) {
  return function (props) {
    const navigate = useNavigate()
    const router = { navigate }
    return <WrapperComponent {...props} router={router} />
  }
}

Router的两种模式

BrowserRouter

  • 使用 history 模式

HashRouter

  • 使用 hash 模式

Router 的参数传递

params 传递(/user/:id)

  • 定义:<Route path="/detail/:id" element={} />
  • detail接收:通过 useParams() 方法

query 传递(/user?username=1&age=18)

  • 使用 useLocation()

router 配置文件

  • 定义相关 router/index.js 文件
  • 然后再路由出口使用 useRoutes(router) 即可,{useRoutes(router)}

router.js

import Home from '../pages/Home'
import About from '../pages/About'
import Login from '../pages/Login'
import NotFound from '../pages/NotFound'
import HomeRecommend from '../pages/HomeRecommend'
import HomeRanking from '../pages/HomeRanking'
import Category from '../pages/Category'
import HomeSongMenu from '../pages/HomeSongMenu'
import Order from '../pages/Order'
import Detail from '../pages/Detail'
import User from '../pages/User'
import { Navigate } from 'react-router-dom'


const router = [
  {
    path: "/",
    element: <Navigate to="/home" />
  },
  {
    path: "/home",
    element: <Home />,
    children: [
      {
        path: '/home',
        element: <Navigate to="/home/recommend" />
      },
      {
        path: '/home/recommend',
        element: <HomeRecommend />
      },
      {
        path: '/home/ranking',
        element: <HomeRanking />
      },
      {
        path: '/home/songmenu',
        element: <HomeSongMenu />
      },
    ]
  },
  {
    path: '/about',
    element: <About />
  },
  {
    path: "/login",
    element: <Login />
  },
  {
    path: "/category",
    element: <Category />
  },
  {
    path: '/order',
    element: <Order />
  },
  {
    path: '/user',
    element: <User />
  },
  {
    path: '/detail',
    element: <Detail />
  },
  {
    path: '*',
    element: <NotFound />
  }
]


export default router

App.jsx

import React, { PureComponent } from 'react'
import { Routes, Route, Link, NavLink, Navigate, useRoutes } from 'react-router-dom'
import router from './router'

export function App() {
  return (
    <div>
      <div className="app">
        <div className="header">
          <span>header</span>
          <div className="nav">
            <NavLink to="/home" style={({ isActive }) => ({ color: isActive ? 'red' : '' })}>
              首页
            </NavLink>
            <NavLink to="/about">关于</NavLink>
            <NavLink to="/login">登录</NavLink>
            <Link to="/user?name=why&age=19">用户</Link>
            <button onClick={e => this.navigateTo('/category')}>分类</button>
            <span>订单</span>
          </div>
        </div>
        <div className="content">
          {useRoutes(router)}
        </div>
        <div className="footer">footer</div>
      </div>
    </div>
  )
}


export default App

router 懒加载

  • 使用 React.lazy(() => import(地址))
  • React.lazy(() => import('../page/user'))

Suspense 组件

  • 配置页面 loading
root.render(
  <HashRouter>
    <Suspense fallback={<h3>loading</h3>}>
      <App />
    </Suspense>
  </HashRouter>
);

Hooks

  • 普通函数不能使用 Hook
  • 再自定义 hook 中,需要用到 hook,必须要以 use 开头
  • 为什么要以 use 开头,而不是 create 开头?
    • create 可能不是很准确,因为 state 只在组件首次渲染的时候被创建
    • 这也是 hook 的名字以 use 开头的原因

为什么需要 Hook

  • Hook是 React 16.8 的新增特性,它可以让我们在不编写lass的情况下使用state以及其他的React特性(比如生命周期)
  • 我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
  • class组件可以定义自己的state,用来保存组件自己内部的状态
    • 函数式组件不可以,因为函数每次调用都会产生新的临时变量
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
    • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次
    • 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求:

useState

  • useState 来自于 react,需要从 react 导入,是一个 hook
  • 参数:初始化值,不设置为 undefined
  • 返回值:数组,包含两个元素
    • 元素一:当前状态的值
    • 元素二:设置状态值的函数,类似于(this.setState)

useEffect

  • 可以完成类似于class中生命周期的功能
  • 类似于网络请求、手动更新DOM、一些事件的监听,都可以用 useEffect
  • 通过该 hook,可以告诉 React 需要再渲染后执行某些操作
  • useEffect要求我们传入一个回调函数,当React执行完恒心DOM操作之后,就会回调
  • 默认情况下,无论是第一次渲染,还是每次更新之后,都会执行该回调
  • 为什么要在 effect 中返回一个函数?
    • 这是effect 可选的清除机制。每个 effect 都可以返回一个清除函数
    • 可以将添加和移除订阅的逻辑放在一起
    • 都属于 effect 的一部分
    • React 会在组件更新和卸载时执行清除操作

如何清除 Effect(副作用)

  • 在 class 组件中,一般在 componentWillUnMount 中执行
  • 在函数式组件中,通过在 useEffect() 函数的返回函数中来实现
  • 用来代替 class 中的 componentWillUnMount 生命周期
useEffect(() => {
  console.log('监听')
  return () => {
    console.log('取消 redux')
  }
})

Effect 性能优化

  • 某些代码我们只希望执行一次即可
  • 另外,多次执行也会导致一定的性能问题
  • 我们如何决定useEffect 在什么时候应该执行和不执行呢?
    • useEffect有两个参数:
    • 参数一:执行的回调函数
    • 参数二:该 useEffect 在那些 state 发生变化时,才重新执行
  • 如果一个函数不希望依赖任何的内容,就可以传入一个 空数组[]
  • 如果受某些变量影响,例如:[message],那么当message发生变化,就会执行

useContext

  • 避免了 class 组件中 context 的繁琐用法
  • 当 context 发生变化,组件自动重新渲染
  • 使用步骤如下:
    1. 首先在 index.js 中引入 context,使用 context 对 进行包裹
root.render(
  <UserContext.Provider value={{ name: 'why', level: 99 }}>
    <ThemeContext.Provider value={{ color: 'red', size: 30 }}>
      <App />
    </ThemeContext.Provider>
  </UserContext.Provider>
);
  1. 然后在需要使用 context 的 jsx 中,引入 context 和 useConext(),拿到返回值直接使用
import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from './context'
const App = memo(() => {
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)
  return (
    <div>
      <h2>
        User: {user.name}-{user.level}
      </h2>
      <h2 style={{ color: theme.color, fontSize: theme.size }}>Theme</h2>
    </div>
  )
})

export default App

useReducer

  • 仅仅是一种 useState 的代替方案
  • 如果某种场景下,state 的处理逻辑比较复杂,可以通过 useReducer 进行拆分
import React, { memo, useReducer, useState } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, counter: state.counter + 1 }
    case 'decrement':
      return { ...state, counter: state.counter - 1 }
    default:
      return state
  }
}
const App = memo(() => {
  const [state, dispatch] = useReducer(reducer, { counter: 0 })


  return (
    <div>
      <h2>当前计数:{state.counter}</h2>
      <button onClick={e => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={e => dispatch({ type: 'decrement' })}>-1</button>
    </div>
  )
})


export default App

useCallback

  • 用来实现性能优化
  • 有闭包陷阱
  • 如何进行性能优化呢?
    • useCallback会返回一个函数的 memoized(记忆的)值
    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • 性能优化的点
    • 当需要将一个函数传递给子组件时,最好使用useCallback进行优化,将优化后的函数传递给子组件
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'

const HYIncrement = memo(props => {
  const { increment } = props
  return <button onClick={increment}>increment + 1</button>
})

const App = memo(() => {
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState('hello')

  // 闭包陷阱 useCallback
  // 进一步优化,当count发生变化时,也使用同一个函数
  // 做法一:将 count 从数组中移出,缺点是闭包陷阱
  // 做法二:useRef,在组件多次渲染时,返回的值是同一个
  const countRef = useRef()
  countRef.current = count
  const increment = useCallback(function foo() {
    console.log('increment')
    setCount(countRef.current + 1)
  }, [])
  // const increment = useCallback(
  //   function foo() {
  //     console.log('increment')
  //     setCount(count + 1)
  //   },
  //   [count]
  // )
  return (
    <div>
      <h2>计数:{count}</h2>
      <button onClick={increment}>+1</button>
      <HYIncrement increment={increment} />
      <h2>message: {message}</h2>
      <button onClick={e => setMessage(Math.random())}>修改message</button>
    </div>
  )
})

export default App

useMemo

  • 对返回结果做优化,而 useCallback对返回函数做优化
import React, { memo, useMemo, useState } from 'react'


function calcNumTotal(num) {
  console.log('被调哟')
  let total = 0
  for (let i = 0; i < num; i++) {
    total += i
  }
  return total
}
const App = memo(() => {
  const [count, setCount] = useState(0)


  let result = useMemo(() => {
    return calcNumTotal(count * 2)
  }, [count])


  return (
    <div>
      <h2>计算结果:{result}</h2>
      <h2>计数器:{count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
})


export default App

useRef

  • 返回一个 ref 对象,返回的 ref 在组件的整个生命周期保持不变
  • 可以解决闭包陷阱
import React, { memo, useCallback, useRef, useState } from 'react'

let obj = null
const App = memo(() => {
  const [count, setCount] = useState(0)
  const nameRef = useRef()
  console.log(nameRef === obj)
  obj = nameRef

  // 通过 useRef 解决闭包陷阱
  const increment = useCallback(() => {
    setCount(count + 1)
  }, [])

  return (
    <div>
      <h2>Hello World : {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
})

export default App

useLayoutEffect

  • 看起来和 useEffect 相似
  • useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 更新
  • useLayoutEffect 会在渲染的内容更新到 DOM 之前执行,会阻塞 DOM 更新
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'


const App = memo(() => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    console.log('useEffect')
  }, [])


  useLayoutEffect(() => {
    console.log('useLayoutEffect')
  })
  return (
    <div>
      <h2>count : {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
})


export default App

useImperativeHandle

  • 用来限制子组件对父组件的操作权限
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react'


const HelloWorld = memo(
  forwardRef((props, ref) => {
    // 子组件对父组件传入的 ref 进行处理
    useImperativeHandle(ref, () => {
      return {
        focus() {
          console.log('focus')
          ref.current.focus()
        }
      }
    })
    return <input type="text" ref={ref} />
  })
)


const App = memo(() => {
  const titleRef = useRef()
  const inputRef = useRef()


  function handleDom() {
    inputRef.current.focus()
    inputRef.current.value = ''
  }
  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <HelloWorld ref={inputRef} />
      <button onClick={handleDom}>DOM操作</button>
    </div>
  )
})


export default App

useTransition

  • 返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数
  • 告诉 react 对于某部分任务的更新优先级较低,可以稍后进行更新
import React, { memo, useState, useTransition } from 'react'
import namesArray from './namesArray'


const App = memo(() => {
  const [showNames, setShowNames] = useState(namesArray)
  const [pending, setTransition] = useTransition()


  function valueChangeHandle(event) {
    setTransition(() => {
      const keyword = event.target.value
      const filterShowNames = showNames.filter(item => item.includes(keyword))
      setShowNames(filterShowNames)
    })
  }


  return (
    <div>
      <input type="text" onInput={valueChangeHandle} />
      <h2>用户名列表:{pending && <span>正在加载</span>}</h2>
      <ul>
        {showNames.map((item, index) => {
      return <li key={index}>{item}</li>
    })}
      </ul>
    </div>
  )
})


export default App

useDeferredValue

  • 接受一个值,并返回该值的新副本,该副本推迟到更紧急地更新之后
  • 也就是更新延迟,和 useTransition 一样

自定义 Hook

  • 必须用 use 开头
  • 本质上只是一种函数代码逻辑的抽取,严格意思上来说,并不算 React 的特性

案例:useLogLife

function useLogLife(cName) {
  useEffect(() => {
    console.log(cName + '组件创建')
    return () => {
      console.log(cName + '组件销毁')
    }
  }, [])
}

案例:useUserToken

// useUserToken.js
import { useContext } from "react";
import { UserContext, TokenContext } from "../context";

function useUserToken() {
  const user = useContext(UserContext)
  const token = useContext(TokenContext)


  return [user, token]
}


export default useUserToken

// App.jsx
import React, { memo, useContext, useState } from 'react'
import { useUserToken } from './hooks'

const Home = memo(() => {
  const [user, token] = useUserToken()
  return <h1>Home Page - {token}</h1>
})
const About = memo(() => {
  return <h1>About Page</h1>
})

const App = memo(() => {
  const [isShow, setIsShow] = useState(true)
  return (
    <div>
      <h1>App Root Component</h1>
      <button onClick={e => setIsShow(!isShow)}>切换</button>
      <Home />
      <About />
    </div>
  )
})

export default App

// hooks/index.js
import useUserToken from "./useUserToken";

export { useUserToken }

案例:useScrollPosition

// useScrollPosition.js
import { useEffect, useState } from "react"

function useScrollPosition() {
  const [scrollX, setScrollX] = useState(0)
  const [scrollY, setScrollY] = useState(0)
  useEffect(() => {
    function handleScroll() {
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
      console.log(window.scrollX, window.scrollY)
    }
    window.addEventListener('scroll', handleScroll)
    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])


  return [scrollX, scrollY]
}

export { useScrollPosition };

// App.jsx
import React, { memo, useContext, useEffect, useState } from 'react'
import './style.css'
import { useScrollPosition } from './hooks/useScrollPosition'

const Home = memo(() => {
  const [scrollX, scrollY] = useScrollPosition()
  return (
    <h1>
      Home Page - {scrollX} - {scrollY}
    </h1>
  )
})
const About = memo(() => {
  const [scrollX, scrollY] = useScrollPosition()

  return (
    <h1>
      About Page - {scrollX} - {scrollY}
    </h1>
  )
})

const App = memo(() => {
  return (
    <div className="app">
      <h1>App Root Component</h1>
      <Home />
      <About />
    </div>
  )
})

export default App

案例:useLocalStorage

import { useEffect, useState } from "react"

function useLocalStorage(key) {
  const [data, setData] = useState(JSON.parse(localStorage.getItem(key)))
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data));
  }, [data])


  return [data, setData]
}

export default useLocalStorage

redux hooks

  • 在之前的 redux 开发中,为了让组件和 redux 结合,使用了 react-redux 的 connect
    • 但是这种方式必须使用高阶函数组合返回的高阶组件
    • 必须编写:mapStateToProps 和 mapDispatchToProps 映射的函数
  • 在 Redux7.1开始,提供了 hook 的方式,可以不需要再编写 connect 和对应的映射函数了
  • useSelector :类似于 mapStateToProps
    • 作用是将 state 映射到组件中
    • 会默认比较返回的两个对象是否相等
    • 参数一:将 state 映射到需要的数据中
    • 参数二:可以进行比较来决定是否组件重新渲染 shallowEqual
    • 使用 useSelector 相当于监听了整个 state,需要用shallowEqual
  • useDispatch 的作用是获取 dispatch 函数
  • useStore 获取当前的 store 对象
import React, { memo } from 'react'
import { useSelector, useDispatch, shallowEqual } from 'react-redux'
import { addNumberAction, subNumberAction } from './store/modules/counter'

const Home = memo(props => {
  const { message } = useSelector(
    state => ({
      message: state.counterReducer.message
    }),
    shallowEqual
  )
  console.log('Home render')

  return (
    <div>
      <h2>Home: {message}</h2>
    </div>
  )
})

const App = memo(props => {
  // 1. 使用 useSelector 将数据映射到组件中
  const { counter } = useSelector(state => ({
    counter: state.counterReducer.counter
  }))

  // 2.
  const dispatch = useDispatch()
  function addNumberHandle(num, isAdd = true) {
    if (isAdd) {
      dispatch(addNumberAction(num))
    } else {
      dispatch(subNumberAction(num))
    }
  }
  console.log('App render')
  return (
    <div>
      <h2>当前计数:{counter}</h2>
      <button onClick={e => addNumberHandle(1)}>+1</button>
      <button onClick={e => addNumberHandle(-6, false)}>-6</button>
      <hr />
      <Home />
    </div>
  )
})

export default App

  • 创建项目的方式:create-react-app
  • 项目配置:
    • 配置项目的 icon
    • 配置项目标题
    • 配置 jsconfig.json
  • 通过 craco 配置别名和 less 文件

image.png

面试题

this 指向问题

因为 经过 babel 转换的代码,是使用严格模式,而在严格模式里面,this 为 undefined

  • 可以利用 handleClick = () => {}
  • 利用 bind(this)
  • 利用 () => this.btnClick()
class App extends React.Component {
  handleClick() {
    console.log(this);// undefined 
  }
  render() {
    return (
      	<button onClick={this.btnClick.bind(this)}>按钮</button>
              
        <button onClick={this.btnClick2}>按钮</button>
              
        <button onClick={() => this.btnClick2()}>按钮</button>
    )
  }
}

this 的四种绑定规则:

  1. 默认绑定:独立执行 foo()
  2. 隐式绑定:被一个对象执行 obj.foo() -> obj
  3. 显式绑定:call / apply / bind -> foo.call
  4. new 绑定: new Foo() -> 创建一个新对象,将它赋值给 this

React 调用规则

当你在 React 中创建一个对象时,实际上是通过
React.createElement("button", {onClick: this.btnClick }}这种方式来创建对象,
然后将 onClick 也赋值过去,内部通过定义变量来接收,步骤为:

  • const click = config.onClick
  • click()

这种方法属于上述的默认绑定this,所以this指向window,但是在 babel 环境下,使用了严格模式,所以 this 为 undefined
image.png

为什么使用 setState 函数

  • vue中是没有setState 函数来进行更新数据,而是他的底层会默认的将 template -> render() 进行转化( div -> h('div', {}, children)),vue有数据劫持
  • react 里面没有 template 而是直接有render函数 (div -> React.createElement('div', {}, children),React中没有数据劫持,而是通过 setState 进行判断
  • 如果上一个数据和当前一样,可以用 shouldComponentUpdate() 生命周期函数处理
  • setState 函数 是从 Component 组件中继承而来

setState 详细使用

  1. setState 基本用法
  • 原理:类似于创建一个新对象,传递了过去,是用 Object.assign(this.state, newState) 将两个对象合并
  • this.setState({ message: 'aaa' })
  1. setState 可以传入一个回调函数
  • 好处一:可以在回调函数中编写新的 state 的逻辑
  • 好处二:当前的回调函数,会将之前的 state 和 props 传递进来
  • this.setState(() => {return { message: '' }
  1. setState 在 React 的事件处理中,是异步的
  • 可以通过设置 this.setState(第一个,第二个) 的第二个参数拿到最新的数据
  • this.setState( {}, () => {})

为什么 setState 设计成异步的?

  • React18之前是同步代码(使用setTimeout),React之后是异步
  • 可以显著的提高性能
    • 如果每次调用 setState 都进行一次更新,那么 render() 被重复调用,页面重新渲染,效率低下
    • 最好的办法是获取到多个更新,之后进行批量更新
  • 如果同步更新了 state,但是还没有执行 render() 函数,那么 state 和 props 不能保持同步
    • state 和 props 如果不一致,会存在很多问题

什么是闭包陷阱?

useCallback 和 useMemo

爱彼迎项目

难点

封装按钮滚动

  • 记录索引:posIndex
  • newIndex = posIndex + 1
  • 根据 newIndex 索引获取到子元素 children[newIndex]
  • newEl.offsetLeft
    • 注意:设置定位
  • 修改 scrollContentRef.current.style.transform = translate(xx)
  • 设置最新索引
  • 判断左边按钮是否显示
  • 判断右边按钮是否显示

项目遇到的问题

useState 设置值

  • 因为 useState 只有组件第一次被渲染时,才是有效的
  • 当你后面从 infoData 里面取值然后赋值是,是无效的
  • 解决方法:当这个值存在的时候,再渲染组件
// 从 props 获取数据
const { infoData } = props


// 定义内部的 state
const initialName = Object.keys(infoData.dest_list ?? {})[0] ?? ''
console.log(initialName)
const [name, setName] = useState(initialName)
const tabNames = infoData.dest_address?.map(item => item.name)
const tabClickHandle = useCallback((index, name) => {
  setName(name)
}, [])

细节

防止页面一直刷新

  • 实现首页滚动时,记录当前滚动索引值使用 useRef
  • 可以使用 useRef 来代替 useState

外界传入的函数,最好用 useCallback包裹

  • 可以防止多次定义