一文通关React

一、前言

最近公司Vue@2全家桶需要转型React@18全家桶,并开发相应轮子等等计划。我上次看React还是在2年之前,所以最近花了一两周从头儿复习了React,结合各路大佬的笔记和视频,总结了自己的一篇,因为我习惯记录在有道云上面然后有空才会整理标题、文字、代码到CSDN,如果本文的文字片段中(非代码片段)出现
这类标签的请忽略。

文章共计8w8k字,写到最后CSDN都卡了,觉得不错的朋友点个赞鼓励下喔。

二、核心概念

不管是Vue转过来的同学,还是初学者,我都建议要耐心看核心概念,包括框架的思想和原理,当工作时间越长,之后你会发现无论是JS、Java还是GO还是什么,都只是工具而已,重要的是它其中的思想、设计理念。

2.1、React简介

2.1.1、React与传统MVC框架的关系

(1)MVC框架是什么:MVC也即Model业务模块、View用户视图、Controller控制器的统称。当View视图层有事件发生,它会将事件交给Controller控制层处理,Controller控制器再决定用哪一个Model业务模型去处理,这个时候View视图层会注册成为这个Model业务模型的订阅者,当Model业务模型处理完毕会通知View视图层进行更新。

(2)MVC框架的弊端是:一个Model关联一个View,这样会造成高度耦合,也即这个View层无法进行复用,再大型项目中这是很大的弊端。这也正是促使React被发明出来的原因。

(3)React既不是MVC框架也不是MVVM框架,严格来说它只能算V,也即View层,甚至React并不认可MVC框架的设计。

2.1.2、React的特性

(1)声明式设计:即我们只负责数据层的逻辑开发,视图层交给React自动完成生成。

(2)高效:React通过虚拟DOM,大幅度的减少与真实DOM的交互。

(3)灵活:React可以和已知的库或框架很好的配合。

(4)JSX:JSX语法可以使html、css、js在同一页面书写。

(5)组件:具有高度可复用性。

(6)单向响应的数据流:即数据自父到子传递,如果子有改变数据的逻辑则通知父,由父决定是否修改,再命令子修改。

2.1.3、虚拟DOM

在以前的JS开发中,如果我们有100条数据渲染到DOM中去,并在某个时段删除其中的一个DOM,我们需要将整个数据对象清空,再重新渲染99条数据到DOM中。而虚拟DOM(vdom)的出现,可以很好的避免这种情况:React使用JS创建两个数据对象,通过Diff算法让100条的和99条的做对比,得到更改的地方,再单独将改变的地方做增加或删除,其他不变的DOM不做处理。这样极大的提升了性能,减少了性能浪费。

2.2、安装脚手架

npx install create-react-app -g
create-react-app 文件名

create-react-app 会将 webpack 的配置文件隐藏起来,类似 vue.config.js 的东西没法使用。

你可以观察 package.json 中有 script 命令叫做:react-scripts eject ,它会把 webpack 的配置文件都暴露出来。

但是这样是不太安全的,所以这里推荐使用 Umi 、Vite 这样的脚手架去创建项目,它们都有对于 webpack 配置文件修改的方案,并且也都有多人数的社区。

当然如果你已经使用 npx create-react-app 这个脚手架了的话,可以安装下载 craco 这个插件,也能帮助你配置 webpack:

// 1、安装
pnpm install -D @craco/craco

// 2、修改 package.json 中的 script 命令
{
  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "craco eject"
  }
}

// 3、项目根目录下创建 craco.config.js,在其中进行 webpack 相关配置
const path = require('path')
const pathResolve = pathUrl => path.join(__dirname, pathUrl)

module.exports = {
  webpack: {
    alias: {
      '@': pathResolve('src'),
      '@assets': pathResolve('src/assets')
    }
  }
}

2.3、元素渲染

2.3.1、React的元素渲染

元素是构成React最小的砖块,元素描述了在屏幕上想看到的内容。 但是注意,与浏览器中的DOM元素不同,React元素创建的是开销极小的普通对象。 React DOM会负责更新DOM来与React元素保持一致,可以看成为比对再更新的状况。

const element = 

Hello, world

ReactDOM.render(element, document.getElementById('root'))

2.3.2、更新已渲染的元素

React元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的UI。 根据我们已知的知识,更新UI唯一的方法就是使用ReactDOM.render()。 我们来看一个定时器的例子:

function tick () {
  const element = (
    

Hello, world!

It is {new Date().toLocaleTimeString()}.

); ReactDOM.render(element, document.getElementById('root')) } setInterval(tick, 1000)

2.3.3、只更新需要更新的部分

React DOM会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使DOM达到预期的状态。

2.4、JSX

2.4.1、JSX简介

下面的这个语法,既不是字符串也不是HTML,他被称为JSX,也是JavaScript的语法扩展。JSX可能会使人联想到模版语言,但它具有JavaScript的全部功能。

const element = 

Hello, world!

2.4.2、在JSX中嵌入表达式

在下面的例子中,我们声明了一个名为name的变量,然后在JSX中使用它,并将它包裹在大括号中。 最后通过ReactDOM.render渲染到指定DOM中(渲染将在后面章节中介绍,你可以暂时理解为将元素呈现到屏幕上的方法)。 在括号内,可以放置任何有效的JavaScript表达式,例如:2+2、user.name、formatTime(time)等等,和vue的{{}}效能类似。

const welcome = 'Hello,'
const name = 'Josh Perez'
const element = 

Hello, {welcome + name}

ReactDOM.render( element, document.getElementById('root') );
function formatName (user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
}

const element = (
  

Hello, {formatName(user)}!

) ReactDOM.render( element, document.getElementById('root') )

2.4.3、JSX也是一个表达式

在编译之后,JSX表达式会被转为普通JavaScript函数调用,并且对其取值后得到JavaScript对象。 也就是说,你可以在if语句和for循环的代码块中使用JSX,将JSX赋值给变量,把JSX当作参数传入,以及从函数中返回JSX:

function getGreeting (user) {
  if (user) {
    return 

Hello, {formatName(user)}!

} return

Hello, Stranger.

}

2.4.4、JSX表达对象

Babel会把JSX转译成一个名为React.createElement()函数调用。 以下两种表达式完全等效: 

const element = (
  

Hello, world!

) const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' )

2.5、组件和props

2.5.1、函数组件和class组件

定义组件最简单的方法就是写JavaScript函数,下面的例子是一个有效的React组件。 因为它接收了唯一带有数据的“props”对象并返回一个React元素: 

import React from "react"
export default function Welcome (props) {
  return (
    

Hello, {props.name}

) } // 注意:在React16.8版本之前函数组件没有自身的状态,也被称为无状态组件, // 在16.8版本之后才支持有状态。 // 函数组件当然可以使用箭头函数方式: const Tabber = () => { return (
Tabber
) }

你同时还可以使用 ES6 的 class 来定义组件:

import React, { Component } from "react";
export default class Welcome extends Component {
  render() {
    return (
      

Hello, {this.props.name}

) } }

2.5.2、props组件通信(父传子)

// 父组件
import React, { Component } from "react"
import Tabbar from "./views/TabBar/TabBar.js"
import SideBar from "./views/SideBar/SideBar.js"

export default class App extends Component {
  let obj = {
    title: "123",
    showBtn: true
  }
  render() {
    return (
      
// 如果子组件恰巧也是命名相同的,我们可以用...展开符传递
) } } // 子组件 import React, { Component } from "react" export default class Tabbar extends Component { render() { let {title, showBtn} = this.props // 这是解构赋值,或者写为 let title = this.props.title; console.log(this.props) // {title: "导航栏", showBtn: true} return (
{title} {showBtn && }
) } } // 函数式子组件 // 函数组件特别需要注意的是,因为没有this,所以我们不同通过this.props来获取属性 // 我们需要通过形参来接收props import React from "react"; export default function SideBar(props) { console.log(this); // undefined console.log(props); // {title: "侧边栏"} let {title} = props; // 这是解构赋值的写法,你当然可以写为:let title = props.title; return (
{title}
); }

2.5.3、callback组件通信(子传父)

子组件无法直接修改父组件的State状态,因为状态是私有的,所以我们通过在父组件中定义方法回调,在子组件中调用props属性下挂载的方法回调,让父组件自己修改自身State状态,以达到在子组件点击按钮控制父组件下另一个子组件的显隐。 

// 父组件
import React, { Component } from "react"
import TabBar from "./views/TabBar/TabBar.js"
import SideBar from "./views/SideBar/SideBar.js"

export default class App extends Component {
  state = {
    show: false
  }

  handleShow = (text) => {
    this.setState({
      show: !this.state.show
    })
    console.log(text)  // 我是子组件的值
  }
  
  render() {
    return (
      
{this.state.show && }
) } } // 子组件 import React, { Component } from "react" export default class TabBar extends Component { state = { text: "我是子组件的值" } handleClick = () => { this.props.event(this.state.text) } render() { return (
) } }

2.5.4、状态提升(兄弟组件通信)

状态提升也称为:中间人模式(有点类似于Vue中的兄弟组件通信,也是需要通过共同的父组件去传递需要共享的数据)。
也就是说,多个组件需要去共享的State状态提升到最近的父组件中,父组件在通过更改后使用props分发给子组件。
需要注意的是,假设两个需要共享信息的组件不是亲兄弟组件,再通过这样去找最近父组件的方式,就过于麻烦了,要经过多层的传递,所以状态提升只适合亲兄弟组件,也就是这两个组件是同一个父组件。 

B和C是亲兄弟,只需要提升一层到A即可完成通信:
          Component A
Component B       Component C
D和E不是亲兄弟,需要提升多层到A才能完成通信:
            Component A
    Component B     Component C
Component D             Component E

2.5.5、发布订阅模式(兄弟组件通信,一种思想,纯JS的解决方案)

import React, { Component } from 'react'

export default class App extends Component {
  var bus = {
    list: [],
    subscribe(callback) {
      this.list.push(callback)
    },
    publish(info) {
      this.list.forEach(callback => {
          callback && callback(info)
      })
    }
  }
  
  render() {
    return (
      
APP
) } } // 订阅者 bus.subscribe((info) => { console.log("111", info) }) bus.subscribe((info) => { console.log("222", info) }) // 发布者 bus.publish('发布') // 111, 发布 222, 发布

2.5.6、context状态树传参(官方推荐的兄弟组件通信解决方案)

context可以跨多层级进行组件通信,前提是这些需要通信的组件都在context的控制范围内,也即需要通信的组件是消费者(consumer),他们需要一个供应商(provider)来提供context的环境。 

// 父组件
import React, { Component } from 'react'
import TabBar from "./views/TabBar/TabBar.js"
import SideBar from "./views/SideBar/SideBar.js"
const GlobalContext = React.createContext()

export default class App extends Component {
  constructor() {
    state = {
      text: "标题"
    }
  }
  
  render() {
    return (
       {
            this.setState({
              title: value
            })
          }
        }
      }>
        
) } } // 子组件 TabBar import React, { Component } from 'react' const GlobalContext = React.createContext() export default class TabBar extends Component { render() { return ( {/* 这里用这样的写法是因为关键点在value身上, 我们需要接收到供应商给我们提供的服务, 所以设计上采用了回调函数的写法, 方便我们拿到value这个服务形参。 */} { (value) => { console.log(value) // {title: "标题", text: "文本"} return (
{value.title} // 先是“标题”,点击sidebar的按钮后更新内容,调用setState重新渲染
) } }
) } } // 子组件 SideBar import React, { Component } from 'react' const GlobalContext = React.createContext() export default class TabBar extends Component { render() { return ( { (value) => { return (
{ value.changeTitle("更改标题") }}> 按钮
) } }
) } }

2.5.7、props属性类型检测和默认属性

我们引入React内置的prop-types,用它挂载的各种方法可以分别检测props的类型。
props的默认属性则使用static设置类属性即可。 

import React, { Component } from 'react'
import checkType from "prop-types"

export default class Tabbar extends Component {
  static propsTypeTest = {
    title: checkType.string,
    showBtn: checkType.bool
  };
  static defaultProps = {
    showBtn: true
  };
  render() {
    let {title, showBtn} = this.props
    console.log(this.props)    // {title: "导航栏", showBtn: true}
    return (
      
{title} {showBtn && }
) } } // 需要特别注意的一点是:函数式组件不支持以上的static写法 // 我们需要在外部给函数身上分别挂上两个属性 import React from 'react' import checkType from "prop-types" export default function SideBar(props) { let {title, showBtn} = props console.log(props) // {title: "侧边栏", showBtn: true} return (
{title}
) } SideBar.typeProps = { title }

2.5.8、props的只读性

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

React很灵活但它有一个严格的规则:所以的React组件必须保护它们的props不被更改。

当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。在下一章节中,我们将介绍一种新的概念,称之为 “state”。在不违反上述规则的情况下,state允许React组件随用户操作、网络响应或者其他变化而动态更改输出内容。

2.6、事件处理

2.6.1、事件代理模式

React的事件并不会绑定在每一个具体元素上,而是采用了事件代理的模式:

(1)事件会绑定在#root根节点上。

(2)当有事件发生时,root会查找所有的子元素身上的相应事件,找到之后进行处理。

(3)因为是事件代理了所有事件,所以默认有event参数,可以在函数中打印出来。

2.6.2、React元素和DOM元素的事件处理区别

React元素的事件处理和DOM元素的事件处理很相似,但是语法上会有不同:

(1)React事件的命名采用小驼峰式(camelCase),而不是纯小写。

(2)使用JSX语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

(3)你不能通过return false的方式阻止默认行为。你必须显式的使用preventDefault。

2.6.3、代码示例

// 传统HTML


// React


// 阻止默认行为
// 在这里,e是一个合成事件。React根据W3C规范来定义这些合成事件,所以你不需要担心跨浏览器的兼容性问题。
function ActionLink() {
  function handleClick(e) {
    e.preventDefault()
    console.log('The link was clicked.')
  }

  return (
    
      Click me
    
  )
}

你必须谨慎对待JSX回调函数中的this,在JavaScript中,class的方法默认不会绑定this。如果你忘记绑定this.handleClick并把它传入了onClick,当你调用这个函数的时候this的值为undefined。我们在construtor中使用bind来绑定this:

class Toggle extends React.Component {
  constructor(props) {
    super(props)
    this.state = {isToggleOn: true}
    // 为了在回调中使用 `this`,这个绑定是必不可少的
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }))
  }
  render() {
    return (
      
    )
  }
}

ReactDOM.render(
  ,
  document.getElementById('root')
)

如果你觉得bind来绑定this太麻烦,你可以使用另外两种办法:
public class fields语法、在回调中使用箭头函数:

// public class fields语法
class LoggingButton extends React.Component {
  // 此语法确保 `handleClick` 内的 `this` 已被绑定。
  // 注意: 这是 *实验性* 语法。
  handleClick = () => {
    console.log('this is:', this)
  }
  render() {
    return (
      
    )
  }
}


// 在回调中使用箭头函数
class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this)
  }
  render() {
    // 此语法确保 `handleClick` 内的 `this` 已被绑定。
    return (
      
    )
  }
}

2.6.4、向事件处理程序传递参数

以下两种方式是等价的,分别通过箭头函数和Function.prototype.bind来实现。 在这两种情况下,React的事件对象e会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过bind的方式,事件对象以及更多的参数将会被隐式的进行传递。


2.7、条件渲染

也即使用if else、&&、三目运算符来决定元素的渲染。

2.7.1、元素变量

也即可以声明一个变量来储存元素。

class LoginControl extends React.Component {
  constructor(props) {
    super(props)
    this.handleLoginClick = this.handleLoginClick.bind(this)
    this.handleLogoutClick = this.handleLogoutClick.bind(this)
    this.state = {isLoggedIn: false}
  }

  handleLoginClick() {
    this.setState({isLoggedIn: true})
  }

  handleLogoutClick() {
    this.setState({isLoggedIn: false})
  }

  render() {
    const isLoggedIn = this.state.isLoggedIn
    let button
    if (isLoggedIn) {
      button = 
    } else {
      button = 
    }

    return (
      
{button}
) } } ReactDOM.render( , document.getElementById('root') )

2.7.2、与运算符&&

function Mailbox(props) {
  const unreadMessages = props.unreadMessages
  return (
    

Hello!

{unreadMessages.length > 0 &&

You have {unreadMessages.length} unread messages.

}
) } const messages = ['React', 'Re: React', 'Re:Re: React'] ReactDOM.render( , document.getElementById('root') )

2.7.3、三目运算符

render() {
  const isLoggedIn = this.state.isLoggedIn
  return (
    
{isLoggedIn ? : }
) }

2.7.4、阻止元素渲染

在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让render方法直接返回null,而不进行任何渲染。

function WarningBanner (props) {
  if (!props.warn) {
    return null
  }

  return (
    
Warning!
) } class Page extends React.Component { constructor(props) { super(props) this.state = {showWarning: true} this.handleToggleClick = this.handleToggleClick.bind(this) } handleToggleClick () { this.setState(state => ({ showWarning: !state.showWarning })) } render() { return (
) } } ReactDOM.render( , document.getElementById('root') );

2.8、列表和key

2.8.1、渲染多个组件

下面,我们使用Javascript中的map()方法来遍历numbers数组。将数组中的每个元素变成li标签,最后我们将得到的数组赋值给listItems,并把listItems整个渲染进DOM中。

const numbers = [1, 2, 3, 4, 5]
const listItems = numbers.map((number) =>
  
  • {number}
  • ) ReactDOM.render(
      {listItems}
    , document.getElementById('root') )

    将上述代码转换为组件写法:

    function NumberList (props) {
      const numbers = props.numbers
      const listItems = numbers.map((number) =>
        
  • {number}
  • ) return (
      {listItems}
    ) } const numbers = [1, 2, 3, 4, 5] ReactDOM.render( , document.getElementById('root') )

    这时控制台会报没有key的错误,如果是使用过Vue应该知道是少绑了key,React同理,你可以绑定数据遍历后的id,在没有的情况下可以绑定index索引。在两种语法中,key都是独一无二的,是告诉语法哪些元素发生改变了,需要更新或添加或删除,所以我们应该切记给每个元素特定的key。但是Vue和React两个语法的写法有所不同:

    function NumberList (props) {
      const numbers = props.numbers
      const listItems = numbers.map((number, index) =>
        
  • {number}
  • ) return (
      {listItems}
    ) } const numbers = [1, 2, 3, 4, 5] ReactDOM.render( , document.getElementById('root') )

    这里需要提一嘴的是,如果写成下面这样的写法时,需要记得加上 return,而上面提前 const 声明的 map 循环子项则不需要 return:

    export default function Test () {
      const list = [{ name: 'Stefan', id: 1 }, { name: 'Edward', id: 2 }, { name: 'Simon', id: 3 }]
    
      return (
        
    Test { list.map(item => { return
    {item.name}
    }) }
    ) }

    2.9、插槽

    插槽的内在在于:提高复用性、减少父子通信。

    在Vue中使用slot去使用插槽,在React中使用this.props.children,这是一个官方默认写法,也就是说只能用这种方式拿到插槽内的DOM元素。

    // 父组件
    import React, { Component } from "react"
    import Child from "./Views/Components/Child.js"
    
    export defalut class App extends Component {
      render() {
        return (
          
    Slot 1
    Slot 2
    ) } } // 子组件 import React, { Component } from "react" export defalut class Child extends Component { render() { return (
    {this.props.children} // 会渲染两个DOM,Slot 1 和 Slot 2 {/* 不想把全部都渲染进来,我们可以使用数组取值的写法: */} {this.props.children[1]} // 这里只会渲染第二个DOM,Slot 2
    ) } }

    插槽还有一个妙用:虽然父组件需要被插入的DOM会最终插入到子组件中去,但是它可以访问到父组件状态。需要知道的是:它虽然可以访问父组件状态,但是跟父传子还是有区别的,子组件并没有得到信息,所以它替代不了父传子。

    import React, { Component } from "react";
    import Child from "./Views/Components/Child.js"
    
    export defalut class App extends Component {
      constructor() {
        state = {
          isShow: true,
        }
      }
      
      handleShow = () => {
        this.setState({
          isShow: !this.state.isShow 
        })
      }
      
      render() {
        return (
          
    { this.state.isShow &&

    标题

    }
    ) } } // 子组件 import React, { Component } from "react" export defalut class Child extends Component { render() { return (
    {this.props.children} // 会渲染两个DOM,Slot 1 和 Slot 2
    ) } }

    2.10、生命周期

    这一章节简单的了解下即可,后期使用React18+版本,实际上用不到这些东西了,官方推荐使用函数组件去写,不推荐使用class组件。

    2.10.1、初始阶段

    componentWillMount () {}  
    // 渲染前(render之前最后一次修改状态的机会)
    // 用来初始化数据最好用
    // React18中已经弃用,建议代码写在componentDidMount或constructor中
    
    render () {}             
    // 渲染中(只能访问props和state,不能修改它们、不能修改DOM输出)
    
    componentDidMount () {}   
    // 渲染后(成功render并渲染完成真实DOM之后触发,可以修改DOM)
    // 用来进行ajax数据请求、setInterval、订阅函数调用、对创建完的DOM进行各种操作、等等最好用

    2.10.2、运行阶段

    componentWillReceiveProps () {}   
    // 父组件属性是否更新(父组件修改属性时触发)
    
    shouleComponentUpdate (nextProps, nextState) {
      console.log(nextProps, nextState);
    }       
    // 组件是否需要更新(返回false会阻止render调用)
    // 这时组件还没有更新,所以nextProps、nextState两个参数代表更新后的属性和状态
    
    componentWillUpdate () {}         
    // 更新前(不能修改props和state)
    
    render () {}                      
    // 渲染中(只能访问props和state,不能修改它们、不能修改DOM输出)
    
    componentDidUpdate (prevProps, prevState) {
      console.log(prevProps, prevState)
    }      
    // 更新后(可以修改DOM)
    // prevProps、prevState两个参数代表组件更新前的属性和状态

    2.10.3、销毁阶段

    componentWillUnmount () {}        
    // 组件销毁前(在删除组件之前进行清理操作,比如清除定时器、事件监听器)

    2.10.4、新生命周期

    state = {}
    // componentWillMount 初始化类似,都是在渲染前最后一次更改状态的地方
    static getDerivedStateFromProps () {
      return {}
    }
    // 要注意的是,state、static关键字和return必须写,就算暂时没有状态和需要合并返回的状态也需要写不然会报错
    
    
    state = {
      name: "Stefan",
      age: "24"
    }
    static getDerivedStateFromProps () {
      return {
        name: "STEFAN"
      }
    }
    componentDidMount () {
      console.log(this.state.name, this.state.age);     // "STEFAN", 24
    }
    // 最终return出的是合并后的状态,如果同名会覆盖

    2.11、State

    2.11.1、State运用

    之前的定时器更新UI的方式,是通过ReactDOM.render()实现的,但是它有局限性,我们来看下面的一个例子,来实现一个真正封装的可复用的定时器组件:

    import React from "react"
    
    class Clock extends React.Component {
      constructor(props) {
        super(props)
        this.state = { 
          date: new Date(),
          timer: null
        }
      }
      componentDidMount () {
        this.setState({
          timer: setInterval(() => this.tick(), 1000),
        })
      }
      componentWillUnmount () {
        clearInterval(this.state.timer)
      }
      tick () {
        this.setState({
          date: new Date()
        })
      }
      render() {
        return (
          
    {this.state.date.toLocaleTimeString()}
    ) } } export default Clock

    上述代码如果你学习过vue,那么是很好理解的,如果没有学习过vue,那么我们来一步一步的解答其中的步骤:

    首先,我们要使用class组件来替代掉函数组件;

    之后,我们编写一个constructor构造函数,然后再其中为this.state赋初值(这里的state不能在构造函数外的任何地方直接更改,比如:this.state.date = null,这类直接修改值的方式是不被允许的,我们要使用this.setState实现修改操作,构造函数是唯一可以给this.state赋值的地方);

    其次,我们定义一个拥有this.setState动作的函数;

    然后,在生命周期componentDidMount()中设置计时器,并在计时器中调用具有this.setState动作的函数;

    最后,在生命周期componentWillUnmount()中清除定时器。

    上面两个生命周期的意思分别为:组件挂载DOM后、组件将要在DOM卸载后。

    2.11.2、setState({}, callback)

    setState特别需要注意的是:

    当它处在同步逻辑中的时候,它是异步去更新状态、真实DOM的;

    当它处在异步逻辑中的时候,它是同步去更新状态、真实DOM的。

    它接受第二个参数,callback回调函数,当状态和dom更新完毕之后触发这个回调。

    import React, { Component } from "react"
    export default class App extends Component {
      constructor() {
        state = {
          count: 0
        }  
      }
      
      render() {
        return (
          
    {this.state.count}
    ) } /* react触发标识位后,会把整个方法体进行异步处理,等方法体内代码全部走完, 再合并多个setState为一个,最后更新状态。 */ handleClick1 = () => { this.setState({ count: this.state.count++ }, () => { console.log(this.state.count); // 1 }) console.log(this.state.count); // 0 this.setState({ count: this.state.count++ }) console.log(this.state.count); // 0,页面中的dom是1 } handleClick2 = () => { setTimeout(() => { this.setState({ count: this.state.count++ }) console.log(this.state.count) // 1 this.setState({ count: this.state.count++ }) console.log(this.state.count) // 2 }, 0) } }

    2.11.3、数据是向下流动的

    组件可以选择把它的state作为props向下传递到它的子组件中:

    import React fomr "react"
    import Child from "@/components/Child.js"
    
    class Father extends React.Componet {
      constructor(props) {
        super(props)
        this.state = {
          date: new Date()
        }
      }
      render() {
        return (
          
    ) } }

    而子组件Child会在它的props中接收到参数date,但是组件本身不知道它是来自于父组件Father的state还是props,或是手动输入的:

    function Child (props) {
      return {props.date.toLocaleTimeString}
    }

    2.12、PureComponent

    PureComponent会帮你比较新旧props,新旧state,决定shouldComponentUpdate返回true或false,从而决定要不要呼叫render function。

    注意:如果你的state和props永远都会变,那么PureComponent并不会更高效,因为shallowEqual也需要花费时间。

    以下示例中,第一次点击button修改状态会重新render,我们可以通过shouldComponentUpdate去写逻辑判断是否重新render,也可以直接使用extends PureComponent来交给React判断,state变为thiStefan之后再次点击button修改为thiStefan,React判断前后两次值一样,就会阻止render:

    import React, { PureComponent } from "react"
    
    class Test extends PureComponent {
      state = {
        name: "Stefan"
      }
      
      handleChange() {
        this.setState({
          name: "thiStefan"  
        })
      }
      
      render() {
        console.log("render!")
        return (
          
    {this.state.name}
    ) } } export default Test

    三、夯实基础

    3.1、组件类

    3.1.1、简介

    组件类,详细分的话有三种类,第一类说白了就是我平时用于继承的基类组件 Component,PureComponent,还有就是 react 提供的内置的组件,比如 Fragment,StrictMode,另一部分就是高阶组件 forwardRef,memo 等。

    3.1.2、Component

    Component 是 class 组件的根基,类组件一切始于 Component 。对于 React.Component 使用,我们没什么好讲的,我们这里重点研究一下 React 对 Component 做了什么:

    // react/src/ReactBaseClasses.js
    function Component (props, context, updater) {
      this.props = props
      this.context = context
      this.$refs = emptyObject
      this.updater = updater || ReactNoopUpdateQueue
    }

    这就是 Component 函数,其中 updater 对象上保存着更新组件的方法。

    我们声明的类组件是什么时候以何种形式被实例化的呢?

    // react-reconciler/src/ReactFiberClassComponent.js
    function constructClassInstance (
      workInProgress,
      ctor,
      props
    ) {
      const instance = new ctor(props, context)
      instance.updater = {
        isMounted,
        enqueueSetState () {
          /* setState 触发这里面的逻辑 */
        },
        enqueueReplaceState () { },
        enqueueForceUpdate () {
          /* forceUpdate 触发这里的逻辑 */
        }
      }
    }

    对于 Component , react 处理逻辑还是很简单的,实例化我们类组件,然后赋值 updater 对象,负责组件的更新。然后在组件各个阶段,执行类组件的 render 函数,和对应的生命周期函数就可以了。

    3.1.3、PureComponent

    PureComponent 和 Component 用法,差不多一样,唯一不同的是,纯组件 PureComponent 会浅比较,props 和 state 是否相同,来决定是否重新渲染组件。所以一般用于性能调优,减少 render 次数。

    什么叫做浅比较,我这里举个列子:

    class Index extends React.PureComponent {
      constructor(props) {
        super(props)
        this.state = {
          data: {
            name: 'alien',
            age: 28
          }
        }
      }
      handleClick = () => {
        const { data } = this.state
        data.age++
        this.setState({ data })
      }
      render () {
        const { data } = this.state
        return 
    你的姓名是: {data.name}
    年龄: {data.age}
    } }

    这里点击 button 按钮让年龄做++操作,组件并不会重新渲染,因为 PureComponent 会比较新旧 data 对象,结果都指向同一个 data,没有产生变化,所以不更新视图。(这里的比较你可以根据深拷贝和浅拷贝去理解)

    解决这个问题同样很简单,你需要把 handleClick 事件这么写:

    this.setState({ data: { ...data } })

    浅拷贝就能根本解决问题。

    3.1.4、memo

    React.memo 和 PureComponent 作用类似,可以用作性能优化,React.memo 是高阶组件,函数组件和类组件都可以使用, 和 PureComponent 区别是 React.memo 只能对 props 的情况确定是否渲染,而 PureComponent 是针对 props 和 state。

    React.memo 接受两个参数,第一个参数原始组件本身,第二个参数,可以根据一次更新中props是否相同决定原始组件是否重新渲染。是一个返回布尔值,true 证明组件无须重新渲染,false 证明组件需要重新渲染,这个和类组件中的 shouldComponentUpdate() 正好相反 。

    React.memo:第二个参数返回 true 组件不渲染 , 返回 false 组件重新渲染。 shouldComponentUpdate:返回 true 组件渲染 , 返回 false 组件不渲染。

    接下来我们做一个场景,控制组件在仅此一个 props 数字变量,一定范围渲染。

    例子:

    控制 props 中的 number :

    • 只有 number 更改,组件渲染。

    • 只有 number 小于 5 ,组件渲染。

    // 引入 React 和 memo
    import React, { memo } from 'react'
    
    // 定义memo的第一个参数也即原始组件本身
    function TextMemo (props) {
      console.log('子组件渲染')
      console.log(props)  // { num: 1, number: 1 }
      if (props)
        return 
    hello,world
    } // 定义memo的第二个参数,在其中做判断返回布尔值 const controlIsRender = (pre, next) => { if (pre.number === next.number) { // number 不改变 ,不渲染组件 return true } else if (pre.number !== next.number && next.number > 5) { // number 改变 ,但值大于5 , 不渲染组件 return true } else { // 否则渲染组件 return false } } // 定义组件 const NewTexMemo = memo(TextMemo, controlIsRender) // 定义将要使用 memo 组件的类组件 class Index extends React.Component { constructor(props) { super(props) this.state = { number: 1, num: 1 } } render () { const { num, number } = this.state return
    改变num:当前值 {num}
    改变number: 当前值 {number}
    {/* 将两个 props 传入 memo 组件中 */}
    } }

    效果是:只有点击改变 number 的 button 按钮改变了 number 值,且在 number 小于5的时候才会渲染组件。因为 memo 的第二个参数中只根据 number 去做了 true 和 false 的返回,所以加减 num 不会造成 memo 组件的渲染。

    React.memo 一定程度上,可以等价于组件外部使用 shouldComponentUpdate ,用于拦截新老props,确定组件是否更新。

    3.1.5、forwardRef

    官网对 forwardRef 的概念和用法很笼统,也没有给定一个具体的案例。很多同学不知道 forwardRef 具体怎么用,下面我结合具体例子给大家讲解 forwardRef 应用场景。

    3.1.5.1、转发引入Ref

    这个场景实际很简单,比如父组件想获取孙组件,某一个 dom 元素。这种隔代 ref 获取引用,就需要 forwardRef 来助力。

    function Son (props) {
      const { grandRef } = props
      return 
    这个是想要获取元素
    } class Father extends React.Component { constructor(props) { super(props) } render () { return
    } } const NewFather = React.forwardRef((props, ref) => ) class GrandFather extends React.Component { constructor(props) { super(props) } node = null componentDidMount () { console.log(this.node) } render () { return
    this.node = node} />
    } }

    react 不允许 ref 通过 props 传递,因为组件上已经有 ref 这个属性,在组件调和过程中,已经被特殊处理,forwardRef 出现就是解决这个问题,把 ref 转发到自定义的 forwardRef 定义的属性上,让 ref 可以通过 props 传递。

    3.1.5.2、高阶组件转发Ref

    由于属性代理的 hoc,被包裹一层,所以如果是类组件,是通过 ref 拿不到原始组件的实例的,不过我们可以通过 forWardRef 转发 ref。

    以此来解决高阶组件转发 Ref 的需求:

    function HOC (Component) {
      class Wrap extends React.Component {
        render () {
          const { forwardedRef, ...otherprops } = this.props
          return 
        }
      }
      return React.forwardRef((props, ref) => )
    }
    class Index extends React.Component {
      componentDidMount () {
        console.log(666)
      }
      render () {
        return 
    hello,world
    } } const HocIndex = HOC(Index, true) export default () => { const node = useRef(null) useEffect(() => { {/* 就可以跨层级,捕获到 Index 组件的实例了 */} console.log(node.current) // 打印出组件对象 console.log(node.current.componentDidMount) // node.current下挂载的原型对象中可以拿到生命周期等函数 }, []) return (
    ) }

    3.1.6、lazy

    React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库。

    React.lazy 和 Suspense 配合一起用,能够有动态加载组件的效果。React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。

    我们模拟一个动态加载的场景:

    // 父组件
    import { Spin } from 'antd'
    import Test from './comTest'
    
    const LazyComponent = React.lazy(() => new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          default: () => 
        })
      }, 2000)
    }))
    
    class TestLazy extends React.Component{   
      render () {
        return 
    {/* Spin组件是antd的icon图标,Test组件将会在2S之后替换掉它 */}
    }>
    } }
    // 子组件
    class Test extends React.Component {
      constructor(props) {
        super(props)
      }
      componentDidMount () {
        console.log('--componentDidMount--')
      }
    
      render () {
        return 
    Test
    } }

    3.1.7、Suspense

    何为 Suspense,Suspense 让组件“等待”某个异步操作,直到该异步操作结束即可渲染。

    用于数据获取的 Suspense 是一个新特性,你可以使用 以声明的方式来“等待”任何内容,包括数据。本文重点介绍它在数据获取的用例,它也可以用于等待图像、脚本或其他异步的操作。

    上面讲到高阶组件 lazy 时候,已经用 lazy + Suspense 模式,构建了异步渲染组件。我们看一下官网文档中的案例:

    const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载
    
    }>
      
    

    3.1.8、Fragment

    React 不支持一个组件返回多节点元素,(类似于 Vue2 的规则,在 Vue3 中已经自动允许返回多节点),我们写个例子:

    export default () => {
      return (
        
  • Stefan
  • Edward
  • Simon
  • ) }

    当然最简单的是按照规则去解决这个问题,即加一层外层 DOM 去包裹:

    export default () => {
      return (
        
  • Stefan
  • Edward
  • Simon
  • ) }

    但是如果我们不想额外的增加 DOM 节点,所以就引出了 React 的 Fragment 碎片化概念:

    
      
  • Stefan
  • Edward
  • Simon
  • 还可以简写为:

    <>
      
  • Stefan
  • Edward
  • Simon
  • 和 Fragment 区别是,Fragment 可以支持 key 属性。<> 不支持 key 属性。

    这里需要提一嘴的是,我们使用 map 遍历的元素,React 会默认在外层嵌套一层 Fragment:

    const list = [{name: 'Stefan', id: 1}, {name: 'Edward', id: 2}, {name: 'Simon', id: 3}]
    {
      list.map(item => {
    	return 
    	  {item.name}
    	
      })
    }
    
    // React底层处理过之后等价于:
    
      Stefan
      Edward
      Simon
    

    3.1.9、Profiler

    Profiler 这个 api 一般用于开发阶段,性能检测,检测一次 react 组件渲染用时,性能开销。

    Profiler 需要两个参数:

    第一个参数:是 id,用于表识唯一性的 Profiler。

    第二个参数:onRender 回调函数,用于渲染完成,接受渲染参数。

    const index = () => {
      const callback = (...arg) => console.log(arg)
      return 
    {renderRoutes(menusList)}
    }

    arg 会打印出这些内容:

    0: "root",
    1: "mount",
    2: 6.685000262223184,
    3: 4.430000321008265,
    4: 689.7299999836832,
    5: 698.5799999674782,
    6: Set(0) {}
    
    // 他们分别代表着:
    0: Profiler 树的 id
    1: mount 挂载,update 渲染了
    2: 更新 committed 花费的渲染时间。
    3: 渲染整颗子树需要的时间
    4: 本次更新开始渲染的时间
    5: 本次更新committed 的时间
    6: 本次更新的 interactions 的集合

    Profiler 是一个轻量级的组件,但是我们也应该只在需要的时候使用它。

    3.1.10、StrictMode

    StrictMode 见名知意,严格模式,用于检测 react 项目中的潜在的问题,。与 Fragment 一样, StrictMode 不会渲染任何可见的 UI 。它为其后代元素触发额外的检查和警告。

    严格模式检查仅在开发模式下运行;它们不会影响生产构建。

    StrictMode 目前有助于:

    • 识别不安全的生命周期

    • 关于使用过时字符串 ref API 的警告

    • 关于使用废弃的 findDOMNode 方法的警告

    • 检测意外的副作用

    • 检测过时的 context API

    对于不安全生命周期指的是:UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps,UNSAFE_componentWillUpdate

    这些声明周期是官方不推荐使用的,如果在严格模式中使用会控制台报错,在之前大多使用 class 组件的时期如果一定要用这几个声明周期,只有去除严格模式,依然是可以生效的,但是不推荐使用,因为这些生命周期 React 已经停止维护了。React 官方称之为过时的生命周期,且不推荐在新版本 React 中继续使用它们。

    class Test extends React.component {
      UNSAFE_componentWillMount () {
        console.log('-- UNSAFE_componentWillMount --')
      }
      
      render () {
    	return 
    Test
    } } // 控制台报错 Warning: Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. 警告:不建议在严格模式下使用不安全的生命周期,这可能表明你的代码有隐患。

    3.2、工具类

    3.2.1、createElement

    一提到 createElement,就不由得和 jsx 联系一起。我们写的 jsx,最终会被 babel,用 createElement 编译成 react 元素形式。我写一个组件,我们看一下会被编译成什么样子:

    render () {
      return 
        生命周期
              Flagment      text文本   
    } // 编译之后是这样: render() {   return React.createElement("div", { className: "box" },     React.createElement("div", { className: "item" }, "\u751F\u547D\u5468\u671F"),     React.createElement(Text, { mes: "hello,world" }),     React.createElement(React.Fragment, null, " Flagment "),     "text\u6587\u672C"); }

    所以我们可以不用 jsx 模式,而是直接通过 createElement 进行开发:

    // createElement 模型
    React.craeteElement(
      type,
      [props],
      [...children]
    )
    
    //  createElement 参数
    第一个参数:如果是组件类型就传入组件,如果是 DOM 类型就会传入 div 或者 span 这样的字符串
    第二个参数:是一个对象,如果是组件类型就是 props,如果是 DOM 类型就是属性
    其他参数:依次为 children,根据顺序排列
    

    经过 createElement 处理,最终会形成 $$typeof = Symbol(react.element) 对象。对象上保存了该 react.element 的信息。

    3.2.2、cloneElement

    createElement 把我们写的 jsx,变成 element 对象; 而 cloneElement 的作用是以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。

    那么 cloneElement 感觉在我们实际业务组件中,可能没什么用,但是在一些开源项目,或者是公共插槽组件中用处还是蛮大的,比如说,我们可以在组件中,劫持 children element,然后通过 cloneElement 克隆 element,混入 props。经典的案例就是 react-router 中的 Swtich 组件,通过这种方式,来匹配唯一的 Route 并加以渲染。

    我们设置一个场景,在组件中,去劫持 children,然后给 children 赋能一些额外的 props:

    function Father ({ children }) {
      const newChildren = React.cloneElement(children, { age: 24 })
      return 
     {newChildren} 
    } function Son (props) {   console.log(props)   return 
    hello,world
    } class Test extends React.Component {   render () {     return 
                               
      } } // 控制台打印: { name: 'Stefan', age: 24 }

    3.2.3、createContext

    createContext 用于创建一个Context 对象,createContext 对象中,包括用于传递 Context 对象值 value 的 Provider,和接受 value 变化订阅的Consumer。

    const MyContext = React.createContext(defaultValue)
    

    createContext 接受一个参数 defaultValue,如果 Consumer 上一级一直没有 Provider,则会应用 defaultValue 作为 value。只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

    我们来模拟一个 Context.Provider 和 Context.Consumer 的例子:

    import React from 'react'
    const MyContext = React.createContext()
    
    function ComponentB () {
      /* 用 Consumer 订阅, 来自 Provider 中 value 的改变  */
      return 
        {(value) => }
      
    }
    
    function ComponentA (props) {
      const { name, mes } = props
    
      return 
        
     姓名:{name}
        
     想对大家说:{mes}
      
    } function index () {   const [value] = React.useState({     name: 'alien',     mes: 'let us learn React '   })   return                     
    }

    Provider 和 Consumer 的良好的特性,可以做数据的,Consumer 一方面传递 value,另一方面可以订阅 value 的改变。

    Provider 还有一个特性可以层层传递 value,这种特性在 react-redux 中表现的淋漓尽致

    3.2.4、createFactory

    React.createFactory(type)
    

    返回用于生成返回用于生成指定类型 React 元素的函数。类型参数既可以是标签名字符串(像是 'div' 或 'span'),也可以是 React 组件 类型 ( class 组件或函数组件),或是 React fragment 类型。

    使用:指定类型 React 元素的函数。类型参数既可以是标签名字符串(像是 'div' 或 'span'),也可以是 React 组件 类型 ( class 组件或函数组件),或是 React fragment 类型。

    使用:

    const Text = React.createFactory(() => 
    hello,world
    ) function Index () {     return (                 
      ) }

    使用后组件可正常显示,但控制台会抛出错误,因为此方法已经废弃了,建议换成 React.createElement 效果相同:

    // 控制台抛出该方法即将废弃的警告
    Warning: React.createFactory() is deprecated and will be removed in a future major release.Consider using JSX or use React.createElement directly instead.

    3.2.5、createRef

    createRef 可以创建一个 ref 元素,附加在 react 元素上。

    class Index extends React.Component {
      constructor (props) {
        super(props)
        this.node = React.createRef()
      }
      componentDidMount () {
        console.log(this.node)
      }
    
      render () {
        return 
          Some text..
        
      } }

    我们其实可以避开 createRef() 这个方法这样在 class 组件里捕获到 ref :

    class Index extends React.Component {
      node = null
      componentDidMount () {
        console.log(this.node)
      }
    
      render () {
        return  this.node}>
          Some text..
        
      } }

    或者在 function 组件中使用 Hook 捕获:

    function Index(){
      const node = React.useRef(null)
      useEffect(()=>{
        console.log(node.current)
      },[])
    
      return (
     
       Some text..
        
      ) }

    3.2.6、isValidElement

    这个方法可以用来检测是否为 react element 元素,接受待验证对象,返回 true 或者 false。这个 api 可能对于业务组件的开发,作用不大,因为对于组件内部状态,都是已知的,我们根本就不需要去验证,是否是 react element 元素。 但是,对于一起公共组件或是开源库,isValidElement 就很有作用了。

    没有使用之前:

    const Text = () => 
    Oi!
    class WarpComponent extends React.Component {   constructor (props) {     super(props)   }   render () {     return this.props.children   } } function Index () {   return                    
    Mr White.
          It's good to see you again!     
      
    } // 页面显示为: // Oi! // Mr White. // It's good to see you again!

    使用 isVaildElement 进行验证并通过 filter 过滤之后:

    class WarpComponent extends React.Component {
      constructor (props) {
        super(props)
        this.newChidren = this.props.children.filter(item => React.isValidElement(item))
      }
      render () {
        return this.newChidren
      }
    }
    
    // 页面显示为:
    Oi!
    Mr White.
    

    过滤掉了非 react element 的最后一行文本 It's good to see you again! 。

    3.2.7、Children.map

    React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

    有的同学会问遍历 children 用数组方法 map 或者 forEach 不就可以了吗? 请我们注意一下不透明数据结构,什么叫做不透明结构?

    我们先来看一下透明结构:

    class Text extends React.Component {
      render () {
        return 
    hello,world
      } } function WarpComponent (props) {   console.log(props.children)   return props.children } function Index () {   return 
                                    hello,world        
    } // 控制台打印出: [   {$$typeof: Symbol(react.element), key: null, ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), key: null, ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), key: null, ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), type: "span", key: null, ref: null, props: {...}, ...} ]

    我们把组件 Index 的结构改动一下:

    function Index () {
      return 
               { new Array(3).fill(0).map((item, index) => ) }       hello,world        
    } // 控制台打印出: [   [{...}, {...}, {...}],   {$$typeof: Symbol(react.element), type: "span", key: '.1', ref: null, props: {...}, ...} ]

    这个数据结构,我们不能正常的遍历了,即使遍历也不能遍历到每一个子元素,此时就需要 react.Chidren 来帮忙了:

    function WarpComponent (props){
      const newChildren = React.Children.map(props.children, (item) => item)
      console.log(newChildren)
      return newChildren
    }
    
    // 控制台打印出:
    [
      {$$typeof: Symbol(react.element), key: '.0:$0', ref: null, props: {...}, type: f, ...},
      {$$typeof: Symbol(react.element), key: '.0:$1', ref: null, props: {...}, type: f, ...},
      {$$typeof: Symbol(react.element), key: '.0:$2', ref: null, props: {...}, type: f, ...},
      {$$typeof: Symbol(react.element), type: "span", key: '.1', ref: null, props: {...}, ...}
    ]
    

    注意 如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历:

    function Index () {
      return 
               <>         { new Array(3).fill(0).map((item, index) => ) }         hello,world               
    } // 控制台打印出: [   {     $$typeof: Symbol(react.element),     key: '.0',     ref: null,     props: {children: Array(2)},     type: Symbol(react.fragment),     ...   } ]

    3.2.8、Children.forEach

    Children.forEach 和 Children.map 用法类似,Children.map 以返回新的数组,Children.forEach 仅停留在遍历阶段。

    我们将之前 Children.map 的 WarpComponent 方法,用 Children.forEach 改一下。

    function WarpComponent (props){
      React.Children.forEach(props.children, (item) => console.log(item))
      return props.children
    }   

    3.2.9、Children.count

    children 中的组件总数量,等同于通过 map 或 forEach 调用回调函数的次数。对于更复杂的结果,Children.count 可以返回同一级别子组件的数量。

    我们还是把 Children.map 的例子进行改造:

    function WarpComponent (props) {
      const childrenCount =  React.Children.count(props.children)
      console.log(childrenCount, 'childrenCount')
      return props.children
    }
    
    function Index () {
      return ( 
        
                   {           new Array(3).fill(0).map((item, index) => {             new Array(2).fill(1).map((ite, ind) => {                            })           })         }         hello,world            
      ) } // 控制台打印出: 7 "childrenCount"

    3.2.10、Children.toArray

    Children.toArray 返回扁平化后的结果:

    function WarpComponent (props) {
      const newChidrenArray =  React.Children.toArray(props.children)
      console.log(newChidrenArray, 'newChidrenArray')
      return newChidrenArray
    }
    
    function Index () {
      return ( 
        
                   {           new Array(3).fill(0).map((item, index) => {             new Array(2).fill(1).map((ite, ind) => {                            })           })         }         hello,world            
      ) } // 控制台打印出: [   {$$typeof: Symbol(react.element), key: ".0:0:$0", ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), key: ".0:0:$1", ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), key: ".0:1:$1", ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), key: ".0:1:$2", ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), key: ".0:2:$2", ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), key: ".0:2:$3", ref: null, props: {...}, type: f, ...},   {$$typeof: Symbol(react.element), type: "span", key: ".1", ref: null, props: {...}, ...} ], "childrenCount"

    newChidrenArray 就是扁平化的数组结构。React.Children.toArray() 在拉平展开子节点列表时,更改 key 值以保留嵌套数组的语义。也就是说, toArray 会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。

    3.3、react-dom

    3.3.1、render

    render 是我们最常用的 react-dom 的 api,用于渲染一个 react 元素,一般 react 项目我们都用它,渲染根部容器 app。

    ReactDOM.render(element, container[, callback])
    

    使用

    ReactDOM.render(
      ,
      document.getElementById('app')
    )
    

    ReactDOM.render 会控制 container 容器节点里的内容,但是不会修改容器节点本身。

    3.3.2、hydrate

    服务端渲染用 hydrate 。用法与 render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。

    ReactDOM.hydrate(element, container[, callback])

    3.3.3、createPortal

    Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。createPortal 可以把当前组件或 element 元素的子节点,渲染到组件之外的其他地方。

    那么具体应用到什么场景呢?

    比如一些全局的弹窗组件 model, 组件一般都写在我们的组件内部,倒是真正挂载的 dom,都是在外层容器,比如 body 上。此时就很适合 createPortalAPI。

    createPortal接受两个参数:

    ReactDOM.createPortal(child, container)
    

    第一个: child 是任何可渲染的 React 子元素 第二个: container是一个 DOM 元素。

    接下来我们实践一下:

    function WrapComponent ({ children }) {
      const domRef = useRef(null)
      const [PortalComponent, setPortalComponent] = useState(null)
      React.useEffect(() => {
        setPortalComponent(ReactDOM.createPortal(children, domRef.current))
      }, [])
    
      return (
        
          
          {PortalComponent}        ) } class Index extends React.Component {   render () {     return (       
                       
    hello,world
            
          
        )   } }

    我们 children 实际在 container 之外挂载的,但是已经被 createPortal 渲染到 container 中。

    3.3.4、unstable_batchedUpdates

    在 react-legacy 模式下,对于事件,react 事件有批量更新来处理功能,但是这一些非常规的事件中,批量更新功能会被打破。所以我们可以用 react-dom 中提供的 unstable_batchedUpdates 来进行批量更新。

    一次点击实现的批量更新:

    class Index extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          numer: 1,
        }
      }
    
      handleClick = () => {
        this.setState({ numer: this.state.numer + 1 })
        console.log(this.state.numer)
        this.setState({ numer: this.state.numer + 1 })
        console.log(this.state.numer)
        this.setState({ numer: this.state.numer + 1 })
        console.log(this.state.numer)
      }
    
      render () {
        return (
          
            click me       
        )   } } // 控制台打印: 1 1 1 // 渲染次数一次

    批量更新条件被打破:

    handleClick = () => {
      Promise.resolve().then(() => {
        this.setState({ numer: this.state.numer + 1 })
        console.log(this.state.numer)
        this.setState({ numer: this.state.numer + 1 })
        console.log(this.state.numer)
        this.setState({ numer: this.state.numer + 1 })
        console.log(this.state.numer)
      })
    }
    
    // 控制台打印:
    2
    3
    4
    // 渲染次数三次
    

    借助 unstable_batchedUpdates 解决批量更新问题:

    handleClick = () => {
      Promise.resolve().then(() => {
        ReactDOM.unstable_batchedUpdates(() => {
          this.setState({ numer: this.state.numer + 1 })
          console.log(this.state.numer)
          this.setState({ numer: this.state.numer + 1 })
          console.log(this.state.numer)
          this.setState({ numer: this.state.numer + 1 })
          console.log(this.state.numer)
        })
      })
    }
    
    // 控制台打印:
    1
    1
    1
    // 渲染次数一次

    3.3.5、flushSync

    flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。我们知道 react 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。比如:

    ReactDOM.flushSync(() => {
      /* 此次更新将设置一个较高优先级的更新 */
      this.setState({name: 'alien'})
    })
    

    看 Demo 了解 flushSync:

    /* flushSync */
    import ReactDOM from 'react-dom'
    class Index extends React.Component {
      state = { number: 0 }
      handerClick = () => {
        setTimeout(() => {
          this.setState({ number: 1 })
        })
        this.setState({ number: 2 })
        ReactDOM.flushSync(() => {
          this.setState({ number: 3 })
        })
        this.setState({ number: 4 })
      }
      render () {
        const { number } = this.state
        console.log(number) // 打印什么??
        return 
          
    {number}
          测试flushSync     
      } }

    先不看答案,点击一下按钮,打印什么呢?

    // 控制台打印:
    0
    3
    4
    1
    

    首先 flushSync this.setState({ number: 3 })设定了一个高优先级的更新,所以3 先被打印;

    然后 2 4 被批量更新为 4。

    相信这个 Demo 让我们更深入了解了 flushSync。

    3.3.6、findDOMNode

    findDOMNode用于访问组件DOM元素节点,react推荐使用ref模式,不期望使用findDOMNode。

    ReactDOM.findDOMNode(component)
    

    注意的是:

    接下来让我们看一下,findDOMNode 具体怎么使用的:

    class Index extends React.Component {
      handerFindDom = () => {
        console.log(ReactDOM.findDOMNode(this))
      }
    
      render () {
        return (
          
            
    hello,world
            获取容器dom       
        )   } }

    我们完全可以将外层容器用 ref 来标记,获取捕获原生的 dom 节点。

    3.3.7、unmountComponentAtNode

    从 DOM 中卸载组件,会将其事件处理器和 state 一并清除。 如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true ,如果没有组件可被移除将会返回 false 。

    我们来简单举例看看 unmountComponentAtNode 如何使用?

    function Text () {
      return 
    hello,world
    } class Index extends React.Component {   node = null   constructor(props) {     super(props)     this.state = {       numer: 1     }   }   componentDidMount () {     /* 组件初始化的时候,创建一个 container 容器 */     ReactDOM.render(, this.node)   }   handerClick = () => {     /* 点击卸载容器 */     const state = ReactDOM.unmountComponentAtNode(this.node)     console.log(state)   }   render () {     return (       
             this.node = node} >
            click me            )   } }

    四、Hook

    4.1、Hook简介

    4.1.1、使用Hook的理由

    1. Hook没有破坏性的改动,当前版本完全可用且向后兼容,不会影响对React的概念理解,反而提供了更直接的API:props,state,context,refs以及生命周期。
    2. Hook 使你在无需修改组件结构的情况下复用状态逻辑,这使得在组件间或社区内共享 Hook 变得更便捷。
    3. class组件常常在componentDidMount和componentDidUpdate中获取数据,但是同一个componentDidMount中可能还要别的逻辑,比如事件监听等,而之后需要在componentWillUnmount中清除,互相关联的代码被拆分到两个地方,而不相干的代码却在同一个地方,如此很容易产生 bug,并且导致逻辑不一致。为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
    4. class组件需要你首先了解JavaScript的this指向问题,还不能忘记绑定事件处理器,它更像是学习React的一种屏障。而Hook的出现可以使开发者在不使用class组件的情况下使用更多的React特性。

    4.1.2、Hook使用规则

    Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

    4.2、State Hook

    4.2.1、useState

    声明State变量

    // class component
    class Example extends React.Component {
      constructor (props) {
        super(props)
        this.state = {
          count0
        }
      }
    
    // function component
    import { useState } from 'react';
    
    function Example () {
      const [count, setCount] = useState(0)
    }
    

    调用 useState 方法的时候做了什么?

    它定义一个 “state 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 banana。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

    useState 需要哪些参数?

    useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)

    useState 方法的返回值是什么?

    返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。

    4.2.2、惰性初始 state

    initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

    const [state, setState] = useState(() => {
      const initialState = someExpensiveComputation(props)
      return initialState
    })
    

    4.2.3、方括号有什么用?

    const [fruit, setFruit] = useState('banana')
    

    方括号其实是数组解构,它意味着我们创建了fruit和setFruit两个变量,fruit是useState返回的第一个值,而setFruit是第二个值。

    它等价于以下代码:

    var fruitStateVarible = useState('banana')
    var count = fruitStateVarible[0]
    var setCount = fruitStateVarible[1]
    

    当我们使用 useState 定义 state 变量时候,它返回一个有两个值的数组。第一个值是当前的 state,第二个值是更新 state 的函数。使用 [0] 和 [1] 来访问有点令人困惑,因为它们有特定的含义。这就是我们使用数组解构的原因。

    4.2.4、更新state变量

    // class component
    
    
    // function component
    <button onClick={() => setCount(count + 1)}>
      Click me
    button>
    

    在function组件中,我们没有this,也在声明state变量count的时候生命过改变它的方法setCount,所以直接调用setCount和count变量即可。

    4.2.5、更新state的写法

    const DemoState = (props) => {
      const [number, setNumber] = useState(0)
      return (
        <div>
          <span>{number}span>
       <button onClick={() => {
         setNumber(number + 1) 	/* 写法一 */
      setNumber(number => number + 1 ) 	/* 写法二 */
      console.log(number) 	/* 这里的number是不能够即时改变的,可以通过useEffect拿取最新值  */
       }}>
      num++
       button>
        div>
      )
    }
    

    4.2.6、使用useState写待办列表组件,具有输入、添加、删除功能

    import React, { useState } from "react";
    
    export default function App () {
      const [text, setText] = useState("")
      const [list, setList] = useState(["aa""bb""cc"])
      
      const handleChange = (e) => {
        setText(e.target.value)
      }
      
      // 点击按钮给list追加值
      const handleAdd = () => {
        setList([...list, text])
        setText("")
      }
      
      // 删除对应下标的值
      const handleDel = (index) => {
        var newList = [...list]
        newList.splice(index, 1)
        setList(newList)
      }
      
      return (
        <div>
          <input onChange={ handleChange } value={text} />
          <button onClick={ handleAdd }>添加到列表button>
          <ul>
            {
              list.map((item, index) => {
                return <li key="index">
                  {item}
                  <button onClick={() => handleDel(index)}>删除button>
                li>
              })
            }
          ul>
          {!list.length && <div>暂无div>}
        div>
      );
    }

    4.3、Effect Hook

    4.3.1、什么是副作用?什么是useEffect?

    你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。

    useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。(我们会在使用 Effect Hook 里展示对比 useEffect 和这些方法的例子。)

    当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。

    例子:

    import { useEffect, useState } from 'react'
    
    function App () {
      const [count, setCount] = useState(0)
    
      useEffect(() => {
        document.title = `you click ${count} times`
      })
    
      return (
        
           setCount(count + 1)}>click
        
      )
    }
    

    useEffect做了什么?

    通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

    为什么在组件内部调用useEffect?

    将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

    useEffect 会在每次渲染后都执行吗?

    是的,默认情况下,它在第一次渲染之后每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

    4.3.2、不需要清除的effect

    有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的:

    // class component
    class Example extends React.Component {
      constructor (props) {
        super(props)
        this.state = {
          count: 0
        }
      }
    
      componentDidMount () {
        document.title = `You clicked ${this.state.count} times`
      }
      componentDidUpdate () {
        document.title = `You clicked ${this.state.count} times`
      }
    
      render () {
        return (
          
            

    You clicked {this.state.count} times

             this.setState({ count: this.state.count + 1 })}>           Click me                
        )   } // function component import React, { useState, useEffect } from 'react' function Example () {   const [count, setCount] = useState(0)   useEffect(() => {     document.title = `You clicked ${count} times`   })   return (     
          

    You clicked {count} times

           setCount(count + 1)}>         Click me            
      ) }

    我们可以在class组件中发现,我们如果需要一个逻辑操作在渲染之后执行,但是class组件没有提供这样的方法,所以我们得在componentDidMount和componentDidUpdate两个地方重复调用它。而function组件中我们不难看出,只需要调用useEffect就可以了,因为useEffect会在第一次渲染和每次更新之后执行。(我们稍后会在:需要清除的effect里讲到如何控制它)。

    4.3.3、需要清除的effect

    之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

    假设我们有一个ChatAPI模块,用来订阅好友在线状态。

    4.3.3.1、首先我们来看class组件

    componentDidMount和componentWillUnmount相互对应,使用生命周期函数迫使我们拆分开了这些逻辑代码,即使订阅和取消订阅都是作用于同一个副作用的逻辑:

    // class component
    class FriendStatus extends React.component {
      constructor (props) {
        super(props)
        this.state = { isOnline: null }
        this.handleStatusChange = this.handleStatusChange.bind(this)
      }
    
      componentDidMount () {
        ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange)
      }
      componentDidUpdate () {
        ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange)
      }
      componentWillUnmount () {
        ChatAPI.unsubscribeToFriendStatus(this.props.friend.id, this.handleStatusChange)
      }
    
      handleStatusChange (status) {
        this.setState({
          isOnline: stutas.isOnline
        })
      }
    
      render () {
        ...
      }
    }
    

    4.3.3.2、再来看function组件

    可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

    import React, { useState, useEffect } from 'react'
    
    function FriendStatus (props) {
      const [isOnline, setIsOnline] = useState(null)
    
      useEffect(() => {
        function handleStatusChange(status) {
          setIsOnline(status.isOnline)
        }
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
        // 特别的清除方式在于这里,这里也不需要一定是具名函数,也可以是箭头函数:
        return function cleanup() {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
        }
      })
    
      if (isOnline === null) {
        return 'Loading...'
      }
      return isOnline ? 'Online' : 'Offline'
    }
    

    4.3.3.3、为什么要在 effect 中返回一个函数?

    这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

    4.3.3.4、React 何时清除 effect?

    React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。

    4.3.3.5、useEffect有效的划分开了不同功能的代码

    既然如此,那么我们使用Hook替代声明周期函数的目的已经很明确了,在下面的例子中我们可以看到Hook有效的划分开不同功能的逻辑代码:

    function FriendStatusWithCounter (props) {
      const [count, setCount] = useState(0)
      useEffect(() => {
        document.title = `You clicked ${count} times`
      })
    
      const [isOnline, setIsOnline] = useState(null)
      useEffect(() => {
        function handleStatusChange(status) {
          setIsOnline(status.isOnline)
        }
    
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
        }
      })
      // ...
    }
    

    Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

    4.3.3.6、解释: 为什么每次更新的时候都要运行 Effect

    继续借用上述的订阅和解绑订阅的例子,在class组件中,常因为忘记处理componentDidUpdate的逻辑而产生bug:

    componentDidMount () {
      ChatAPI.subscribeToFriendStatus(this.props.friend.id, handleStatusChange)
    }
    componentDidUpdate (prevProps) {
      // 需要解绑上个props的friend.id
      ChatAPI.unsubscribeToFriendStatus(prevProps.friend.id, handleStatusChange)
      ChatAPI.subscribeToFriendStatus(this.props.friend.id, handleStatusChange)
    }
    componentWillUnmount () {
      ChatAPI.unsubscribeToFriendStatus(this.props.friend.id, handleStatusChange)
    }
    

    而useEffect不需要这样的逻辑处理,当我们return一个清除函数的时候,useEffect会在调用一个新的effect之前对上一个effect进行清理。

    为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:

    // Mount with { friend: { id: 100 } } props
    ChatAPI.subscribeToFriendStatus(100, handleStatusChange)     // 运行第一个 effect
    
    // Update with { friend: { id: 200 } } props
    ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange) // 清除上一个 effect
    ChatAPI.subscribeToFriendStatus(200, handleStatusChange)     // 运行下一个 effect
    
    // Update with { friend: { id: 300 } } props
    ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange) // 清除上一个 effect
    ChatAPI.subscribeToFriendStatus(300, handleStatusChange)     // 运行下一个 effect
    
    // Unmount
    ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange) // 清除最后一个 effect
    

    4.3.3.7、那么如何去阻止effect无意义的调用呢?也即effect的优化

    你可能会想,如此高频的去执行effect和执行清理,性能上面的损耗会很大。在class组件中,我们通过componentDidUpdate函数的两个参数prevProps和prevState可以去做对比判断是否渲染,useEffect同样可以达到这样的效果,而且要更加简便:

    // class component
    componentDidUpdate (prevProps, prevState) {
      if (prevState.count !== this.state.count) {
        document.title = `You clicked ${this.state.count} times`
      }
    }
    
    // function component
    useEffect(() => {
      document.title = `You clicked ${count} times`
    }, [count])
    

    useEffect接受第二个参数,用来判断之前effect的count和当前effect的count做对比,如果相等就不会重新渲染,如果不相等就会执行渲染。

    而且第二个参数数组中如果有多个元素,只要有一个元素发生改变,都会引起渲染。

    这样的机制对于有清除逻辑的effect同样适用:

    useEffect(() => {
      function handleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
    
      ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
      };
    }, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
    

    我们再来看个更实际的例子:

    import './App.css'
    import { useEffect, useState } from 'react'
    
    function App () {
      const [count, setCount] = useState(0)
    
      useEffect(() => {
        document.title = `you click ${count} times`
        console.log(`render: ${count} times`)
        return () => {
          document.title = `React 18+`
        }
      }, [count])
    
      function changeCount () {
        if (count < 10) {
          setCount(count + 1)
        } else {
          setCount(10)
        }
      }
    
      return (
        
           changeCount()}>click
        
      )
    }
    
    export default App
    
    // 浏览器打印出:
    render: 0 times
    render: 1 times
    render: 2 times
    render: 3 times
    render: 4 times
    render: 5 times
    render: 6 times
    render: 7 times
    render: 8 times
    render: 9 times
    render: 10 times 
    // 之后就的click事件就不会打印任何东西了,也即没有触发useEffect
    

    在未来的react版本中,可能会在构建useEffect的时候自动添加第二个参数。

    4.3.4、effect 执行时机详细说明

    与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

    然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

    虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

    4.4、自定义Hook

    4.4.1、使用自定义Hook:

    // 自定义Hook
    import { useState, useEffect } from 'react'
    
    function useFriendStatus (friendId) {
      const [isOnline, setIsOnline] = useState(null)
    
      function handleStatusChange (status) {
        setIsOnline(status.isOnline)
      }
    
      useEffect(() => {
        ChatAPI.subscibeFriendStatus(friendId, handleStatusChange)
        return () => {
          ChatAPI.unsubscibeFriendStatus(friendId, handleStatusChange)
        }
      })
    
      return isOnline
    }
    
    // 在别的组件中使用自定义Hook
    function FriendList (props) {
      const isOnline = useFriendStatus(props.friend.id)
    
      return (
        
          {props.friend.name}
        
      )
    }
    

    不管这个自定义Hook被调用多少次,它们的state都是独立的,Hook是一种复用状态逻辑的方式,但是它不复用state本身。

    事实上 Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。

    自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。

    你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器等等。

    4.4.2、在多个Hook之间传递信息

    Hook 本身就是函数,因此我们可以在它们中间传递信息。

    我们接着上面的聊天程序的另一个组件来说明,该组件会显示当前选定的好友是否在线:

    const friendList = [
      { id: 1, name: 'Phoebe' },
      { id: 2, name: 'Rachel' },
      { id: 3, name: 'Ross' },
    ]
    
    function ChatRecipientPicker () {
      const [recipientID, setRecipientID] = useState(1)
      const isRecipientOnline = useFriendStatus(recipientID)
    
      return (
        <>
          
          
        
      );
    }
    

    每次通过 selector 改变了 recipientID 这个 state 状态的时候,都把这个新的 state 传递给了 useEffect,useEffect 就可以取消订阅上一个 id 的好友,然后订阅新的 recipientID 好友。

    4.5、Hook规则

    4.5.1、只在顶层使用 Hook

    不要再循环、条件或嵌套函数中调用Hook,确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

    4.5.2、只在React函数中使用 Hook

    不要再普通的 JavaScript 函数中调用 Hook,你可以:

    遵循此规则,确保组件的状态逻辑在代码中清晰可见。

    4.5.3、为什么只能在顶层使用 Hook?

    React Hook 的设计中,我们可以在单个组件中使用多个 State Hook 和 Effect Hook。那么 React 怎么知道哪个 State 对应哪个 useState?答案是:调用 Hook 的顺序。

    我们下面来看符合规则的调用:

    // ------------
    // 首次渲染
    // ------------
    useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
    useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
    useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
    useEffect(updateTitle)     // 4. 添加 effect 以更新标题
    
    // -------------
    // 二次渲染
    // -------------
    useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
    useEffect(persistForm)     // 2. 替换保存 form 的 effect
    useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
    useEffect(updateTitle)     // 4. 替换更新标题的 effect
    

    只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?

    下面来看不符合规则的调用:

    //  在条件语句中使用 Hook 违反第一条规则
    if (name !== '') {
      useEffect(function persistForm() {
        localStorage.setItem('formData', name)
      })
    }
    
    // 调用顺序就会发生改变:
    useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
    // useEffect(persistForm)  //  此 Hook 被忽略!
    useState('Poppins')        //  2 (之前为 3)。读取变量名为 surname 的 state 失败
    useEffect(updateTitle)     //  3 (之前为 4)。替换更新标题的 effect 失败
    

    React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应得是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。

    这就是为什么 Hook 需要在我们组件的最顶层调用。

    4.5.4、那么如果想要有条件的执行effect怎么办?

    答案是:将判断放在 useEffect 内部。

    useEffect(function persistForm() {
      // 将条件判断放置在 effect 中
      if (name !== '') {
        localStorage.setItem('formData', name)
      }
    })

    4.6、其他Hook

    4.6.1、useReducer

    import React, { useReducer } from 'react'
    
    const MyChildren = (props) => {
      console.log(props)
    }
    
    const DemoUseReducer = () => {
      /* number为更新后的state值,  dispatchNumbner 为当前的派发函数 */
      const [number, dispatchNumbner] = useReducer((state, action) => {
        const { payload, name } = action
        /* return的值为新的state */
        switch (name) {
          case 'add':
            return state + 1
          case 'sub':
            return state - 1
          case 'reset':
            return payload
        }
        return state
      }, 0)
      
      return 
        当前值:{number}     { /* 派发更新 */}      dispatchNumbner({ name: 'add' })}>增加      dispatchNumbner({ name: 'sub' })}>减少      dispatchNumbner({ name: 'reset', payload: 666 })}>赋值     { /* 把dispatch 和 state 传递给子组件 */}        
    }

    4.6.2、useCallback

    useMemo 和 useCallback 接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果, useCallback 返回的是函数。 返回的 callback 可以作为 props 回调函数传递给子组件。

    import React, { useState, useEffect, useCallback } from 'react'
    
    const DemoChildren = ({ getInfo }) => {
      /* 只有初始化的时候打印了 子组件更新 */
      console.log('子组件更新')
      useEffect(() => {
        getInfo('子组件')
      }, [getInfo])
    
      return 
    子组件
    } const DemoUseCallback = () => {   const [number, setNumber] = useState(1)   const getInfo = useCallback((sonName) => {     console.log(sonName)   }, [])   return (     
          {/* 点击按钮触发父组件更新 ,子组件不会更新 */}        setNumber(number + 1)} >增加            
      ) }

    4.6.3、useMemo

    useMemo接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为dep依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值

    4.6.3.1、应用场景1:缓存一些值,避免重新执行上下文:

    const number = useMemo(() => {
      {/* ....大量的逻辑运算 */}
      return number
    }, [props.number])  // 只有 props.number 改变的时候,重新计算number的值
    

    4.6.3.2、应用场景2:减少不必要的 dom 循环

    // 用 useMemo 包裹的 list 可以限定当且仅当 list 改变的时候才更新此 list,这样就可以避免 selectList 重新循环
    
    {
      useMemo(() => (
        
          {      selectList.map((item, index) => {                  {item.patentName}                  })       }     
      ), [selectList]) }

    4.6.3.3、应用场景3:减少子组件渲染次数

    // 只有当 props 中,list 列表改变的时候,子组件才渲染
    
    const goodListChild = useMemo(() => , [ props.list ])
    

    4.6.4、useRef

    在类组件中,使用 ref 可以引入 react 中的 createRef() 函数去初始化,但在 function 组件中,使用 useRef 这个钩子性能更强。

    // src/App.jsx
    import React, { useRef } from 'react'
    import Input from './pages/Input.jsx'
    
    const App = () => {
      const inputRef = useRef()
      function handleClick () {
        console.log(inputRef)
        console.log(inputRef.current)
        inputRef.current.focus()
      }
      
      return 
             点击   
    } // src/pages/Input.jsx import React, { forwardRef } from 'react' const Input = forwardRef((props, inputRef) => {   return  }) export default Input

    4.6.5、useLayoutEffect

    useLayoutEffect 和 useEffect 的区别简单来说就是调用的时机不同,useLayoutEffect 调用时机其实更像 componentDidMount 和 componentDidUpdate,但是需要注意的是,useLayoutEffect 会在 DOM 树更新完成之后同步调用,而 useEffect 会在渲染完成之后调用。不难看出,useLayoutEffect 的调用时机会阻塞 UI 的渲染,所以在性能上面 useEffect 是更胜一筹的。

    那么什么时候,我们更适合使用 useLayoutEffect呢?当我们想避免页面抖动的时候可以使用 useLayoutEffect。页面抖动会在使用 useEffect 的时候出现,因为它的调用时机是在 DOM 树和渲染树都完毕的时候才会调用,这个时候如果使用它修改 DOM,那么就会引起浏览器的回流和重绘。而使用 useLayoutEffect 的话,它的调用时机是在 DOM 树更新之后,渲染树更新之前,这些 DOM 修改会和 React 做出的更改一次性的渲染到屏幕上,只有一次回流和重绘的代价。显然修改 DOM 时,useLayoutEffect 比起 useEffect 是更好的选择。

    useLayoutEffect (() => {
      document.getElementById('test').style.display = 'none'
      return () => {
        document.getElementById('test').style.display = 'flex'
      }
    }, []) 
    

    4.6.6、useContext

    我们可以使用 useContext ,来获取父级组件传递过来的 context 值,这个当前值就是最近的父级组件 Provider 设置的 value 值,useContext 参数一般是由 createContext 方式引入 ,也可以父级上下文 context 传递 ( 参数为 context )。useContext 可以代替 context.Consumer 来获取 Provider 中保存的 value 值。

    import React, { useContext } from 'react'
    const Context = React.createContext()
    
    /* 用useContext方式 */
    const DemoContext = () => {
      const value = useContext(Context)
      
      return 
     my name is {value.name}
    } /* 用Context.Consumer 方式 */ const DemoContext1 = () => {   return (            {(value) => 
     my name is {value.name}
    }     
      ) } export default function Test () {   return (                                             ) }

    五、CSS Module

    我们在写样式之后,不管是 Vue 还是 React,最后都会将 CSS 注入到 HTML 文件 head 标签下的各个 style 文件中去。那么如果两个文件都具有同样的 DOM 和 class 名或者 id 名,就会造成样式污染,最后加载的文件一定会覆盖之前加载的文件的同名 DOM 的样式。那么两个框架是怎么解决这样的问题的?

    Vue 具有 scope 的 api,只需要在 style 标签上写出来即可防止样式污染,它的原理是将 DOM 加入了随机生成的哈希值作为自定义命名,在附加样式时就可以正确的区分每个 DOM 的样式了:

    // tag.vue
    
    
    
      // ...
    
    
    
    .tag {
     color: #595959;
      &-text {
        font-weight: 550;
      }
    }
    
    
    // list.vue
    
    
    
      // ...
    
    
    
    .list {
     color: #797979;
    }
    
    
    // 浏览器控制台
    for example
    data-v-f8e9e086.tag {
      color: #595959;
    }
    data-v-f8e9e086.tag-text {
      font-weight: 550;
    }
    
    
    
  • for example
  • data-v-c3m6a324.tag {   color: #797979; }

    React 没有 Vue 这样的 scope Api 可以使用,不过有一样思路的解决方式,那就是修改 css 文件名,在 .css 前加入 .module 的写法,再使用 import 去引入这个包,就可以拿到对象,而且因为是包的形式,所以样式表里的选择器命名也会变化,一般按照模块名 *_ 选择器 _ *随机值的格式,比如下方例子的 .User_spanActive_eRszwc :

    // src/pages/user/user.module.css
    .spanActive {
      color: #595959;
    }
    #buttonActive {
      color: #797979;
    }
    /* module 只会将 class 选择器和 id 选择器进行命名更改,而 ul li 不会,所以要在他之前限制在 .user 下的 ul 和 li */
    .user ul li {
      padding: 10px 20px;
    }
    /* 使用 :global 的语法的话,module 不会更改 #textColor 的命名,以达到全局样式的效果 */
    :global(#textColor) {
      color: #000000;
    }
    
    // src/pages/user/index.jsx
    import style from '@/pages/user/user.module.css'
    
    export default function User () {
      {/* module 会把样式文件里的 .spanActive 更名为 .User_active_eRszwc 这样的随机命名方式,达到避免样式污染的目的 */}
      console.log(style.spanActive)  // .User_spanActive_eRszwc
      console.log(style.buttonActive)  // #User_buttonActive_35sad2
    
      return (
        
       商城
          
        {     list.map(item => {    return 
    • {item.name}
    •     })   }       
           ) }

    六、Router

    这里暂且只写V5版本的,实际上V6才有比较好用的路由表解决方案,类似于VueRouter,感兴趣的可以自行去学习V6版本。

    6.1、安装

    pnpm install --save react-router-dom@5
    

    6.2、基础使用

    React 和 Vue 路由的书写方式很不同,在 React 中一切都是组件,包括路由也是:

    // src/router/index.js
    import { HashRouter, Route, Redirect, Switch } from 'react-router-dom'
    
    export default function MyRouter () {
      return (
        
          
            
            
    
            {/* 重定向,模糊匹配斜杠,这样会导致其他路径也被匹配到,所以可以添加 exact 来设置为精准匹配 */}
            
    
            {/* 404页面,当所有的路径都没有匹配到就会走到这个组件 */}
            
          
        
      )
    }
    
    // src/App.js
    import MyRouter from '@/router/index.js'
    
    export default function App () {
      return (
        
          
        
      )
    }
    

    从上面的例子,我们可以看到每一个路由都是由 Route 标签的 path 和 component 来进行设置的,它们都是一个个组件。

    就连在 Vue 中的配置路由的 redirect 也是以标签组件的形式引入进来的。

    6.2.1、HashRouter

    HashRouter 跟 Vue 中的 mode 设置为 hash 是一样的概念,是以 /#/ 的方式来表现页面的路由的。

    那么与之相对的则有 BrowserRouter,也就是 history 的模式了。

    6.2.2、Route

    根据上面的例子,也不难发现,Route 有两个很重要的属性来决定路径和组件的:path 决定路径,component 决定组件渲染。

    Route 还支持 exact、strict、location等属性。

    6.2.3、Redirect

    Redirect 重定向,通过 from 属性规定匹配的内容,通过 to 属性规定跳转的地址。

    Redirect 会在所有 Route 都没有匹配到的时候,还有页面刷新的时候触发,所以这里就有了个小问题:假设我们的 to 设置的是单页面应用的首页,然后用户点击导航,从首页来到了用户中心页面,此时如果刷新页面,Redirect 会把你重定向到 to 设置的地址,也就是首页。这样的效果显然不符合大多数的业务场景。那当已经正确找到 Route,这时怎么阻止 Redirect 的触发呢?这里就要用到另一个 React api:Switch,下一章会提到它。

    Redirect 还有第二个小问题,那就是 exact 精确匹配的配置,它接受一个 bool 值,为真时意为开启,为假时则反之。 为什么说它有个小问题,因为回顾上面的 App.js 中的 Redirect 标签,去掉 exact 属性的话,当用户输入一个不存在的地址时,它会一直重定向 to 属性的地址,而不会继续往下走到 404 的 Route。这是因为 Redirect 默认的模糊匹配,假如我们要去 https://xxx.com/#/example 这个地址,遇到第一个 Route 是 /user 的不对,接着往下走 /shopping 也不对,再往下走遇到 Redirect,虽然我们没有 /example 这个地址,但是 /example 还是包含了斜杠,于是 Redirect 把地址重定向到 /user 去了。这时就要用到 exact 这个属性,来精确匹配,只有根路径 / 才会触发重定向。

    6.2.4、Switch

    Switch 很像我们初识 JavaScript 的 Switch,我们知道 Switch 有 case 和 break,React Router 的 Switch 同样如此,只是不需要我们去手动写条件。当匹配到正确的 Route 的时候,就会 break 跳出,不会继续往下走。如果都没有匹配到,就会继续往下走,一般会走我们设置好的 404 页面(这里就不在重复的去写代码了,可以去看上面的代码,那个没有写 path 的 Route 标签就是了,也有注释标注)。

    当然除了 404 的功能,Switch 因为其具有 break 的特点,还解决了上一章遗留的刷新页面的问题。

    6.2.5、动态路由、路由传参

    在 Vue 中,我们希望路由传递参数时,分两种情况,一种是 query 的形式以 ? 拼接在 URL 后面,一种是 params 的形式添加在路由对象的 params 下面:

    this.$router.push({
      path: `/detail`,
      query: {
        id: this.id
      }
    })
    
    this.$router.push({
      name: 'Detail',
      params: {
        id: this.id
      }
    })
    
    // src/component/Detail.vue
    console.log(this.$route.query)
    console.log(this.$route.params)
    

    在 React 中,可以在要用到动态传参的 Route 标签中留下占位符 : ,下面例子中的 :myId,就像 Vue 中传参对象的属性名,让我们使用编程式路由或者声明式路由跳转到这个 Route 时,占位符预留的这个属性就会截取到它,且会把它放在 props 下的 params中去:

    // src/router/index.js
    import ...
    import Detail from '@/pages/component/Detail.jsx'
    
    export default function MyRouter () {
      return (
        
          
            ...
            
            ...
          
        
      )
    }
    
    // src/pages/Shopping.jsx
    import ...
    import React, { useHistory } from 'react'
    
    export default function Shopping () {
      const history = useHistory()
      function handleClick (id) {
        history.push(`/detail/${id}`)
      }
      return (
        ...
          
      )
    }
    
    // src/component/Detial.jsx
    import ...
    
    export default function Detail (props) {
      console.log(props.match.params) // { myId: 888 }
      return (
        ...
      )
    }
    

    还有一种方式是 query 传参和 state 传参,和 Vue 中的 this.$router.push 非常相似,同样可以做到传参和动态路由的形式,当然这里就不需要使用 Route 标签的占位符了,尽量不要两掺去写:

    // function 组件中两个可以存放参数的地方,一个是 location.query,一个是 location.state
    props.history.push({
      pathname: '/user',
      query: {
        id: '888'
      }
    })
    console.log(props.location.query.id)  // 888
    
    props.history.push({
      pathname: '/user',
      state: {
        id: '888'
      }
    })
    console.log(props.location.state.id)  // 888
    
    // 在 class 组件中,自然是 this.props
    this.props.history.push({
      pathname: '/user',
      query: {
        id: '888'
      }
    })
    console.log(this.props.location.query.id)  // 888
    
    // 利用 Hook,效果和 props 一样,因为 component 是在 Route 标签中实例化的,props 能拿到路由信息,而 Hook 就是封装好的而已
    import { useHistory } from 'react'
    const history = useHistory()
    history.push({
      pathname: '/user',
      query: {
        id: '888'
      }
    })
    console.log(props.location.query.id)  // 888
    

    需要注意的是,query 和 state 传参,如果刷新了页面或者复制地址给别人,会导致拿不到这个参数,和 Vue 的 this.$router.push 一样,因为这个参数需要上一历史栈页面的传递才能拿到。那么如果你想解决这个问题,就可以利用第一种方式,也就是动态路由的方式,因为它改变了 URL 地址。

    6.2.6、路由拦截

    在 Vue 中有一套严谨的路由拦截方式,在实例化 router 路由对象时,可以对这个对象设置好路由拦截。

    但是在 React 中的 V5 版本 React Router 中,没有提到过路由拦截相关的配置。我们可以通过下面这种方式去判断 render 的东西和方式,来达到路由拦截的目的:

    // src/router/index.js
    import ...
    
    export default function MyRouter () {
      function isAuth () {
        return localstorage.getItem('token') ? true : false
      }
    
      return (
        
          
    
          {/*
            这里从 component 切换成 render 的方式渲染组件会导致 Route 组件无法在内部实例化组件,从而得不到 props.history
            props.match props.location 这些参数,Recat 也相应的提供了别的方式,这些 props 会以参数的形式在 redner 函数里
            传递,我们需要手动的把它拿出来再传递给组件
          */}
           {
           return isAuth() ?  : 
          }} />
    
          
        
      )
    }
    

    6.2.7、withRouter

    当你的组件 props 无法拿到路由信息,且父组件也拿不到,用 props 去传递路由信息会传递很多层的时候,可以使用 React Router 提供的高阶组件 withRouter 包裹想使用路由信息的组件即可。

    // src/pages/Shoppng.jsx
    import ShoppingItem from '@/component/ShoppingItem.jsx'
    
    export default function Shopping () {
      const withRouterItem = withRouter(ShoppingItem)
      return (
        
       {   list.map(item => {     return    })    }     
      ) } // src/component/ShoppngItem.jsx import { withRouter } from 'react-router-dom' function ShoppingItem (props) {   const {name, imgUrl, shoppingId} = props   function handleClick (id) {     {/* 这种方式的传递首先需要父组件给子组件传递路由信息,不然是拿不到 location history这些东西的,     那如果父组件没有就需要父组件的父组件一层层传递下来,很麻烦,于是这里要引入 withRouter 的概念 */}     props.history.push(`/detail/${id}`)   }   return (   handleClick(shoppingId)}>           {name}     ) } {/* withRouter 包裹后的组件可以在其 porps 里拿到 location、match、history 等等,   当然你在使用这个组件的拿路由信息的时候也有一定的代价,那就是你的组件会被包裹一层 withRouter 的 DOM 结构,   所以实际上就是 withRouter 通过实例化你传入的组件,把你的组件作为子组件并传递给子组件 props 而已 */} export default withRouter(ShoppingItem)

    6.3、反向代理

    跟 Vue 不同的是,你需要安装一下下面这个插件才能使用反向代理相关的配置能力:

    pnpm install http-proxy-middleware --save
    // or
    yarn add http-proxy-middleware
    

    然后在 src 文件夹下面新建 setupProxy.js 文件,修改文件内容为以下文本:

    const { createProxyMiddleware } = require('http-proxy-middleware')
    
    module.exports = function(app) {
      app.use(
        '/api',
        createProxyMiddleware({
          target: 'http://i.maoyan.com',
          changeOrigin: true
        })
      )
    }
    

    跟 Vue 的 vue.config.js 中的 proxy 配置项内容差不多,只是需要注册 http-proxy-middleware 这个中间件。

    use 中的参数,第一个是限制以什么开头儿的需要反向代理,例子中是 '/api' 那么我们在请求 '/api/shopping/getShoppingList' 这样的一些接口地址的时候,就能触发反向代理。第二个参数就是注册中间件。target 就是服务器的地址,比如你要在猫眼电影的服务器拿数据,那就拼上他们的服务器地址。target 是域名的话,需要配置 changeOrigin: true 。

    配置完成之后需要重启终端,setupProxy.js 文件的配置才能生效。

    七、Redux

    7.1、Flux与Redux

    Flux 是一种思想,也即全局状态树的思想,而 Redux 则是实现这种思想的一种插件,同样思想的插件还有很多,比如:Facebook Flux,它是 Facebook 官方根据 React 设计的状态管理插件,但是它并没有 Redux 好用。这就是 Flux 和 Redux 的关系。

    Redux 三大原则:

    7.2、Redux工作流

    React Component
      ⬇
      ⬇  onClick 页面事件
      ⬇
    Action
      ⬇
      ⬇  dispatch 提交到仓库
      ⬇
    Store
      ⬇
      ⬇  (prevState, action) 旧状态和操作提交给Reducer处理
      ⬇
    Reducers
      ⬇
      ⬇  newState 生成新状态交给React组件
      ⬇
    React Component 
    

    可以发现 Store 也是根据 Reducer 去实现更新状态的,这跟 React Hook 里的 useReducer 类似,只不过 Redux 帮我们去实现了更新的逻辑。当我们在实现跨多层级通信的需求时,Redux 更加方便更加节省代码。如果使用过 Vue 框架生态的 Vuex 仓库的话,更能理解全局 Store 的好处,它们在实现上可能不同,但是目的上是一致的。

    7.3、Redux实战

    我们知道 Redux 的原理是订阅发布模式,但是其实在写法上,你也不难看出它的渊源。React 不像 Vue,它没有很多封装和指令。所以你可以很直观的从写法上看出 Redux 和订阅发布模式的关系:

    // store/index.jsx
    import { createStore } from 'redux'
    
    const reducer = (prevState = {
      show: true
    }, action) => {
      console.log(action)  {/* {type: 'hide-tabbar'} */}
      let newState = {...prevState}
      switch (action.type) {
     case 'hide-tabbar':
       newState.type = false
       return newState
        case 'show-tabbar':
       newState.type = true
       return newState
     default:
       return prevState
      }
      return prevState
    }
    const store = createStore(reducer)
    export default store
    
    
    // App.jsx
    import React, { useEffect, useState } from 'react'
    import MyRouter from '@/router/index.jsx'
    import Tabbar from '@/components/tabbar/index.jsx'
    import store from '@/store/index.jsx'
    
    function App () {
      const [tabbarStatus, setTabbarStatus] = useState(store.getState().show)
    
      useEffect(() => {
          store.subscribe(() => {
      console.log('APP中的订阅', store.getState())
      setTabbarStatus(store.getState().show)
       })
      }, [])
    
      return (
        
       { tabbarStatus &&  }
     
      )
    }
    export default App
    
    
    // Test.jsx
    import React, { useEffect } from 'react'
    import store from '@/store/index.jsx'
    
    function Test () {
      useEffect(() => {
          store.dispatch({ type: 'hide-tabbar' })
          return () => {
      store.dispatch({ type: 'hide-tabbar' })
          }
      }, [])
      
      return (
     
    Test
      ) } export default Test

    store.dispatch() 提交的 type: 'hide-tabbar' 实际上可以通过拆分到别的文件中进行统一管理,我们来改造一下试试:

    // store/actionCreator/tabbarActionCreator.js
    function hide () {
      return {
     type: 'hide-tabbar'
      }
    }
    function show () {
      return {
     type: 'show-tabbar'
      }
    }
    export {
      hide,
      show
    }
    
    
    // Test.jsx
    import React, { useEffect } from 'react'
    import store from '@/store/index.jsx'
    import { hide, show } from '@/store/actionCreator/tabbarActionCreator.js'
    
    function Test () {
      useEffect(() => {
          store.dispatch(hide())
          return () => {
      store.dispatch(show())
          }
      }, [])
      
      return (
     
    Test
      ) } export default Test

    这样拆分开分别区分统一管理的目的,实际上是为了之后处理异步拿数据的目的,我们知道 Redux 是为了方便我们管理全局状态而被设计出来,它本身就是为了存储数据,那么我们异步去拿接口数据的方法也应放在 Redux 中去,拆分并统一管理 dispatch 就方便了我们以后在其中进行接口请求。

    7.4、Redux原理

    我们从前三章学到了一些 Redux 仓库的方法,比如:store.dispatch()、store.subscribe()、store.getState(),我们这章就来看看 Redux 的源码。

    这是前三章我们用到的声明仓库的写法:

    import { createStore } from 'redux'
    
    const reducer = (prevState, action) => {
      return prevState
    }
    
    const store = createStore(reducer)
    
    export default store
    

    接下来我们就来写一个我们自己的简易版 createStore 方法,用来替代 createStore(reducer),看看能否正常工作:

    function createStoreByMyself (reducer) {
      var list = []
      var state = reducer(undefined, {})
    
      function subscribe(callback) {
        list.push(callback)
      }
    
      function dispatch(action) {
        state = reducer(state, action)
        for (let i in list) {
       list[i] && list[i]()
        }
      }
    
      function getState() {
     return state
      }
    
      return {
        subscribe,
     dispatch,
     getState
      }
    }
    

    第三章的实战项目引入上述方法可以正常使用,但是这个方法只是简易版的封装,不考虑后面 Reducer 合并之类的高阶使用,还有一些错误的回调,只是单纯的用来理解 Redux 的实现原理。

    7.5、Reducer合并

    7.5.1、纯函数的定义

    在说 Reducer 合并之前,我们需要先知道什么是纯函数,因为 Reducer 就是符合纯函数的设计理念的。

    纯函数具有以下特点:

    7.5.2、Reducer合并实战

    之前我们将 reducer 写在 index 声明文件里,这样去做会导致代码量越来越多,最后难以维护。在 Vuex 中我们知道有 module 的概念,去把各个功能模块儿对应的状态仓库分开管理,并在声明文件夹中引入。那么在 Reducer 中同样有相应的理念,我们下面来改造一下下面的代码。

    改造前:

    // @/store/index.js
    import { createStore } from 'redux'
    
    const reducer = (prevState = {
      show: true,
      cityName: '郑州',
      list: []
    }, action) => {
      let newState = {...prevState}
      switch (action.type) {
        case 'hide-tabbar':
          newState.show = false
          return newState
        case 'show-tabbar':
          newState.show = true
          return newState
        case 'change-city-name':
          newState.cityName = action.payload
          return newState
        case 'get-cinema-list':
          newState.list = action.payload
          return newState
        default:
          return prevState
      }
    }
    
    const store = createStore(reducer)
    export default store
    

    改造后:

    // @/store 目录下新建 reducers 文件夹,并新建 TabbarReducer.js 和 CityNameReducer.js
    // @/store/reducers/TabbarReducer.js
    const TabbarReducer = (prevState = {
      show: true
    }, action) => {
      let newState = {...prevState}
      switch (action.type) {
        case 'hide-tabbar':
          newState.show = false
          return newState
        case 'show-tabbar':
          newState.show = true
          return newState
        default:
          return prevState
      }
    }
    export default TabbarReducer
    
    
    // @/store/reducers/CityNameReducer.js
    const CityNameReducer = (prevState = {
      cityName: '北京'
    }, action) => {
      let newState = {...prevState}
      switch (action.type) {
        case 'change-city-name':
          newState.cityName = action.payload
          return newState
        default:
          return prevState
      }
    }
    export default CityNameReducer
    
    
    // @/store/reducers/CinemaListReducer.js
    const CinemaListReducer = (prevState = {
      list: []
    }, action) => {
      let newState = {...prevState}
      switch (action.type) {
        case 'get-cinema-list':
          newState.list = action.payload
          return newState
        default:
          return prevState
      }
    }
    export default CinemaListReducer
    
    
    // @/store/index.js
    import { combinReducers, createStore } from 'redux'
    import CityNameReducer from '@/store/reducers/CityNameReducer.js'
    import TabbarReducer from '@/store/reducers/TabbarReducer.js'
    import CinemaListReducer from '@/store/reducers/CinemaListReducer.js'
    
    const reducer = combinReducers({
      CityNameReducer,
      TabbarReducer,
      CinemaListReducer
    })
    const store = createStore(reducer)
    export default store
    

    这里我们看到了 Redux 的新方法:combinReducers(),它可以将拆散开的各个仓库合并起来。

    但是你还记得订阅者取值的 store.getState() 方法吗?我们在将各个 reducer 外面都额外的套了一层,原来的 get 方法还能拿到吗?

    我们再次拿到第三章的实战代码,打印看看这时 store.getState() 会是什么结构:

    // App.jsx
    import React, { useEffect, useState } from 'react'
    import MyRouter from '@/router/index.jsx'
    import Tabbar from '@/components/tabbar/index.jsx'
    import store from '@/store/index.jsx'
    
    function App () {
      const [tabbarStatus, setTabbarStatus] = useState(store.getState().show)
    
      useEffect(() => {
          store.subscribe(() => {
      console.log('App.jsx中的订阅', store.getState())
      {/*
        没有拆分仓库之前是:
       { show: true, cityName: '郑州' }
              拆分合并之后是:
       { CityNameReducer: { cityName: '郑州' }, TabbarReducer: { show: true }, CinemaListReducer: { list: [] } }
      */}
      {/* setTabbarStatus(store.getState().show) 所以原先的 .show 自然也是拿不到东西的,我们需要改成下面的写法 */}
      setTabbarStatus(store.getState().TabbarReducer.show)
       })
      }, [])
    
      return (
        
       { tabbarStatus &&  }
     
      )
    }
    export default App

    7.6、redux-thunk

    7.6.1、中间件

    在 Redux 里,action 仅仅是携带了数据的普通 JS 对象,action creator 返回的值是这个 action 类型的对象。然后通过 store.dispatch() 进行分发。同步的情况下,这一切很完美,但是 reducer 无法处理异步情况。而中间件 redux-thunk 则为 Redux 赋予了解决异步的能力。

    7.6.2、为什么 reducer 无法处理异步

    这一小节是为了消除疑虑,为什么同步可以,异步却不行。

    还记得第三章 Redux 实战的 Test.jsx 组件么?我们将 store.dispatch() 进行了拆分。

    Redux 本身就是来帮助我们处理数据的,所以我们请求后端接口的方法也应放在 Redux 中去:

    // @/store/reducers/CinemaListReducer.js
    const CinemaListReducer = (prevState = {
      list: []
    }, action) => {
      let newState = {...prevState}
      switch (action.type) {
        case 'get-cinema-list':
          newState.list = action.payload
          return newState
        default:
          return prevState
      }
    }
    export default CinemaListReducer
    
    
    // @/store/actionCreator/CinemaListActionCreator.js
    import axios from 'axios'
    
    function getCinemaList () {
      axios.get('https://xxx/xxx/getCinemaList').then(res => {
     console.log(res)
        return {
          type: 'get-cinema-list',
          payload: res.data.data.cinemas
        }
      }).catch(err => {
     console.log(err)
      })
    }
    export {
      getCinemaList
    }
    
    
    // Test.jsx
    import React, { useState, useEffect } from 'react'
    import store from '@/store/index.jsx'
    import { getCinemaList } from '@/store/actionCreator/CinemaListActionCreator.js'
    
    function Test () {
      const [list, setlist] = useState(store.getState().CinemaListReducer.list)
      useEffect(() => {
        store.dispatch(getCinemaList())
      }, [])
      
      return (
     
    Test
      ) } export default Test // 控制台打印: Error: Action must be plain object.Instead the actual type was 'undefined'.You may need to add middleware to your store setup to handle dispatching other value, Such as 'redux-thunk' to handle dispatching functions.  报错:action 必须是一个普通对象。但是接收到是 undefined。你可能需要在你的仓库中添加中间件去控制 dispatch 其他类型,比如 'redux-thunk'去控制 dispatch 函数。

    那么为什么明明在 axios 的回调里面 return 返回了 type 和 payload,拿到的却是 undefined?原因是 store.dispatch(getCinemaList()) 直接调用之后拿到的实际是 function getCinemaList () {} ,return 不在该函数体内,而是在 axios 的成功回调中,当 axios 异步完成的时候,getCinemaList() 方法体早已经走完了,所以什么也没有返回,action 就得到了 undefined。如果还没有明白的话,我们举个简单的例子来说明为什么得到的undefined:

    function Test () {
      setTimeout(() => {
     return console.log('--setTimeout--')
      }, 3000)
    }
    
    // 这里的 return 是定时器的返回,而 func Test 并没有返回任何东西
    // 而 dispatch 无法接受一个 undefined,它只接受一个普通 JS 对象
    // 所以会报错,并提示你使用中间件处理异步
    store.dispatch(Test())  // Error: Action must be plain object.Instead the actual type was 'undefined'...
    

    7.6.3、常见异步中间件

    常见的异步中间价有:redux-thunk 和 redux-promise,本章讲一下 redux-thunk 的写法,我们来改造一下异步获取数据的方法 getCinemaList:

    // @/store/actionCreator/CinemaListActionCreator.js
    import axios from 'axios'
    
    function getCinemaList () {
      return (dispatch) => {
        axios.get('https://xxx/xxx/getCinemaList').then(res => {
       console.log(res)
          dispatch ({
            type: 'get-cinema-list',
            payload: res.data.data.cinemas
          })
        }).catch(err => {
       console.log(err)
        })
      }
    }
    

    不难发现我们只是在 axios 异步请求数据的方法外面套了一层箭头函数,并接收到了 dispatch 参数,那么源码上面它是如何实现的呢:

    export default function thunkMiddleWare({ dispatch, getState }) {
      return next => action =>
        typeof action === "function" ? action(dispatch, getState) : next(action)
    }
    

    原来中间件判断如果是 func 就会执行它,如果是普通 JS 对象就会继续向下执行。所以参数 dispatch 是中间件执行 func 传参过来的。此时你执行 dispatch 还是会报错的,但是错误由以前接收到 undefined 变成了接收到 function:

    store.dispatch(getCinemaList())   // Error: Action must be plain object.Instead the actual type was 'function'...
    

    改造完之后依然报错的原因是现在还没有配置好中间件依赖,明白了它的原理,我们就来在下一小节安装配置一下 redux-thunk。

    7.6.4、安装配置 redux-thunk

    三种包管理工具的安装方式:

    npm i redux-thunk
    pnpm i redux-thunk
    yarn add redux-thunk
    

    修改 Redux 仓库声明文件 store/index.js 中的写法,我们这里拿第五章 Reducer 合并中的示例代码:

    // @/store/index.js
    import { applyMiddleware, combinReducers, createStore } from 'redux'
    import CityNameReducer from '@/store/reducers/CityNameReducer.js'
    import TabbarReducer from '@/store/reducers/TabbarReducer.js'
    import CinemaListReducer from '@/store/reducers/CinemaListReducer.js'
    import reduxThunk from 'redux-thunk'
    
    const reducer = combinReducers({
      CityNameReducer,
      TabbarReducer,
      CinemaListReducer
    })
    const store = createStore(reducer, applyMiddleware(reduxThunk))
    export default store
    

    我们在第一行额外引入了一个 func 叫做 applyMiddleware,它可以使 Redux 支持中间件,并在第五行引入 redux-thunk 依赖,最后在 createStore 中添加上 applyMiddleware(reduxThunk) 就配置完毕了。

    7.6.5、完善实例代码

    我们再来完善一下 Test.jsx 组件的调用流程:

    // Test.jsx
    import React, { useState, useEffect } from 'react'
    import store from '@/store/index.jsx'
    import { getCinemaList } from '@/store/actionCreator/CinemaListActionCreator.js'
    
    function Test () {
      const [list, setlist] = useState(store.getState().CinemaListReducer.list)
      useEffect(() => {
     if (store.getState().CinemaListReducer.list.length === 0) {
       store.dispatch(getCinemaList())
        } else {
          console.log('走了 store 缓存')
        }
    
     // 订阅
     store.subscribe(() => {
       console.log('cinema 中订阅', store.getState().CinemaListReducer.list)
       setlist(store.getState().CinemaListReducer.list)
        })
      }, [])
      
      return (
     
       
        {     list.map((item) => {       return {item.cinemaName}     })      }    
     
      ) } export default Test

    运行之后发现没有报错了,并且 redux-thunk 还帮我们缓存了数据,每次进入页面不用重新请求接口了,提高了页面加载速度。这样看上去没错,实际上是有问题存在的,因为每次进入页面都会重新订阅一次,当别的 dispatch 调用的时候,所有的订阅者都会被触发,所以进了多少次 Test 页面,重新订阅了多少次,就会被触发多少次,控制台就会打印多少次:

    'cinema 中订阅', [{cinemaName: ...}, {id: ...}, ...]
    'cinema 中订阅', [{cinemaName: ...}, {id: ...}, ...]
    'cinema 中订阅', [{cinemaName: ...}, {id: ...}, ...]
    ...
    

    如果你没有搞明白的话,举个例子,你家里订刊物,同一本刊物你重复订阅了10次,看的东西是一样的,不过你花了10份的钱。

    为什么会重复走 store.subscribe 呢?原来在 React 中当你离开当前页面,页面组件会被销毁,这没问题,可是 store 并不会销毁,因为它是外部的仓库。

    如何解决这个重复订阅的问题呢?我们来优化一下它:

    useEffect(() => {
     if (store.getState().CinemaListReducer.list.length === 0) {
       store.dispatch(getCinemaList())
        } else {
          console.log('走了 store 缓存')
        }
    
     // 订阅
     const unSubscribe = store.subscribe(() => {
       console.log('cinema 中订阅', store.getState().CinemaListReducer.list)
       setlist(store.getState().CinemaListReducer.list)
        })
    
     return () => {
       unSubscribe()
        }
      }, [])
    

    实际上 store.subscribe() 执行完毕之后会返回一个函数,返回的函数就是取消它订阅的唯一执行函数,我们接收一下这个函数,并在 useEffect 的销毁阶段调用它,即可取消订阅。

    7.7、redux-promise

    7.7.1、简介

    第六章我们学习了 redux-thunk 处理异步,它需要返回值是 func,而本章讲述的是另一个处理异步的中间件 redux-promise,顾名思义它需要返回值是 promise。

    我们先来改造一下第六章的代码:

    // @/store/actionCreator/CinemaListActionCreator.js
    import axios from 'axios'
    
    function getCinemaList () {
      // return (dispatch) => {
        return axios.get('https://xxx/xxx/getCinemaList').then(res => {
       console.log(res)
          // dispatch ({
            return {
         type: 'get-cinema-list',
              payload: res.data.data.cinemas
      }
       //})
        }).catch(err => {
       console.log(err)
        })
      // }
    }
    
    export {
      getCinemaList
    }
    

    可以看到我们把 return(dispatch) => {} 这一层给注释掉了,它是 redux-thunk 的语法,我们改写成 return axios({}) 的写法,因为 axios 是一个 promise 对象。我们还注释调了 dispatch({}) 的写法,重新改回 return {} 的写法,因为没有 redux-thunk 给我们传递 dispatch 参数了。

    此时 dispatch(getCinemaList) 控制台是下面的状态:

    store.dispatch(getCinemaList())
    // 控制台报错:
    Error: Action must be plain object.Instead the actual type was 'promise'...
    报错:action 必须是一个普通对象。但是接收到是 promise。
    

    跟第六章一开始的报错一样,因为我们没有安装 redux-promise 中间件的依赖,所以现在的 redux 也是不认可这样的语法。下一小节我们来安装一下中间件的依赖再试试。

    7.7.2、安装中间件

    pnpm install redux-promise
    yarn add redux-promise
    npm install redux-promise
    

    7.7.3、引入中间件

    同样的在 Redux 声明文件中引入 redux-promise,并把它写在 applyMiddleware() 这个 func 中,显而易见 applyMiddleware 可以写入多个中间件。

    这样 action 对象既可以是一个 func 也可以是个 promise。

    // @/store/index.js
    import { applyMiddleware, combinReducers, createStore } from 'redux'
    import CityNameReducer from '@/store/reducers/CityNameReducer.js'
    import TabbarReducer from '@/store/reducers/TabbarReducer.js'
    import CinemaListReducer from '@/store/reducers/CinemaListReducer.js'
    import reduxThunk from 'redux-thunk'
    import reduxPromise from 'redux-promise'
    
    const reducer = combinReducers({
      CityNameReducer,
      TabbarReducer,
      CinemaListReducer
    })
    const store = createStore(reducer, applyMiddleware(reduxThunk, reduxPromise))
    export default store
    

    7.7.4、async / await 语法糖

    你只要保证 return 返回值是一个 promise 就是正确的 redux-promise 语法,所以不过是 ES6 的 promise 也好,还是 ES7 的 async await 也好,都是可以的:

    // @/store/actionCreator/CinemaListActionCreator.js
    import axios from 'axios'
    
    async function getCinemaList () {
      const list = await axios.get('http://xxx/xxx/getCinemaList').then(res => {
     console.log(res)
     return {
       type: 'get-cinema-list',
       payload: res.data.data.cinemas
     }
      }).catch(err => console.log(err))
      return list
    }
    
    export {
      getCinemaList
    }

    八、react-redux

    8.1、简介

    8.1.1、为什么要用 react-redux

    之前学习的 Redux 是基于原生 JS 开发的库,它和 React 没有关系,只是利用了订阅发布模式设计的库,所以我们在使用的时候要频繁使用 subscribe 订阅 dispatch 等等操作。

    但是 react-redux 不同,它会帮我们节省掉 subscribe 这些繁琐的操作。

    需要知道的是 react-redux 是依据 Flux 设计,依赖 Redux 和 React 的库,更加适合在 React 项目中使用。

    8.1.2、react-redux 省略了哪些繁琐的操作

    还记得 Redux 章节隐藏显示 Tabbar 组件的案例吗?我们展示了 Redux 如何去利用订阅发布模式,在进入和离开某组件的适合,控制隐藏和显示另外一个组件。我们来复看一下代码:

    // store/index.jsx
    import { createStore } from 'redux'
    
    const reducer = (prevState = {
      show: true
    }, action) => {
      console.log(action)  {/* {type: 'hide-tabbar'} */}
      let newState = {...prevState}
      switch (action.type) {
     case 'hide-tabbar':
       newState.type = false
       return newState
        case 'show-tabbar':
       newState.type = true
       return newState
     default:
       return prevState
      }
      return prevState
    }
    const store = createStore(reducer)
    export default store
    
    
    // App.jsx
    import React, { useEffect, useState } from 'react'
    import MyRouter from '@/router/index.jsx'
    import Tabbar from '@/components/tabbar/index.jsx'
    import store from '@/store/index.jsx'
    
    function App () {
      const [tabbarStatus, setTabbarStatus] = useState(store.getState().show)
    
      useEffect(() => {
          const unsubscribe = store.subscribe(() => {
      console.log('APP中的订阅', store.getState())
      setTabbarStatus(store.getState().show)
       })
       return () => {
      unsubscribe()
       }
      }, [])
    
      return (
        
       { tabbarStatus &&  }
     
      )
    }
    export default App
    
    
    // Test.jsx
    import React, { useEffect } from 'react'
    import store from '@/store/index.jsx'
    
    function Test () {
      useEffect(() => {
          store.dispatch({ type: 'hide-tabbar' })
          return () => {
      store.dispatch({ type: 'hide-tabbar' })
          }
      }, [])
      
      return (
     
    Test
      ) } export default Test

    我们需要在 Test.jsx 中 dispatch 一个 action,又需要在 App.jsx 中订阅状态和取消订阅,一套流程非常的繁琐。

    react-redux 库的出现就是为了节省这些操作,它会使用 connect 函数在 App.jsx 组件外面生成一个父组件,由父组件来订阅和取消订阅,并把状态父传子给 App.jsx 使用。那么 connect 函数生成的父组件如何知道我们要 dispatch 的是哪一个 action 呢?答案是我们要子传父给它,告诉它要 dispatch 哪一个 action 即可。

    react-redux 其实就是在 Redux 基础上增加了以下两个功能:

    8.2、安装

    pnpm install react-redux
    npm install react-redux
    yarn add react-redux

    8.3、Provider & connect

    8.3.1、简述

    我们在上一章知道了 react-redux 增加了两个能力:

    Provider 供应商传递 state;

    connect 函数包裹组件提供订阅和取消订阅的能力。

    所以我们要针对这两个方面,移用 Redux 章节的代码,来实践改造一下。

    8.3.2、Provider

    我们在上章节知道它是最外层的供应商,利用 React 底层 Context 能力实现跨级传递 state,那么它理应在最外层,包裹在根组件上。

    所以 Provider 实际上是帮我们完成了订阅的操作:

    // index.jsx
    import React from 'react'
    import ReactDOM from 'react-dom'
    import App from './App.jsx'
    import { Provider } from 'react-redux'
    import store from '@/store'
    
    ReactDOM.render(
      
     
      
      , document.getElementById('root')
    )
    

    8.3.3、connect

    connect 实际上是 HOC 高阶组件,它支持 connect(mapStateToProps, mapDispatchToProps)(App) 这样的写法,mapStateToProps 和 mapDispatchToProps 都是函数,且必须要有返回值。

    mapStateToProps = (state) => {} 有一个参数 state 会返回 store 的状态,比如我们这里沿用之前 Redux 章节的 store 声明文件的代码,就会拿到那些合并后的 Reducer 对象集合。是不是有点类似于 store.getState() ?不过这一步骤 react-redux 帮助我们省略掉了。

    mapDispatchToProps = () => {} 则需要返回 dispatch 的动作,我们可以参考之前 Redux 章节,我们拆分了 dispatch 命令并在 store 文件夹下新建了 actionCreator 文件夹,让它们可以被拆分为各个模块统一的进行管理,并在 middleware 中间件章节给大家展示了如何应对 dispatch 只能同步的解决方案。

    下面我会把代码分为几块儿,方便大家回忆:

    8.3.3.1、Reducer 合并章节的代码

    我们将 Tabbar 功能模块、CinemaList 功能模块、CityName 功能模块分为了三个独立的文件夹和 JS 文件,统一放置在 store 仓库文件夹下的 reducers 文件夹,并在 store/index.js 声明文件或者你叫它入口文件也可,在其中统一的引入,再使用 redux 的合并函数 combinReducers 进行合并,最后使用 createStore 创建它们。我们就得到了一个 Redux 仓库。

    // @/store/reducers/TabbarReducer.js
    const TabbarReducer = (prevState = { show: true }, action) => {
      let newState = {...prevState}
      switch (action.type) {
        case 'hide-tabbar':
          newState.show = false
          return newState
        case 'show-tabbar':
          newState.show = true
          return newState
        default:
          return prevState
      }
    }
    export default TabbarReducer
    
    // @/store/reducers/CinemaListReducer.js
    const CinemaListReducer = (prevState = { list: [] }, action) => {
      let newState = {...prevState}
      switch (action.type) {
        case 'get-cinema-list':
          newState.list = action.payload
          return newState
        default:
          return prevState
      }
    }
    export default CinemaListReducer
    
    // @/store/index.js
    import { applyMiddleware, combinReducers, createStore } from 'redux'
    import CityNameReducer from '@/store/reducers/CityNameReducer.js'
    import TabbarReducer from '@/store/reducers/TabbarReducer.js'
    import CinemaListReducer from '@/store/reducers/CinemaListReducer.js'
    import reduxThunk from 'redux-thunk'
    import reduxPromise from 'redux-promise'
    
    const reducer = combinReducers({
      CityNameReducer,
      TabbarReducer,
      CinemaListReducer
    })
    const store = createStore(reducer, applyMiddleware(reduxThunk, reduxPromise))
    export default store
    

    8.3.3.2、redux-thunk \ redux-promise 中间件章节的代码

    还记得为什么要使用中间件么?因为 dispatch action 需要返回值必须是一个普通 JS 对象,而在异步操作中做 return 动作,函数并不会等你的异步完成之后在回调中 return,函数体本身没有 return 任何东西,dispatch 会得到 undefined。没有记住也没关系,我们看一个代码块回忆一下:

    import axios from 'axios'
    
    function getCinemaList () {
      axios.get(...).then(res => {
     console.log(res)
        return {
       type: 'get-cinema-list',
       payload: res.data.data.cinemas
        }
      }).catch(err => console.log(err))
    }
    export default getCinemaList
    
    // 使用
    store.dispatch(getCinemaList())
    {/* 
    这里得到的是 undefined,因为 dispatch 在不利用中间件的时候只能处理同步操作,为什么呢?
    getCinemaList() 函数是普通的 JS 函数,所以它从上到下执行完函数体的内容,就会结束,
    没有 return 那么你拿到的就是 undefined,因为它同步结束的时候,时机是要比体内的 axios 早的,
    axios 是一个异步的动作,函数可不会等待你的结果再往下走。
    */}
    

    这下应该记忆更加深刻了,那么我们接着回忆当时怎么使用 redux-thunk 和 redux-promise 的,以下是两种写法,被注释掉的是 thunk 的写法,选择一种自己写着更顺畅的即可,毕竟它们功能是一样的:

    // @/store/actionCreator/CinemaListActionCreator.js
    import axios from 'axios'
    
    function getCinemaList () {
      // return (dispatch) => {
        return axios.get('https://xxx/xxx/getCinemaList').then(res => {
       console.log(res)
          // dispatch ({
            return {
         type: 'get-cinema-list',
              payload: res.data.data.cinemas
      }
       //})
        }).catch(err => {
       console.log(err)
        })
      // }
    }
    export {
      getCinemaList
    }
    

    8.3.3.3、页面中使用

    以下代码是为大家展示,connect(mapStateToProps, mapDispatchToProps)() 的使用,从中也可以思考 Reducer 合并和 Action 的拆分提取从中提供的便利:

    // @/pages/test/Detail.jsx
    import React, { useEffect } from 'react'
    import { show, hide } from '@/store/actionCreator/TabbarActionCreator.js'
    import { connect } from 'react-redux'
    
    function Detail (props) {
      const { show, hide } = props
    
      useEffect(() => {
     hide()
     return () => {
       show()
        }
      }, [show, hide])
      
      return (
     
    Test
      ) } const mapDispatchToProps = () => {   show,   hide } export default connect(null, mapDispatchToProps)(Detail) // @/pages/test/Cinema.jsx import React, { useEffect } from 'react' import { getCinemaList } from '@/store/actionCreator/CinemaListActionCreator.js' import { connect } from 'react-redux' function Test (props) {   const { cityName, list, getCinemaList } = props   useEffect(() => {  if (list.length === 0) {    getCinemaList()  }   }, [list, getCinemaList])      return (  
       
    {cityName}
             {     list.map(item => {       return {item.title}     })   }    
        ) } const mapStateToProps = (state) => {   return {     list: state.CinemaListReducer.list,  cityName: state.CityNameReducer.cityName   } } const mapDispatchToProps = () => {   getCinemaList: getCinemaList } export default connect(mapStateToProps, mapDispatchToProps)(Test) // App.jsx import React, { useEffect } from 'react' import MyRouter from '@/router/index.jsx' import Tabbar from '@/components/tabbar/index.jsx' import store from '@/store/index.jsx' function App (props) {   useEffect(() => {     console.log(props)  // {name: 'Stefan', age: 24, tabbarStatus: true}   }, [])   return (         { tabbarStatus &&  }     ) } const mapStateToProps = (state) => {   console.log(state)  // {TabbarReducer: {...}, CityNameReduer: {...}, CinemaListReducer: {...}}   return {     name: 'Stefan',     age: 24,  tabbarStatus: state.TabbarReducer.show   } } export default connect(mapStateToProps)(App)

    8.3.4、总结所有配置和使用的链路

    这里专门开一个小结,我担心初学者看到这里会被绕晕,因为本章是紧接着 Redux 的使用去讲解的,用了很多相同的代码,在之上做改动,react-redux 又省略了很多 dispatch 和 subscribe 的操作,所以比较绕。

    所以跟大家复盘一下整个链路,包括各个文件夹负责什么职能:

    8.3.4.1、store

    8.3.4.2、actionCreator

    8.3.4.3、reducers

    8.3.4.4、Provider

    8.3.4.5、connect()

    8.4、原理

    8.4.1、HOC 和 context 通信在 react-redux 底层的运用

    Provider 组件,可以让容器组件拿到 state,原理是使用了 context。

    connect() 是 HOC 高阶组件。

    8.4.2、高阶组件构建和运用

    HOC 不仅是一个函数方法,更是一个组件工厂,获取低阶组件,生成高阶组件。

    它具有以下特点:

    (1) 代码复用、代码模块化

    (2) 增删改 props

    (3) 渲染劫持

    function Test () {
      return (
     
    Test Component.
      ) } function StefanConnect (cb, obj) {   const value = cb   return (MyComponent) => {     return (props) => {       console.log(props)      /*   props 打印出路由信息 {history: {...}, location: {...}},   因为路由的配置里我们把 Test 组件变成了 Route 组件的孩子,   而这里 return 出去的组件其实就是 Test 组件,   只不过被我们的 StefanConnect 高阶组件包裹了一次    */    return             }   } } mapStateToProps = () => {   return {  a: 1,  b: 2   } } mapDispatchToProps = {   aa () {     console.log('func aa has been called')   },   bb () {     console.log('func bb has been called')   } } export default StefanConnect(mapStateToProps, mapDispatchToProps)(Test)

    8.5、redux-persist持久化

    8.5.1、出现的契机

    当我们希望刷新页面不会丢失数据的时候,我们需要数据具有持久化的特点,我们肯定会想起 localstorage,但是这样做就脱离了 react-redux 的范围,这时我们要用到一个新插件:redux-persist,它拥有数据持久化的效能。

    8.5.2、下载安装

    pnpm install redux-persist
    yarn add redux-persist
    npm install redux-persist
    

    8.5.3、配置

    8.5.3.1、基础配置

    我们继续沿用之前的 store 目录下的 index 声明文件的代码,引入 redux-persist 插件的两个 func 和 storage 属性。

    persistReducer 负责包裹我们自定义的 config 对象还有我们之前的 reducer 对象,

    persistStore 负责包裹 createStore 包裹过的 persistReduer 和一些中间件等等插件,

    最终将 store 和 persistStore 包裹后的 store 都给暴露出去。

    我们来看代码:

    // @/store/index.js
    import { applyMiddleware, combinReducers, createStore } from 'redux'
    import CityNameReducer from '@/store/reducers/CityNameReducer.js'
    import TabbarReducer from '@/store/reducers/TabbarReducer.js'
    import CinemaListReducer from '@/store/reducers/CinemaListReducer.js'
    import reduxThunk from 'redux-thunk'
    import reduxPromise from 'redux-promise'
    import storage from 'redux-persist/lib/storage'
    import { persistStore, persistReduer } from 'redux-persist'
    
    const reducer = combinReducers({
      CityNameReducer,
      TabbarReducer,
      CinemaListReducer
    })
    
    const persistConfig = {
      key: 'Stefan',
      storage
    }
    const persistedReducer = persistReducer(persistConfig, reducer)
    
    const store = createStore(persistedReducer, applyMiddleware(reduxThunk, reduxPromise))
    const persistor = persistStore(store)
    
    export {
      store,
      persistor
    }
    

    接下来要改造项目的入口文件 index.jsx,之前我们记得要在根组件外面包裹 Provider 组件,给项目提供 react-redux 的能力,现在我们需要把 redux-persist 的能力也赋予给项目:

    // index.jsx
    import React from 'react'
    import ReactDOM from 'react-dom'
    import App from './App.jsx'
    import { Provider } from 'react-redux'
    
    /* store 的引入方式也要发生变化了,因为我们改用了 export {} 的写法 */
    import { store, persistor } from '@/store'
    import { PersistGate } from 'redux-persist/integration/react'
    
    ReactDOM.render(
      
     
       
     
      
      , document.getElementById('root')
    )
    

    这时我们再次查看项目,会发现在 F12 开发者工具中的应用选项中 localstorage 属性里会多出一个叫:persist:Stefan 的本地缓存,它里面包含了各个 reducer 对象:

    persist:Stefan
    
    {
      CityNameReducer: "{...}",
      TabbarReducer: "{...}",
      CinemaListReducer: "{...}"
      _persist: "{\"version\": -1, \"rehydrated\": true}"
    }
    

    8.5.3.2、blacklist 和 whitelist

    结果上来看,不管我们需不需要,我们都缓存了所有模块儿的 reducer,这样去处理使不合适的,那么我们需要选择性的去缓存某个或某些模块儿的 reducer,这里就要引入 redux-persist 的黑白名单概念了,我们来改造下 store 的声明文件:

    const persistConfig = {
      key: 'Stefan',
      storage,
      whitelist: ['CityNameReduer']
    }
    

    现在再到 localstorage 中去看,发现 persist:Stefan 的本地缓存发生了变化,只缓存了白名单里的 reducer:

    persist:Stefan
    
    {
      CityNameReducer: "{...}",
      _persist: "{\"version\": -1, \"rehydrated\": true}"
    }

    你可能感兴趣的:(react.js,javascript,前端)