React

lxf2023-02-17 01:50:59

React简介

React是什么?

React是用于构建用户界面的JavaScript库,换句话来说,React是一个将数据渲染为HTML视图的开源JavaScript库。

React是谁开发的?

Facebook公司开发,且开源。【2013年5月宣布开源】近十年沉淀,现被大厂广泛使用。

React的特点

  1. 采用组件模式声明式编码,提高开发效率及组件复用率。
  2. React Native中可以使用React语法进行移动端开发。
  3. 使用虚拟DOM+优秀的Diffing算法,尽量减少与真实DOM的交互。
  4. 速度快(UI渲染中,React通过虚拟DOM与真实DOM的比较,直接避免了不必要的真实DOM操作)
  5. 浏览器兼容、兼容性好(requireJS加载&打包),...

[ 运行这段原生JavaScript实现的代码之后,有两个人的数据,分别为张三、李四。如果我想再往这上面再添加一个王五,JS不会复用页面已有的张三、李四这两条数据,而是重新再生成3个DOM,等于说前两个没必要重新渲染,但还是被渲染了,一旦数据量大,性能方面就有问题了。]

  • 如果往100个人的后面新增一个人,前面100个人都没变,但最终渲染时,会重新生成101个DOM,你会觉得性能浪费吗?1000个人呢?10000个呢?

解释:

React没有直接操作真实DOM,而是通过操作虚拟DOM的方式来进行比较,最终才操作真实DOM。

上面的例子:100个人,第一次React会生成100个虚拟DOM通过比较,没有,则生成100个真实DOM;第二次追加一个人,React会进行虚拟DOM的比较,发现前面的100个React都有,那么最终只生成1个真实DOM。而不是像原生JS一样,追加一条数据,会全部重新渲染,又生成101个真实DOM,不能复用。这就是React的优点之一。

React与Vue的区别

1. Vue是自动挡,React是手动挡。Vue运行更快,学习也更快,唯一的缺点,Vue用久了之后,会发现自己的js基础代码不会写了。

2. React的社区比较活跃,提供了很多第三方的库供React开发人员去选择(像React的路由、状态管理库等都可以根据需要去选择;而Vue灵活性就会稍微差点,(Vue-Router、Vuex,Pinia))

3. Vue是响应式的,而React是手动setState(更新状态的)。

4. React也好上手但是难于写出优美的代码,对JS的要求更高。

5. 任何希望构建复杂应用程序并有可能在未来通过吸引更多开发人员来扩展应用程序的人,React 可能是首选。然而,Vue 是开发简单轻量级应用程序的最佳选择。

学习React之前你要掌握的JavaScript基础知识

  1. 判断this的指向
  2. class(类)
  3. ES6语法规范
  4. 包管理器(npm,yarn等)
  5. 原型、原型链
  6. 数组常用方法
  7. 模块化

使用create-react-app创建React应用

React脚手架

  1. xxx脚手架:用来帮助程序员快速创建一个基于xxx库的模板项目

React

  • (1)包含了所有需要的配置(语法检查、jsx编译、devServer...)
  • (2)下载好了所有相关的依赖
  • (3)可以直接运行一个简单的效果
  1. React提供了一个用于创建React项目的脚手架库:creat-react-app
  2. 项目的整体技术架构为:react + webpack + es6 + eslint
  3. 使用脚手架开发的项目的特点:模块化、组件化、工程化

创建项目并启动

  • 第一步,全局安装:npm install -g create-react-app
  • 第二步,切换到想创项目的目录,使用命令:create-react-app 项目名
  • 第三步,进入项目文件夹:cd 项目名
  • 第四步,启动项目:npm start 或 yarn start,看package.json文件具体命令

React

JSX(JavaScript XML)

前言(扯皮-可以不用知道)

  1. React的特点之一就是JSX,JSX是JavaScript的语法的扩展,使用JSX来开发UI内容。React开发不一定需要使用JSX,但使用JSX会非常便捷。

  2. 实际上JSXReact.createElement函数的语法糖,使用JSX需要使用Babel来将JSX转化成createElement函数调用(React 16版本)。

// JSX Version
   import React from 'react'
   function App() {
     return <h1>Hello,React</h1>
   }
   
// Compiled Version - 编译后
   import React from 'react'
   function App() {
     return React.createElement('h1', null, 'Hello,React')
   }

3.React 17 提供了一个全新的,重构过JSX 转换的版本。jsx语法不再化为createElement函数,而是内部通过react/jsx-runtimejsx函数生成虚拟对象

注意:

React 17如果引入React仅为了JSX存在你就可以从组件代码中删除React的导入。

// import React from 'react'
   function App() {
     return <h1>Hello World!</h1>
   }

了解:

React 17编译器从react/jsx-runtime导入了一个新的依赖项,它处理JSX转换

   import { jsx as _jsx } from 'react/jsx-runtime'
   function App() {
     return _jsx('h1', { children: 'Hello World!' })
   }

什么是JSX?

  1. JSXJavaScript的一种语法扩展,运用于React架构中,其格式像模板语言,但事实上完全是在JavaScript内部实现的。

  2. 元素是构成React应用的最小单位,JSX就是用来声明React当中的元素,React使用JSX来描述用户界面。

  3. JSX 是一种在 JavaScript 代码中添加 HTML 代码的方法。

React为什么推荐使用JSX?

官方:React不强制要求使用JSX,但大多数人发现,在JavaScript代码中将JSX和UI放在一起时,会在视觉上有辅助作用。它还可以使React显示更多有用的错误和警告信息。

个人:JSX语法糖允许前端开发者使用我们最熟悉的类HTML标签语法创建虚拟DOM,在降低学习成本的同时,也提升了研发效率与研发体验。

JSX的使用

函数式

function Demo() {
  // JSX
  return (
    <div>
      <p>函数式在这里写JSX</p>
    </div>
  )
}

类式

class Demo extends React.Component {
  render() {
    {/* JSX */}
    return {
      <div>
        <p>类式在这里写JSX</p>
      </div>
    }
  }
}

使用JSX创建React元素

const element = <h2>你好,React</h2>

在JSX中嵌入表达式

// 案例1
const name = 'JSX!'
const element = <h3>Hi, {name}</h3>
And
// 案例2 - 列表渲染
function Demo() {
  const userInfo = [
    { id: '001', name: 'React', age: 10 },
    { id: '002', name: 'Vue', age: 7 },
    { id: '003', name: 'Angular', age: 3 }
  ]
  return (
      <ul>
      // 在JSX语法中,你可以在大括号内放置任何有效的JavaScript表达式
        {
          userInfo.map(user => {
            const {id, name, age} = user
            return <li key={id}>name:{name},age:{age}</li>
          })
        }
      </ul>
  )
}

JSX条件渲染

// 案例3
function Demo() {
  function test(flag) {
    if (flag) {
      return <h1>I Like React</h1>
    }
    return <h1>I Do Not Like React</h1>
  }
  
  return (
    // <> </> 而不是 <div></div>,减少div层级嵌套
    <>
      {test(true)}
    </>
  )
}

JSX特定属性

// index.jsx
function FinalDemo() {
  const info = {
    img: 'http://www.xxx.com/xxx.png'
  }
  // 通过引号,将属性值指定为字符串字面量
  const el0 = <p tabIndex="9"></p>
  // 在属性值中插入一个JavaScript表达式
  const el1 = <img src={info.img} />
  // class -> className
  const el2 = <div className="container">xxx</div>
  return (
    <>
      {el0} - {el1} - {el2}
    </>
  )
}

JSX中绑定事件

function Demo() {
  const name = 'Are You Happy?'
  function changeName() {
    return `Hi, ${name}`
  }
  return (
    <div>
      <button onClick={changeName}>按钮1</button>
      or
      <button onClick={() => `Hi, ${name}`}>按钮2</button>
    </div>
  )
}

组件三大核心属性

1:state

人的状态影响着行为,组件的状态驱动着页面

需求:定义一个展示天气信息的组件
1. 默认展示天气炎热 或 凉爽
2. 点击文字切换天气

import React, { Component } from 'react'
export default class Weather extends Component {
 // 声明状态
  state = {
    isHot: true
  }

  changeWeather = () => {
  // 改变状态
    this.setState({ isHot: !this.state.isHot })
  }

 // 渲染函数 
  render () {
    const { isHot } = this.state
    return (
      <>
        { isHot ? '天气炎热' : '天气凉爽' }
        <button onClick={this.changeWeather}>改变天气</button>
      </>
    )
  }
}

React setState

用于更新状态(3种方式)

注意:

在类组件中,通过setState改变状态后立马获取该状态、获取到的值是上一次的。

import React, { Component } from 'react'

export default class Index extends Component {
  state = {
    num1: 0,
    num2: 0,
    num3: 0
  }

  try1() {
    const { num1 } = this.state
    // ①对象式的setState
    this.setState({ num1: num1 + 1 })
    console.log('num1:', num1)
  }
  try2 = () => {
    const { num2 } = this.state
    // ②对象式的setState
    this.setState({
      num2: num2 + 1
    }, () => {
      console.log('num2', num2)
      // 对象式 & 函数式 第二个参数 - 可选
      // 调用时机:它在状态更新、界面也更新后(调完render)之后
      //调这个回调,可获取到最新的count值
      console.log('this.state.num2:', this.state.num2)
    })
  }
  try3 = () => {
    const { num3 } = this.state
    // ③函数式的setState
    this.setState((state, props) => {
      console.log('state', state)
      console.log('props', props) // {}
      return { num3: num3 + 1 }
    })
    console.log('num3:', num3)
  }

  render () {
    return (
      <>
        <div>
          num1:{this.state.num1}
          <button onClick={this.try1.bind(this)}>尝试1</button>
        </div>
        <div>
          num2:{this.state.num2}
          <button onClick={this.try2}>尝试2</button>
        </div>
        <div>
          num3:{this.state.num3}
          <button onClick={this.try3}>尝试3</button>
        </div>
      </>
    )
  }
}

React 所以说,更新状态时不要在作用域内试图拿该状态去处理数据,容易出bug。

1.1:state理解

  1. state(React 状态)是组件对象最重要的属性,值是对象(可包含多个key-value的组合)
state = {
 key1: value1,
 key2: value2,
 ...
}
  1. React把组件看成是一个状态机(State Machines),通过更新组件的state来更新对应的页面显示(重新渲染组件)

1.2:需要注意

  1. 类式组件中render方法中的this为组件实例对象

  2. 类式组件自定义的方法中this为undefined,为什么?如何解决?

  • 根据JavaScript的语法规则,所有在类中定义的方法都默认开启局部严格模式。在严格模式下,所有指向window对象的this,都全部变更为undefined
import React, { Component } from 'react'
export default class Index extends Component{
  see() {
    console.log('see-this,' , this)
  }
  render(){
    console.log('render-this', this);
    return <button onClick={this.see}>查看</button>
  }
}

React 解释:

function A() {
  console.log(this)
}
function B() {
  'use strict'
  console.log(this)
}
A() // 输出 window 对象信息
B() // 输出 undefined

React 解决:

  • a)强制绑定this:通过在render中通过bind绑定this

React

  • b)箭头函数: func = () => {},通过作用域链查找获取this指向
  1. 状态数据,不能直接修改或更新
// wrong
this.state.isHot = false
// true
this.setState({ isHot: false })

2:(组件&)props

组件,从概念上类似于JavaScript函数。它接受任意的入参(即“props”),并返回用于描述页面展示内容的React元素。

这段代码会在页面上渲染 Hello,React-props

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>
}

const element = <Welcome name="React-props" />
ReactDOM.render(element, document.getElementsById('root'))

组合组件

我们可以创建一个可以多次渲染Welcome组件的App组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>
}

function App() {
  return (
    <>
      <Welcome name="React" />
      <Welcome name="Angular" />
      <Welcome name="Vue" />
    </>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

Props的只读性

`组件无论是使用函数声明还是通过class声明,都决不能修改自身的props。

开发中遇到的问题

import React, { Component } from 'react'

export default class Index extends Component {
  state = {
    message: '3秒前值还未出现'
  }
  componentDidMount () {
    this.changeMsg()
  }
  changeMsg = () => {
    /*
      1. 因为3秒后才更改了message的状态,所以在3秒之前父组件
      传递的message还是之前的,意味着子组件接收到的props不是
      最新的,3秒之后才能接收到最新的props。
    */
    setTimeout(() => {
      this.setState({ message: 'setTimeout-我出来了' })
    }, 3000)
  }
  render () {
    const { message } = this.state
    return (
      <>
        <Text msg={message} />
      </>
    )
  }
}

// 子组件
class Text extends Component {
  lookProps = () => {
    console.log('props', this.props.msg)
  }
  render () {
    return (
      <div>
        <p>父组件传来的props:{this.props.msg}</p>
        <button onClick={this.lookProps}>查看props</button>
      </div>
    )
  }
}

React

注意:

开发中调用接口,async-await搭配使用,【await】需要等到数据拿到再进行后续操作,在调用接口之后通过this.setState({})改变状态(如果该状态依赖接口数据)再传递给子组件,子组件拿不到最新的props。

伪代码:
   const { list } = await request(API.xxx, params)
   const data = list.filter(item => item.msg != '')
   this.setState({ message: data[0].msg })
   
解决办法:
   // 解决办法
   // 与运算,第一项为true才会执行第二项,所以一定会拿到list数据,
   // 解决了异步拿不到props数据
    {
      list.length > 0 && <Text1 msg={message} />
    }

3:Refs

简介:

1. 用于获取DOM节点React组件实例,(函数组件没有实例,这里指的是类组件)

创建方式:

① 字符串式声明ref

import React, { Component } from 'react'

export default class index extends Component {

  onClick = () => {
    console.log(this.refs)
  }
  render () {
    return (
      <div>
        <input ref="inputRef1" type="text" />
        <input ref="inputRef2" type="text" />
        <button onClick={this.onClick}>打印this.refs</button>
      </div>
    )
  }
}

打印结果:

React

将打印结果展开:

React 获取该input框的值:this.refs.inputRef1.value

同时,可以使用DOM上的API,比如:

import React, { Component } from 'react'

export default class index extends Component {

  onClick = () => {
    this.refs.inputRef1.focus()
  }
  render () {
    return (
      <div>
        <input ref="inputRef1" type="text" />
        <input ref="inputRef2" type="text" />
        <button onClick={this.onClick}>点击按钮聚焦第一个输入框</button>
      </div>
    )
  }
}

React 当然,这种字符串声明ref的方式已不再被推荐使用,将来可能会被废弃。 React

② 用回调的方式创建ref(推荐写法)

import React, { Component } from 'react'

export default class index extends Component {

  getValue = () => {
    console.log('第一个输入框的值:', this.inputRef1.value)
  }
  toFocus = () => {
    this.inputRef2.focus()
  }
  render () {
    return (
      <div>
      // e为input DOM本身
        <input ref={e => this.inputRef1 = e} type="text" />
        <input ref={e => this.inputRef2 = e} type="text" />
        <button onClick={this.getValue}>获取第一个输入框的值</button>
        <button onClick={this.toFocus}>聚焦第二个输入框</button>
      </div>
    )
  }
}

React

③ createRef - 类组件

import { Component, createRef } from 'react'

export default class index extends Component {

  constructor(props) {
    super(props)
    this.inputRef = createRef()
  }

  toFocus = () => {
    console.log(this.inputRef)
    this.inputRef.current.focus()
  }

  render () {
    return (
      <div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.toFocus}>点击按钮,打印并聚焦输入框</button>
      </div>
    )
  }
}

React

creatRef-函数组件

import React from "react"

export default function Index () {
  const inputRef = React.createRef()
  const toFocus = () => {
    console.log(inputRef)
    inputRef.current.focus()
  }
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={toFocus}>点击按钮,打印并聚焦输入框</button>
    </>
  )
}

效果和上面是一样的

④ 若将ref绑定在类组件上,是什么情况?

// 打印结果以注释形式显示
import { Component, createRef } from "react"

export default class Index extends Component {
  indexRef = createRef(null)
  // (2)点击按钮调用focus
  focus = () => {
    //(4)获取Child组件实例
    const childInstance = this.indexRef.current
    //(7)获取child组件input的DOM节点
    console.log(childInstance.childRef.current)
    //(8)聚焦input框
    childInstance.childRef.current.focus()
    //(9)清空input框内容
    childInstance.childRef.current.value = ''
  }
  render () {
    return (
      <div>
        {/* (3)组件绑定ref */}
        <Child ref={this.indexRef} />
        {/* (1)点击按钮后发生了什么? */}
        <button onClick={this.focus}>聚焦Child组件输入框</button>
      </div>
    )
  }
}

class Child extends Component {
  // (5)在Child组件中创建ref
  childRef = createRef(null)
  render () {
    //(6)ref绑定input框
    return <input ref={this.childRef} />
  }
}

效果:

React

⑤ 函数组件没有实例,但如果将ref绑定在函数组件上,可以吗?会是什么情况?

import { Component, createRef } from "react"
export default class Index extends Component {
  indexRef = createRef(null)
  focus = () => {
    // (3)函数组件没有实例,那这个取到的是什么?
    console.log('组件实例??', this.indexRef.current)
  }
  render () {
    return (
      <div>
        // (1)函数组件绑定ref
        <Child ref={this.indexRef} />
        // (2)点击按钮触发focus
        <button onClick={this.focus}>聚焦Child组件输入框</button>
      </div>
    )
  }
}

function Child () {
  const childRef = createRef(null)
  return <input ref={childRef} type="text" />
}

取到的是报错

React

翻译报错信息: React

解决:

报错信息告诉我们,不能在函数组件上绑定ref,如果实在要绑定,也行,需要借助forwardRef的力量。

forwordRef

简单理解:forwordRef是用来做ref转发的(将ref引用转发给某个组件)。

用法:forwordRef包裹着‘在父组件中绑定ref的函数组件’,并接收两个参数,①props ②ref

import { Component, createRef, forwardRef } from "react"
export default class Index extends Component {
  indexRef = createRef(null)
  focus = () => {
    const inputDOM = this.indexRef.current
    console.log('Child中input框的DOM节点', inputDOM)
    inputDOM.focus()
  }
  render () {
    return (
      <div>
        <Child ref={this.indexRef} haha="哈哈">Child组件</Child>
        <button onClick={this.focus}>聚焦Child组件输入框</button>
      </div>
    )
  }
}

// 将被绑定ref的函数组件通过forwardRef包裹,通过接收ref实现ref转发
const Child = forwardRef(function (props, ref) {
  console.log('props', props)
  return <input ref={ref} type="text" />
})

打印结果:

React

效果:

React

Redux

Redux 理解

Redux官方文档

1. 英文文档: redux.js.org/

2. 中文文档: www.redux.org.cn/

  1. Github: github.com/reactjs/red…

Redux是什么?

  1. Redux是一个专门用于做状态管理的JS库(不是react插件库)。
  2. Redux除了和React一起用外,还支持其他界面库。可以用在React、Angular、Vue等项目中,但基本上是与React配合使用。
  3. 体小精悍(只有2KB,包括依赖)
  4. 作用:集中式管理React应用中多个组件共享的状态

什么情况下需要使用Redux?

  1. 某个组件的状态,需要让其他组件可以随时拿到(共享)

如:A、B、C、D都要用到E的某个状态,E只需将需要共享的状态存到Redux中去,需要用到该状态的去Redux中去取就可以了,这也是Redux的作用。

React

如: A将状态存到Redux,B要用到A中存的状态,就去Redux中取

React

Redux原理

React 总览:

redux提供一个管理state的仓库(store),并且规定了存储在store中的状态只能通过reducer(函数)来更新,而reducer必须通过dispatch(action)来触发,action就是普通的JavaScript对象,它约定了执行的类型并且传递数据。使得state的变化是可以预测的,同样的步骤会得到同样的state。

详细:

  1. redux是做集中式状态管理的,其中,store是扛把子,是redux的核心。
  • store是管理state的仓库,使用它之前得先创建它。
import { legacy_createStore } from 'redux'
import reducer from '../reducers/reducer'

export default legacy_createStore(reducer)
  • 创建store得传入reducer,reducer是什么?是个函数
  • 作用:reducer接收到action(type&data),根据type类型和data数据进行对状态的处理并将处理后的数据存入store中。

React

  • action是什么?

React

React action是个一般对象

  • 目前创建store的方法由createStore改成legacy_createStore,名字变更小问题,源码如下。

React

React

React

可以看到,创建store的代码是137-383行,来看一下创建完store后,里面有什么?

  1. 获取存在redux仓库中状态的方法:store.getState()

React

  1. 更新存在redux仓库中状态的方法:store.dispatch(action)

React

Redux用法

演示代码目录:

React

最终效果:

React

Count下的index.jsx

React 可能有的疑问:

  1. 父组件传入store,父组件是什么?
  2. store 从父组件传入 里面的逻辑是什么?
  3. action 里是怎么写的?

让我们看一下App.jsx - 解决第一个疑问 React

再让我们看一下store.js - 解决第二个疑问 React

使用redux必须要先安装依赖,npm i redux 或 yarn add redux

可能有的疑问: 4. reducer是什么?

让我们再看一下reducer里写了什么 - 解决第四个疑问

React

让我们最后看一下action 里写了什么 - 解决第三个问题

React

捋一下使用redux的顺序:

1. 使用redux的目的是多个组件共享状态;那么你要先有组件index.jsx,才可以去使用redux

  • 在组件中使用redux,无非两件事,① 获取状态操作状态

  • 两个API,① store.getState()store.dispatch(action)

  • 目前我们缺的:① storeaction

  • 在redux原理图中,可以看到,store站在C位,没有它,redux根本跑不起来。

React

2. index.jsx中缺少store,我们从App.jsx中引入store传给需要的子组件。

  • 两种方式引入store,① 组件自身引入从App.jsx中传入,这里选择第二种,推荐写法。

3. 引入store,就需要创建store.js

  • 安装redux,从redux中引入legacy_createStore并传入reducer用于创建store,并导出store

  • 当在index.jsx组件中调用dispatch API并传入action时,redux会帮我们去通知reducer进行状态加工。而在创建store时传入reducer的目的是:store与reducer从此建立起联系

  • 问题来了?传入reducer,reducer是什么?

4. 需要reducer,那么就要编写reducer.js

  • 在组件调用dispatch(action)时,redux会帮我们去调reducer,通知reducer干活。

  • 为什么’这个js文件‘就是reducer?,就因为文件名是reducer.js?不,因为在创建store时传入一个函数,这个函数被称为reducer,它接收两个参数 ① 上一次的状态,没有就为undefined ② action对象。

  • 当然可以在store.js中直接编写一个函数并传入legacy_createStore中,但为了后期可维护性,我们选择引入的方式来创建reducer

5. 万事具备,只欠东风。缺少传入dispatch中的action对象

  • 哪里能用到action?答:dispatch(action)

  • 哪里能用到dispatch?答:组件中,dispatch我理解为它是操作redux中状态的方法。

  • action是什么?答:一般对象,{ type: '类型', payload: 值 }

  • 为什么action仅仅是个Object对象,也要单写个文件,使用时将其引入呢?答:后期好维护,并且一目了然,已经成为了一种规范。

  • 在组件中操作状态时可以以dispatch({...})这种形式存在吗?答:当然可以,但不推荐。我的意思是将action用单独一个文件统一管理,再在组件需要action时按需引入会更好。最后将引入的action方法交给dispatch方法。当然,该action方法最终也是返回一个一般对象。

在redux的使用中,会发现当按上面编写完代码时,页面效果没有呈现出来

原因:① redux不是Facebook公司出品,还有一定的小瑕疵,如:组件通过dispatch更改redux中状态时,控制台打印该状态的值,确实已经更改,但没有引发页面的更新。由此推断,redux中状态的更改不会引起页面的重渲染(render)

解决:在入口文件main.jsx中,调用store身上的subscribe方法,该方法用于监听redux中状态的改变,当状态改变,则执行回调,也就是手动调用了render方法,重新渲染App组件

React

React

引起render的三种方法:

  1. 类式组件 - 只要调用了setState,不管状态是否改变,都会调render。

  2. 函数式组件 - 通过useState解构出的setxxx改变状态,若该状态的值没发生更改,即使调了setxxx也不会重渲染,若状态发生改变,则调用render。(React通过Object.is来判断两个值是否相同,是浅比较)

  3. 父组件重新渲染会引起子组件的重渲染。

React Redux

React Redux 理解

简介

  1. ReactRedux是两个团体的作品。Facebook公司发现,很多程序员写React都喜欢用Redux来做集中式状态管理,Facebook本着简化编码的原则,直接官方出品 - React Redux
  2. ReduxReact Redux,它们的关系是:使用react-redux(插件库)可以让你更舒服的在React项目中去使用redux

React Redux原理图

React 解释:

  1. react-redux将组件分为两种。①容器组件UI组件
  2. 所有UI组件的外侧都要包裹着一个容器组件,他们是父子关系
  3. 在容器组件中才能和Redux打交道,可以随意使用Redux的API。
  4. UI组件通过容器组件来获取①redux里的状态操作状态的方法
  5. 容器组件UI组件的通信通过props

React Redux用法

准确的说是 Redux + React Redux

代码目录结构:

React

代码效果: React

代码实现:

先来看看Count组件 React 需要解释的点:

  1. 能取到Redux中状态操作状态方法的是容器组件而不是UI组件!!!
  2. 声明容器组件的方法是:从react-redux插件库中引入connect
  3. 安装:yarn add react-redux
  4. 声明容器组件:connect(a,b)(UI组件)
  • a和b都是函数,它们的作用是分别是①获取状态 ②操作状态
  • react-redux底层经过处理,能使得a函数接收到redux中的状态state
  • 同样,b函数能接收到dispatch方法,用于操作状态。
  1. redux中的状态: state={sum, list}
  2. 操作状态的方法:dispatch(action)

要想使用Redux,必须要有store,那么,我们先把组件看完把,Person组件

React

  1. nanoid - 每次获取都是全世界唯一的id
  2. 安装:yarn add nanoid
  3. 使用:const id = nanoid()
  4. 剩下的就是获取状态和操作状态

React

  1. 再次强调!connect()()用于创建容器组件

  2. state => ({ key: value}) 经react-redux处理,接收到redux中的state,返回一个对象,该对象的key用于UI组件通过props获取状态值,而value则是拿到存储在redux中的状态值

  3. 在Count组件中,第二个参数是function函数,但是我们安装了react-redux!!!React Redux官方出品能让我们在Redux的使用中更加的舒服!第二个参数可以是一个一般对象,里面是一组一组的key-valuekey是用于UI组件通过props获取操作状态的方法,而value则是action!!!

  4. 解释:UI组件中使用this.props.appendInfo({name,age,id:nanoid()})?

  • react-redux会触发dispatch方法同时传入action,在这里传入的是addPerson。addPerson是个函数,接收一个data并返回一个一般对象(appendInfo括号里的参数最终是传给addPerson这个函数的)
  1. React Redux内部会帮我们去调dispatch方法省去了接收dispatch再dispatch(action)

  2. 现在第二个参数直接就 { actionName1: action1, actioknName2: action2, ... }就可以了。

部分源码展示: 可以跳过,只是提供个查看的方向,不全~ React

React 先看ABCD,再看1234 React

|----------------------------------------------------------------|
|const mapStateToProps = initMapStateToProps(dispatch, options);|
|----------------------------------------------------------------|

initMapStateToProps = function initConstantSelector(dispatch)

最终initMapStateToProps(dispatch, options)返回了一个函数,是constantSelector

那么,mapStateToProps 就是 constantSelector 函数

调用constantSelector函数会返回一个对象,并且这个函数的原型上有个dependsOnOwnProps属性

这个对象由bindActionCreators(函数)返回

源码展示到此为止,太多,有兴趣的可以去看看。

================略过。。。看下面=================

(文件/代码编写顺序可以按自己习惯调整)需要操作Redux中的状态,就要dispatch(action),看下action.js

Count组件下的action

React

Person组件下的action

React

解释:

  1. UI组件通过调用容器组件定义的操作状态的方法来改变状态

  2. 具体表现为:this.props.xxx(如果括号里的是参数,则该参数最终传给action处理函数)

  3. 在容器组件中,connect第一个括号里的第二个参数{name: action对象或【action处理函数,处理完其实也就是一个action对象】},当你去调用它,this.props.name(),那么React Redux内部会去触发dispatch方法。

  4. 而当调用了dispatch(action)store会通知reducer进行状态加工,状态加工完后返回给store,UI组件若是需要,则通过容器组件store.getState()获取状态,UI组件再通过props获取,问题来了,store呢?reducer呢?

先来看下reducer

Count下的reducer

React

Person下的reducer

React

再来看下站在C位的store,store.js

React 解释:

  1. 旧版用createStore(reducer)创建store,而新版用legacy_createStore(reducer)创建store
  2. 因为Redux/React Redux + Redux的使命就是集中式状态管理,一个组件对应一个reducer,当有多个组件时,reducer的数量也随之增加,但是store-调度者只有一个,所以当有多个reducer时,就得去redux中引入combineReducers,并将这些reducer集中起来传入legacy_createStore中用于创建store,当需要某个reducer时,store就能通知'这个'reducer进行状态加工。
  3. 而传入combineReducers的是一个对象,该对象就是在Redux保存的总状态对象
  4. 对象中存放着一组一组的key-value,key代表着:当我们需要去redux中取状态时应该通过哪个字段去取;value则代表着:加工该状态(key)的reducer

具体使用:

React 5. import { legacy_createStore, combineReducers } from 'react-redux'可以吗?

React

react-redux帮我们做出的优化1:

解释:

  1. 纯Redux中,我们需要在入口文件中调用store.subscribe来监测Redux中状态的变化,当状态变化时,调用render方法重新渲染页面;
  2. 当我们安装了react-redux这个插件库并使用了容器组件,容器组件connect()()会帮我们监测Redux中状态的改变,若Redux中状态改变则重新渲染页面。图中×掉的部分就不用了。

React

react-redux帮我们做出的优化2:

解释: 1.在纯Redux中,我们需要在父组件App中将store传给子组件Count,因为Count要用到它(store.getState()、store.dispatch(action)),当组件个数逐渐变多时,难道我们也要一个一个的传入store吗?

React

react-redux帮我们做出了优化,从此引入index.js - 入口文件

React 解释:

  1. Provider:你整个应用里边但凡要用到store的容器组件,我都能给他传过去,换句话说,哪个容器组件不需要store?App里所有的容器组件都能收到store,只需要写一次即可。
  2. 容器组件左边连着UI组件,右边连着Redux。在容器组件中才能使用Redux相关的API,所以说这个store对于容器组件来说是必要的

React Hooks

简介:

什么是Hook?

  1. Hook是React 16.8(版本)的新增特性。它可以让你在不编写class的情况下使用state以及其他的React特性。
  2. Hook是一些可以让你在函数组件里“钩入”React state生命周期等特性的函数

Basic Hooks

  • useState
  • useEffect
  • useContext

useState Hook

用法:

import { useState } from 'react'
const [state, setState] = useState(initState)
const [apple, setApple] = useState(initApple)

解释:

  1. 调用useState函数会返回一个state,以及更新state的函数

  2. 初次渲染期间initState的值state的初始值

  3. setState/setApple函数用于更新state。它接受一个新的state值并将组件的一次重渲染加入队列。

  • 当调用setState/setApple时传入state值,React会对新旧值进行浅比较(Object.is(值1,值2)),若两个值相同,则不进行重渲染

  • 对象浅比较 -> Object.is({a:2}, {a:2}) 结果为false比较的不是对象的引用、该对象存储在内存中的地址,所以说setState传入一个对象,不管里面值是否相同都会重渲染

  1. 后续的重新渲染中,useState返回的第一个值将始终是更新后最新的state

注意:

  1. useState传入的参数,也可以是函数,该参数只在初次渲染期间有效后续渲染将被忽略作用是:当初始化值需要通过计算得到,则可以在useState Hook中传入一个函数用于计算初始值。如下:
import { useState } from 'react'
const [size, setSize] = useState(() => {
  // 经过计算,为size赋的初始值
  return undefined
})

代码:

React

效果:

React

useEffect Hook

简介:

Effect Hook可以让你在函数组件中执行副作用操作。

问题:

  1. 什么是副作用?

数据获取设置订阅手动更改React组件中的DOM

个人观点:

只要知道Effect Hook运行的时机, 剩下的就根据不同的业务场景去选择是否要用它。

提示:

  1. 如果你熟悉React class生命周期函数,你可以把useEffect Hook看做componentDidMountcomponentDidUpdatecomponentWillUnmount这三个函数的组合。
  2. React会保存你传递的函数(我们称它为”effect”)。

常见用法:

import { useEffect } from 'react'

1. useEffect(() => {})
2. useEffect(() => {}, [])
3. useEffect(() => {}, [依赖项1,依赖项2,...])
4. useEffect(() => {
     return () => {}
   }, [])
5. useEffect(() => {
     return () => {}
   }, [依赖项1,依赖项2,...])

第一种情况:useEffect(() => {})

代码:

React

效果:

React

解释:

useEffect(() => {
  console.log('xxx')
})

这种情况下,Effect Hook执行的时机是:

组件挂载/渲染完成之后

组件中只要有状态改变

组件中传入的任意一个props的值改变

第二种情况:useEffect(() => {}, [])

代码:

React

效果:

React

解释:

useEffect(() =>console.log('xxx')
}, [])

这种情况下,Effect Hook执行的时机是:

组件挂载完成之后

第三种情况:useEffect(() => {}, [依赖项1,依赖项2,...])

代码:

React

效果:

React

解释:

useEffect(() => console.log('xxx'), [A])

这种情况下,Effect Hook执行的时机是:

组件挂载完成之后

依赖项值发生变化(A状态发生改变时)

第四种情况:

useEffect(() => {
  return () => {}
}, [])

代码:

React

效果:

React

解释:

这种情况下,Effect Hook执行的时机是:

组件挂载完成之后

在组件被卸载和销毁之前立即调用

第五种情况:useEffect(() => { return () => {} }, [依赖项])

代码:

React

index.js - 入口文件

React

效果:

React

解释:

  1. 这种情况下,Effect Hook执行的时机是:

组件挂载完成之后

依赖项值更新

组件将要卸载

  1. useEffect(effect),React会在执行当前effect之前对上一个effect进行清除。

useContext Hook

简介:

Context提供了一个无需为每层组件手动添加props,就能在组件树间进行数据传递的方法。

问题:

  1. 何时使用Context?

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据。换句话说,Context的出现避免了子孙组件需要爷爷组件的数据而从爷爷组件一层一层靠props传递的问题。

个人观点:

  1. 用于组件通信
  2. 主要的应用场景:多个不同层级的组件需要访问相同的数据。

Context API

  1. React.createContext
import React from 'react'
const MyContext = React.creactContext(defaultValue)
或
import { createContext } from 'react'
const MyContext = creactContext(defaultValue)
  • 创建一个Context对象(创建上下文环境)。当React渲染一个订阅了这个Context对象的组件(React渲染了一个使用useContext(Context对象)的组件),这个组件会从组件树中离自身最近且匹配的Provider中读取到当前的context值

  • 只有当组件(这个组件指的是要传递数据的组件<MyContext.Provider value={值}>组件(再嵌套组件)</MyContext.Provider>)所处的树中没有匹配到Provider时(没有匹配到Context对象上的Provider组件),其defaultValue参数才会生效

  1. Context.Provider
<MyContext.Provider value={/* 某个值 */}>
</MyContext.Provider>
  • 每个Context对象(MyContext)都会返回一个Provider(React组件),具体如上。

  • 消费组件只要被这个Context对象上的Provider组件包裹着,那么,这些被包裹的组件或与这些被包裹组件能建立起联系的组件(父子关系等)。 -> 都能以某种方式取到这个‘传递的值’。

  • Provider组件接收一个value属性,里面的值传递给消费组件

  • Provider组件消费组件的关系是:一对多

  • Provider组件可以嵌套使用,里层的会覆盖外层的数据。

  • 当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染

  1. useContext
const value = useContext(MyContext)
  • useContext接收一个context对象(React.createContext的返回值)并返回该context的当前值。当前context的值上层组件中距离当前组件最近的<MyContext.Provider>的value属性决定。换句话说,创建Context对象时都会有Provider组件,当你的组件需要某个Provider组件提供的value值,那么一定是最近的优先

  • 当组件上层最近的<MyContext.Provider>更新时,该Hook(useContext)会触发重渲染并使用最新传递给MyContext provider的context value值换句话说,组件的取值来自于组件上层的MyContext provider的context value值,如果上层的值发生改变,是不是组件用于获取值的Hook(useContext)也该重新渲染拿到最新的值?

  • useContext的参数必须是context对象本身

代码1:

import { createContext, useContext } from 'react'
// 创建上下文环境,Context对象
const MyContext = createContext(null)
// 爷爷组件
function Father() {
  return (
    <>
     // 注:Context对象上有个Provider组件,用于组件通信
     <MyContext.Provider value={{name: '小明', age: 18}}>
       <Son />
     </MyContext.Provider>
    </>
  )
}
// 儿子组件
const Son = () => <GrandSon />
// 孙子组件
const GrandSon = () => {
  // useContext用于获取上层组件中最近的Provider提供的value属性里的值
  const { name, age } = useContext(MyContext)
  return <p>{name + '同学'} - {age}</p>
}

效果1:

React

代码片段:

import { createContext } from 'react'
const MyContext = createContext(defaultValue)

怎么才能使用到默认值?

  • 只有当组件所处的树中没有匹配到Provider时,其默认值defaultValue才会生效。
  • 建议将默认值defaultValueProvider组件上value属性的值设置成同类型不同值。

代码2:

React

效果2: React

Additional Hooks

  • useReducer
  • useCallback
  • useMemo
  • useImperativeHandle
  • useRef
  • useLayoutEffect
  • useDebugValue(测试钩子,没用)
  • useDeferredValue
  • useTransition
  • useId(用于服务端渲染,没啥用)

useReducer Hook

简介:

  1. 官方提供了两种状态管理的Hook,① useStateuseReducer

  2. useReducer是useState的替代方案。useReducer的工作流程几乎和Redux一致,但它不是Redux!

用法:

import { useReducer } from 'react'
const [state, dispatch] = useReducer(reducer, initState, initFunc)
  • useReducer函数接收三个参数,其中第三个是可选参数

  • 第一个参数:reducer,是个函数,在dispatch方法中传入action动作对象时触发,用于处理状态

  • 第二个参数:initState,是初始状态,传入useReducer中经过处理返回一个新的/当前的state

  • 第三个参数:initFunc,是个函数,该函数能接受到初始值 - initState,并将处理过后的初始值返回,用于加工初始状态

import { useReducer } from 'react'
const countReducer = () => {}
export default function Index () {
  const initCount = count => count + 2
  // 如果useReducer有第三个参数,必须在其函数return一个值,否则没有初始值
  const [count] = useReducer(countReducer, 0, initCount)
  // 页面显示的是2
  return <div>{count}</div>
}

下面,我们用代码来模拟一下useReducer的实现:

React 解释:

  1. const [state, updateState] = useState(initState)
  • 使用state hook,传入默认值并返回一个数组,数组第一个是值,第二个是改变值的方法。
  1. const dispatch = action => updateState(reducer(state, action))
  • 因为dispatch方法最终被useReducer返回,所以一定要在useReducer中声明dispatch。

  • dispatch方法传入action动作对象才能通知reducer去干活,去进行状态加工,所以dispatch的参数是action对象

  • 调用了dispatch方法就是 ①通知reducer进行状态加工状态若发生改变则进行重渲染。状态有无更新都要调用state hook返回的更新状态的方法:updateState,因为state hook返回的数组里的更新值的方法会对状态进行浅比较(通过Object.is),若状态改变则调render,若没有改变则不会触发render。

  • 不管最终有没有更新状态,updateState里都要有值,不然它拿什么更新?值从哪里来,靠reducer这个加工状态函数最终返回。

  • reducer接收两个参数①上一次的状态action对象,传入后经过reducer处理则返回一个加工后的状态,updateState接收到这个更新后的状态进行浅比较,若两次值不等则就去更新它。

  • 模拟useReducer结束

我们将模拟的useReducer引入到文件去使用,看是否奏效:

React

效果:

React

问题:

1. useReducer和useState的区别?

  • 个人理解:① useState一次只能声明一个状态多个状态一起更新就推荐使用useReducer一个状态进行不同的操作,推荐使用useReducer
  • 如果说某个state独立于其他的state,就没必要放到Hook里去。

2. 为什么useReducer不是/不能取代Redux?

  • 虽然 useReducer 及其 reducerRedux 工作方式的一部分,但它不是 Redux。useReducer 函数与它的 reducer 紧密耦合,这也适用于它的 dispatch 函数。我们只将 action 对象分派给该 reducer。而在 Redux 中,dispatch 函数将 action 对象发送到 store,store 将其分发给所有组合的 reducer 函数。您可以将 Redux 视为一个全局事件总线,它接收任何事件(动作)并根据动作的有效负载先前的状态将它们处理为新状态

性能优化(提前说明)

  1. useCallback -> 缓存函数
  2. useMemo -> 缓存值
  3. React.memo -> 缓存组件

useCallback Hook

简介:

import { useCallback } from 'react'
const Func = useCallback(fn, [依赖项,...])
  1. useCallback接收两个参数:① 需要被缓存的函数依赖项(可选)
  • useCallback用于缓存函数,如果useCallback没有第二个参数,使用该hook就没有意义,只要页面发生渲染,该函数还是会被重新生成,没有达到缓存的效果。

  • 如果useCallback的第二个参数为空数组,则说明它没有依赖项,它在页面初次渲染时就把该函数缓存下来。之后这个函数也无法再发生改变。

  • 如果useCallback的第二个参数存在且有依赖项,那么,页面初次渲染时会把这个函数缓存下来,当依赖项的值发生变化重新生成函数并将该函数重新缓存到内存中去。

  1. useCallback通常与React.memo一起使用。
import { memo } from 'react'
const List = memo(组件)
  • List是被缓存的组件,当memo包裹的组件所接收到的props发生变化,memo会重新缓存组件。

使用场景:

  1. 对于需要传递函数给子组件的场合,如果不使用useCallback,只要父组件发生重渲染,子组件就会重新渲染。
  • 即使子组件被React.memo包裹,当父组件发生渲染,子组件依然也会重新渲染,为什么呢?因为React.memo是通过浅比较,比较props的变化,当props发生变化,则重新渲染组件再将组件缓存。因为父组件传给子组件的是函数,所以当你没有用useCallback将此函数缓存起来时,你通过某种方式让父组件发生了渲染,那么父组件就会重新生成这个函数,造成的后果是props也发生了改变(重新生成function,引用、地址发生了改变),即子组件重新渲染。但如果父组件传给子组件的是被缓存的值,且子组件被React.memo包裹,那么,只要该值没发生变化,父组件无论怎么重渲染,子组件不会发生渲染(变化)!
  1. 在调用节流、防抖函数时
  • 案例:在输入框中,用户停止输入300毫秒后开始请求接口获取数据,就得用到防抖函数
  • bug出处:父组件的每一次渲染都会重新生成一个函数
  • 解决:useCallback

代码解释:

React 父组件给子组件传递函数,改变父组件的状态,观察子组件是否会被渲染

效果:

React

  • 改变父组件的状态,引起了不必要的渲染 - ① 子组件重新渲染父组件的函数也重新生成

第一步:将子组件用React.memo包裹,将组件缓存

React

效果:

React 问题:

  1. 将子组件用React.memo缓存,但改变父组件的状态,子组件依然被更新,为什么?

因为React.memo会对父组件传来的props进行浅比较,当两次的值不同时,就会重新渲染、缓存组件。

  1. 传递给子组件的是函数,为什么两次的值会不同,好像没改到函数把!?

因为父组件发生渲染时,会重新生成函数,React.memo对props是浅比较,两次函数的props的引用不同,所以导致了即使被React.memo包裹也会重新渲染的问题。

  1. 如何解决?

导致这个问题的主要原因在于:父组件渲染时又重新生成了一个function,新生成的function和原来的function在内存中地址不同、引用不同。如果我们能把该函数缓存下来,且仅在依赖项改变时再去重新缓存函数,那么,当依赖项没有发生改变时,子组件就不会因为父组件的渲染而渲染。由此推出新的hook-useCallback。

第二步,将传递给子组件的函数缓存下来

React

效果:

React

小栗子:

输入框输入用户名称,点击按钮添加用户

优化前:

React

代码:

React

问题:

问题描述:输入框内容的变化会导致子组件重新渲染 ,这在性能方面是个极大的缺陷,如果子组件的体系庞大,当你键入或删除某个字符时,可能会发生延迟的现象。

问题原因:①父组件的重渲染会引起子组件的渲染 - setValue父组件给子组件传递函数,新旧函数引用不同,对比不同,导致次次渲染

最终目的:点击添加用户按钮才触发子组件的渲染。

问题解决: ①React.memouseCallback

React 1. 缓存函数并添加依赖项,当函数中依赖项的值没有发生改变,则该函数就不会重新生成。

2. 缓存组件,当props没有改变,就不会触发子组件的渲染&重新缓存。

优化后:

React

useMemo Hook

简介:

React

从源码中可以看出,useMemo返回的是值,而useCallback返回的是函数。

  • useMemo hook用于性能优化,功能是缓存值
  • useMemo与useCallback可以互相转换(如下图),但不推荐,最好将这两个hook给区分开来。

React

React

用法:

import React from 'react'
const cacheValue = useMemo(() => 值,[依赖项,...])
  1. useMemo Hook根据第二个参数依赖项值的变化来决定是否执行第一个参数并缓存第一个参数所返回的值,最终将缓存的值赋值给cacheValue。
  2. useMemouseCallback都是用于性能优化,但不能滥用,用法类似前者是用于缓存值的,这个值可以是任意的。而后者是缓存函数的。虽然可以转换,但还是希望各司其职
  3. useMemo没有第二个参数,则使用useMemo没有意义,页面渲染,该值还是会重新生成,没有达到缓存的效果
  4. useMemo第二个参数为空数组,则代表没有依赖,页面首次渲染时将该值缓存,之后不再变化。
  5. useMemo第二个参数不为空有依赖项,则当依赖项的值改变时,React会重新执行第一个参数并将返回的值进行缓存。
  6. 传入useMemo的函数会在渲染期间执行。

使用场景:

一组账号密码对应着一个人的个人信息,默认将登录成功后的账号密码缓存,并将账号密码作为依赖项,当任一依赖项改变,切换个人信息。

useRef Hook

简介:

useRef一般是用于与DOM交互的,获取/操作(值)DOM。

用法:

import { useRef } from 'react'
const xxxRef = useRef(initValue)
// 伪代码
<input ref={xxxRef} />
  1. useRef返回一个可变的ref对象,该对象只有一个current属性,current属性的初始值为传入的参数initValue。

如: React

打印结果: React

  1. 更新ref对象上current属性的值不会触发重渲染(re-render)
  2. 返回的ref对象在组件的整个生命周期中保持不变

代码:(使用ref.current存值)

import { useState, useRef, useCallback } from "react"

export default function Index () {
  const [time, setTime] = useState(Date.now())
  const ref = useRef()
  const start = useCallback(() => {
    console.log('开始了...')
    // 存储setInterval返回的ID
    ref.current = setInterval(() => {
      setTime(Date.now())
    }, 500)
  }, [])
  function clear () {
    console.log('结束了...')
    clearInterval(ref.current)
  }
  return (
    <>
      <span>time: {time}</span>
      <button onClick={start}>开始</button>
      <button onClick={clear}>清除定时器</button>
    </>
  )
}

效果:

React

代码:(使用ref.current存DOM节点)

import { useRef } from "react"

export default function Index () {
  // 初始化一个空引用
  const inputRef = useRef(null)
  function toFocus () {
    console.log('inputRef', inputRef)
    console.log('DOM节点', inputRef.current)
    console.log('input值', inputRef.current.value)
    inputRef.current.focus()
  }
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={toFocus}>打印并聚焦</button>
    </>
  )
}

效果:

React

useImperativeHandle Hook

简介:

  1. useImperativeHandle Hook接收3个参数,第一个是ref(该ref用于确认与哪个组件建立起联系,ref由父组件传入),第二个是个回调函数,最终返回一个值,这个值是任意的,返回的值给‘提供ref的这个组件使用’,第三个是依赖项。
useImperativeHandle(ref, () => {
  return ...
}, [])
  • 依赖项改变,将会重新执行/调用第二个参数(回调函数)。依赖项改变的结果是:提供给父组件使用的值可能会发生改变。
  1. 使用useImperativeHandle Hook需要绑定ref,而ref只能从父级传入,所以该Hook需要与useRef Hook一起使用,当使用useRef创建ref并绑定在函数式组件上时,由于函数组件没有实例,当在父组件获取ref上的current属性时,控制台会报错,而报错信息推荐我们使用forwordRef来解决函数组件没有实例不可绑定ref的问题。所以说当我们去使用useImperativeHandle Hook时要与useRef、forwordRef配合使用。

React

  • React.forwordRef用于将父组件创建的ref引用关联到子组件中的任意元素上,也可以理解为子组件向父组件暴露DOM引用。

两个案例:

案例1:

效果图:

React

代码实现:

import { useRef, forwardRef, useImperativeHandle } from 'react'

export default function Index () {
  const inputRef = useRef()
  /*
    1. inputRef.current的值为useImperativeHandle第二个参数返回的值
    2. inputRef.current.userRef取到的也是ref
    3. 需要再取该ref的current属性才能取到该DOM节点
  */
  return (
    <>
      <div>
        <button onClick={() => {
          inputRef.current.userRef.current.focus()
        }}>聚焦输入框</button>
        <button onClick={() => {
          inputRef.current.passwordFocus()
        }}>聚焦密码</button>
      </div>
      // ref绑定在函数组件上
      <Input ref={inputRef} />
    </>
  )
}

// 函数组件没有实例,借助forwordRef转发ref解决问题
// props为父组件传入的除ref以外的值,ref为父级传入
const Input = forwardRef((props, ref) => {
  // 创建ref,分别绑定到用户名、密码输入框
  const userRef = useRef()
  const passwordRef = useRef()
  useImperativeHandle(ref, () => ({
    // 向提供‘ref’的组件提供值,该值是个对象
    userRef,
    passwordFocus: () => {
      passwordRef.current.focus()
    }
  }), [])
  return (
    <>
      <input ref={userRef} type="text" placeholder='用户名' />
      <input ref={passwordRef} type="text" placeholder='密码' />
    </>
  )
})

案例2:

效果图:

React

代码实现:

import { useState, 
        useRef, 
        forwardRef, 
        useImperativeHandle 
      } from 'react'
// 以两种形式声明样式
import style from './style'
import './style.css'

// 弹窗组件 - 子组件
// forwordRef接收的第一个参数为props,是个对象,直接解构
// props解构出的children:<Dialog>这里的内容为children</Dialog>
const Dialog = forwardRef(({ children }, ref) => {

  // 此处声明ref的目的是,获取DOM的位置&聚焦
  const xRef = useRef()
  const qxRef = useRef()
  const qdRef = useRef()
  useImperativeHandle(ref, () => {
    return {
      // 给父组件提供一个数组
      tips: [
        {
        // 索引0,第一个,获取关闭按钮的位置
          position: xRef.current.getBoundingClientRect(),
        // 聚焦关闭按钮
          action: () => xRef.current.focus(),
        // 根据关闭按钮的位置来显示文字
          context: '点击这里关闭弹窗'
        },
        {
          position: qxRef.current.getBoundingClientRect(),
          action: () => qxRef.current.focus(),
          context: '点击这里 取消操作并关闭弹窗'
        },
        {
          position: qdRef.current.getBoundingClientRect(),
          action: () => qdRef.current.focus(),
          context: '点击这里 确认操作并关闭弹窗'
        }
      ]
    }
  }, [])

  const xClick = () => {
    console.log(xRef.current.getBoundingClientRect())
  }

  return (
    <div style={style.container}>
      <div style={style.first}>
       <div>如何使用</div>
       <button ref={xRef} className="xbtn" onClick={xClick}>X</button>
      </div>
      {children}
      <div>
       <button ref={qxRef} className="qxbtn">取消</button>
       <button ref={qdRef} className="qdbtn">确定</button>
      </div>
    </div>
  )
})

// 父组件
export default function StartStudy () {
  const dialogRef = useRef()
  /*
     根据step来判断到底显示什么
     step = -1,显示开始学习按钮,此时什么操作都没有
     step = 0,取子组件数组第一个的内容
     以此类推
  */
  const [step, setStep] = useState(-1)

  const study = () => {
    // 点击开始学习按钮,(-1+1=0)聚焦关闭按钮,
    dialogRef.current.tips[step+1].action()
    // step设置为0
    setStep(step + 1)
  }
  return (
    <Dialog ref={dialogRef}>
      {
      // step = -1 显示开始学习按钮,显示在Dialog组件的children处
        step < 0 &&
        <button style={style.second} onClick={study}>开始学习</button>
      }
      {
      // step = 0/1/2 分别操作 关闭/取消/确定按钮
        step >= 0 &&
       <div style={{
        position: 'absolute',
        top: `${dialogRef.current.tips[step].position.top - 22}px`,
        left: `${dialogRef.current.tips[step].position.left - 70}px`,
        padding: '2px',
        backgroundColor: '#ffeb3b',
       }}>
          {dialogRef.current.tips[step].context}
          <button
            style={{ marginLeft: '8px' }}
            onClick={() => {
              // 点击‘知道了’判断step数值,如果大于数组长度-1,将值设为-1
              // step = -1,显示 开始学习按钮
              if (step > dialogRef.current.tips.lenght - 1) {
                setStep(-1)
              } else {
              // step的值符合要求就进入下一步
                setStep(prveStep => {
                  console.log('prveStep', prveStep)
                  if (prveStep >= 2) {
                    return -1
                  }
                  dialogRef.current.tips[prveStep + 1].action()
                  return prveStep + 1
                })
              }
            }}
          >
            知道了
          </button>
        </div>
      }

    </Dialog>
  )
}
// style.js

const style = {
  container: {
    margin: '200px auto',
    padding: '20px',
    width: '400px',
    border: '1px solid'
  },
  first: {
    display: 'flex',
    justifyContent: 'space-between',
    lineHeight: '30px',
    marginBottom: '30px'
  },
  second: {
    marginBottom: '25px',
    width: '100%'
  }
}

export default style
// style.css

.xbtn {
  width: 100px;
}

.qxbtn {
  margin-right: 8px;
  width: 78px;
}

.qdbtn {
  width: 78px;
}

.xbtn:focus,
.qxbtn:focus,
.qdbtn:focus{
  border: 2px solid greenyellow;
  background-color: deeppink;
}

useLayoutEffect Hook

简介:

useLayout HookuseEffect Hook用法一致,只是触发时机不同。且useEffect Hook是异步的,而useLayout Hook是同步执行的。

import { useEffect, useLayoutEffect } from "react"

export default function Index () {
  console.log(1)
  useEffect(() => {
    console.log('useEffect')
  }, [])
  console.log(2)

  useLayoutEffect(() => {
    console.log('useLayoutEffect')
  }, [])

  console.log(3)
}

打印结果:

React

问题:

  1. useLayoutEffect Hook不是同步的吗?打印结果不应该是【1、2、useLayoutEffect、3、Effect】?
  • 这里说的useLayoutEffect Hook同步指的是‘‘React会在所有的 DOM 变更之后同步调用 effect’’。DOM变更之后,浏览器还需要绘制然后是渲染才会呈现在页面上。所以说当useLayoutEffect Hook的effect callback执行时DOM是还没渲染到页面上的,执行时机可以类比于componentDidMount

  • componentDidMount也是会在浏览器更新屏幕之前触发,即使render函数被调用多次(意味着改变了状态),用户也看不到中间状态。

  1. 什么意思?
  • 不管useLayoutEffect还是useEffect,它们的执行时机都是在render之后的。函数的render方法在哪里?

解释:

  1. render函数在React中有两种形式

①在类组件中指的是render方法

class Index extends React.Component {
  render() {
    return <h1>2022-10-18晚</h1>
  }
}

②在函数组件中render函数指的是函数组件本体

function Index() {
  // 里面全是在render方法的范畴,某些Hook有自己特定的执行时机
  return <h1>2022-10-18晚</h1>
}

也就是说:

  1. 是先执行了render方法才执行的useLayoutEffect/useEffect,这也就解释了为什么先打印出1、2、3了。

  2. render过程中,React将新调用的render函数返回的树与旧版本的树进行比较,这一步是决定如何更新DOM的必要步骤,然后进行diff比较,更新DOM树。而React Hooks的存在是为了操作这些虚拟DOM,最终生成真实DOM,浏览器再绘制渲染到页面的这么一个过程。

执行时机:

  1. useEffect:DOM节点已加载完毕,浏览器绘制(渲染)完后延迟执行effect callback
  2. useLayoutEffect:DOM节点已加载完毕,浏览器绘制前执行effect callback

假设:

  1. 通过某个事件更改了某个状态(state)
  2. React更新这个状态(state)
  3. React处理组件中return出来的DOM节点(diff算法)
  4. 浏览器绘制更新之后的DOM
  5. 渲染完成,将真实DOM展示在页面上

①:useLayoutEffect Hook是在第三点结束,第四点即将要开始时同步执行。同步意味着可能会阻塞后续的步骤

②:useEffect Hook是在第五点结束后异步执行,浏览器什么时候闲了什么时候执行。

用法:(与useEffect用法一致,区别仅在触发时机不同)

import { useState, useLayoutEffect } from "react"
// export const root = ReactDOM.createRoot(document.getElementById('root'))
import { root } from '../index'
export default function Index () {
  const [count, setCount] = useState(0)
  useLayoutEffect(() => {
    let timer
    timer = setInterval(() => setCount(count + 1), 1500)
    console.log('componentDidMount&componentDidUpdate & 依赖更新')
    return function clearTimer () {
      console.log('componentWillUnmount')
      clearInterval(timer)
    }
  }, [count])

  // 组件销毁时调用
  const unmount = () => {
    root.unmount()
  }
  return (
    <>
      <h1>useLayoutEffect</h1>
      <h2>count: {count}</h2>
      <button onClick={unmount}>销毁组件</button>
    </>
  )
}

效果:

React 总是在下一次更新前清除上一次的副作用(effect)

总结:

useLayout HookuseEffect Hook用法一致,区别在调用时机。

useDeferredValue Hook

简介:

useDeferredValueHook用于降低渲染优先级,从而腾出CPU资源来渲染优先级更高的更新,最终目的是提高用户体验。

用法:

import { useDeferredValue } from 'react'
const deferredValue = useDeferredValue(value)
  1. 这个Hook接收一个值并返回一个副本,该副本会被延迟渲染
  2. 延迟渲染的意思是:其他的渲染优先级都高于该副本的渲染优先级,该副本将等待至仅剩本身未渲染时开始渲染。

应用场景

  • 数据量很大,导致页面卡顿,可以考虑使用useDeferredValue这个Hook。

需求:

有10万条数据,根据在输入框中的输入筛选出有关数据

代码演示:

下面会用两种方式进行演示:①没有用这个hook使用了该hook

第一种:

import { useState, useEffect } from "react"
import { nanoid } from 'nanoid'
import './useDeferredValue.css'

export default function Index () {

  const [value, setValue] = useState('')
  const [initData, setInit] = useState([])
  const [filterData, setFilter] = useState([])
  // 键盘输入
  const inputChange = (e) => {
    console.log('输入框:', e.target.value)
    setValue(e.target.value)
  }
 // 根据键盘输入,筛选出相关数据
  useEffect(() => {
    initData && setFilter(initData.filter(item => {
      return !!item['display'].includes(value)
    }))
  }, [value])
 // 初始化,准备10万条数据
  useEffect(() => {
    const arr = []
    for (let i = 0; i < 100000; i++) {
      arr.push({ id: nanoid(), display: '数据' + i })
    }
    setInit(arr)
  }, [])

  return (
    <div className="container">
      <span>搜索:</span>
      <input type="text" onChange={inputChange} />
      <List data={filterData} />
    </div>
  )
}

function List ({ data = [] }) {
  return (
    <ul className="ul"> 
       {  
          data && 
          data.map(item => <li key={item.id}>{item.display}</li>)
       } 
    </ul>
  )
}

// useDeferredValue.css
.container {
  margin: 0 auto;
  width: 300px;
}
.ul {
  list-style: none;
}

.ul > li {
  margin-bottom: 10px;
  padding: 5px;
  text-align: center;
  background-color: aqua;
}

代码效果:

React

解释&问题:

  1. React18提出了一个概念 - “concurrency”,翻译成中文是并发性,有点像同步、异步的意思。

  2. 可以这么理解:React将处理通道由1个变成2个,一个是快速通道,而另一个是慢速通道

  3. 默认都是在快速通道,大家都同时渲染,就算A已经准备就绪,但还是要等待B处理完成,然后A、B同时渲染到页面上。这样的后果是:当数据量庞大时,A已经好了,B没好,等一段时间B好了,A、B同时出现在页面上,用户体验感极差。

  4. 上面的案例就是,键盘输入已经完成,但数据还未筛选出,渲染键盘输入的字符要等到筛选出数据之后才能渲染,导致卡顿,给人的感觉极差。

由此引出,useDeferredValue Hook

第二种:

先看效果:

React

使用了该Hook后,不管是从速度上还是视觉上,都有了极大的提升!

代码:

import { useState, useEffect, useDeferredValue } from "react"
import { nanoid } from 'nanoid'
// 样式同上
import './useDeferredValue.css'

export default function Index () {

  const [value, setValue] = useState('')
  const [initData, setInit] = useState([])
  const [filterData, setFilter] = useState([])
  // 键盘输入
  const inputChange = (e) => {
    console.log('输入框:', e.target.value)
    setValue(e.target.value)
  }
  // 根据键盘输入,筛选出相关数据
  useEffect(() => {
    initData && setFilter(initData.filter(item => {
      return !!item['display'].includes(value)
    }))
  }, [value])
  // 初始化,准备10万条数据
  useEffect(() => {
    const arr = []
    for (let i = 0; i < 100000; i++) {
      arr.push({ id: nanoid(), display: '数据' + i })
    }
    setInit(arr)
  }, [])
  
  // 关键:该副本(passedValue)的渲染将会滞后!
  const passedValue = useDeferredValue(filterData)

  return (
    <div className="container">
      <span>搜索:</span>
      <input type="text" onChange={inputChange} />
      {/*副本传递给List组件*/}
      <List data={passedValue} />
    </div>
  )
}

function List ({ data = [] }) {
  return (
    <ul className="ul"> 
       {  
          data && 
          data.map(item => <li key={item.id}>{item.display}</li>)
       } 
    </ul>
  )
}

解释:

  1. 将一个value值传入useDeferredValue这个Hook,它会返回一个值(副本),React会将该副本放入慢速通道,在慢速通道的渲染都会滞后!

  2. 慢速通道的渲染时机是在快速通道都已渲染完毕后

  3. 意思是:当A已处理完成但B因为某种原因处理时间较长时,可将B的值传给该Hook,使用其返回值(副本)来进行渲染操作(虚拟DOM节点的生成),这样就不会因为B的处理时间长而影响了A的渲染。有点异步的意思。

  4. 上述案例就是利用React18提出的“并发性”来解决数据量庞大,耗时长的问题。

useTransition Hook

简介:

useTransitionuseDeferredValue作用相同,都是将‘渲染’加入到慢速通道,延迟渲染。但它们用法不同,也有区别,之后会介绍

   import { useTransition } from 'react'
   const [isPending, startTransition] = useTransition()

参数介绍:

  1. startTransition: 接收一个回调函数,在回调函数中设置更新,可将更新加入到慢速通道,延迟由该状态导致的UI渲染,换句话说,startTransition可以防止立即执行昂贵的UI渲染
import {useState, useTransition} from 'react'

const [isPending, start] = useTransition()
const [list, setList] = useState('')

start(() => {
  // 可以将昂贵的UI渲染包装在此函数中。
  setList(列表)
})

为什么需要这个功能存在?

  • 请记住,强制昂贵的UI渲染立即完成会阻止更轻、更紧急的UI渲染及时渲染。React默认是等待所有更新都完成再同时渲染,startTransition的出现避免了这一现象,使用它时,轻量级、紧急的渲染将不会被阻塞!

  • startTransition允许你将应用程序中的某些更新标记为非紧急,因此它们会暂停,同时优先考虑更紧急的更新。这使您的应用程序感觉更快,因此,无论你在渲染什么,你的应用程序仍在响应用户的操作。

如果你想在等待昂贵的UI渲染完成时显示一些内容怎么办?你可能显示一个进度条以向用户提供即时反馈,以便用户知道应用程序正在处理他们的请求。

为此,我们可以使用isPending来自useTransition Hook(钩子)的变量。

  1. isPending: 值为true/false,翻译为中文:是否在等待状态。当isPending为真时,说明目前处在等待状态,说明startTransition中的回调函数还未执行完成,我们可以利用isPending为True的这一特性,增加一行提示,告知用户,如:
{
  isPending && <div>正在输入中...</div>
}

isPending为false时,说明在慢速通道的UI渲染已全部完成。

需求:

跟useDeferredValue这个Hook的需求一样,都是根据输入框键入内容从10万条数据中筛选出相关数据。

如果你没有利用React18新提出的‘并发’特性,一旦数据量大,处理速度就慢,一些轻量级的渲染将要等待‘这数据量大的处理(根据键盘键入从十万条数据中筛选出相关数据)’完之后,同时渲染在页面上,页面将会出现卡顿、延迟的现象,用户体验极感极差。

直接来看使用useTransition钩子函数后的效果???

效果:

React

代码:

import { useState, useEffect, useTransition } from "react"
import { nanoid } from 'nanoid'
import './useDeferredValue.css'

export default function Index () {

  const [value, setValue] = useState('')
  const [initData, setInit] = useState([])
  const [filterData, setFilter] = useState([])
  const [ispending, startTransition] = useTransition()
  // 键盘输入
  const inputChange = (e) => {
    console.log('输入框:', e.target.value)
    setValue(e.target.value)
  }

 // 根据键盘输入,筛选出相关数据
  useEffect(() => {
    // 看这!!!
    initData && startTransition(() => {
      // 滞后setFilter导致的UI渲染!
      setFilter(initData.filter(item => {
        return !!item['display'].includes(value)
      }))
    })
  }, [value])

 // 初始化,准备10万条数据
  useEffect(() => {
    const arr = []
    for (let i = 0; i < 100000; i++) {
      arr.push({ id: nanoid(), display: '数据' + i })
    }
    setInit(arr)
  }, [])
  

  return (
    <div className="container">
      <span>搜索:</span>
      <input type="text" onChange={inputChange} />
      { ispending && <div>输入中...</div>}
      <List data={filterData} />
    </div>
  )
}

function List ({ data = [] }) {
  return (
    <ul className="ul"> 
       {  
          data && 
          data.map(item => <li key={item.id}>{item.display}</li>)
       } 
    </ul>
  )
}

useDeferredValue&useTransition

问题:

  1. useDeferredValue和useTransition功能都一样,为什么React要创建这两个功能一样的Hook?
  • useTransition是用来处理更新函数的,而useDeferredValue是用来处理更新函数执行后所更新的数据本身的。有些情况下,你并不能直接获得更新函数,比如你用的是第三方的hooks库,在使用的时候更新函数并不能直接对外暴露,这个时候你只能去优化数据,从而你只能使用useDeferredValue这个Hook。

  • useTransition的好处是它可以一次性处理好几个更新函数

注意事项:

  1. 对于同一个资源的优化,这两个Hook提供的优化效果是一样的,因此不需要同时使用;一旦同时使用,将会带来不必要的性能损耗

  2. 建议只有数据量大的时候才去考虑这两个Hook。

  3. 优先使用useTransition Hook,因为isPending变量能给用户带来不一样的视觉效果。

结语

  1. 不要试图去讨好这个世界,你是这世界唯一的你。 --- 自由极光《这世界唯一的你》

React

  1. 感谢我滴导师,wuwuwu~~ 大佬大佬大佬

React