0%

中间件的实现

React-Router路由

URL的hash:

  • URL的hash也就是锚点(#), 本质上是改变window.location的href属性
  • 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新

React Router的安装

1
npm install react-router-dom

基本使用

BrowserRouter或HashRouter:

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

路由的配置与使用

要使用的组件前面添加路由设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { HashRouter } from 'react-router-dom';


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
// 可以选择HashRouter/BrowserRouter
<HashRouter>
<App />
</HashRouter>
</React.StrictMode>
);

路由的映射

映射设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { PureComponent } from 'react'
import { Routes, Route } from 'react-router-dom'
import Home from './components/Home'
import MyMusic from './components/MyMusic'
export default class App extends PureComponent {
render() {
return (
<div>
<h2>Header</h2>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/my" element={<MyMusic/>}/>
</Routes>
</div>
</div>
)
}
}

注:

  • 如果使用HashRouter,浏览器的路径设置一定要加/#/,否则是失效的

image-20230402152504555

image-20230402152522490

路由的配置跳转

Link元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { PureComponent } from 'react'
import { Routes, Route, Link } from 'react-router-dom'
import Home from './components/Home'
import MyMusic from './components/MyMusic'
export default class App extends PureComponent {
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<Link to="/">发现音乐</Link>
<br />
<Link to="/my">我的音乐</Link>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/my" element={<MyMusic/>}/>
</Routes>
</div>
</div>
)
}
}

image-20230402153039156

注:

  • Link元素会自动渲染为a元素
  • 如果要更改这个a元素的样式,需要使用NavLink组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink } from 'react-router-dom'
import Home from './components/Home'
import MyMusic from './components/MyMusic'
import "./index.css"
export default class App extends PureComponent {
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<NavLink to="/">发现音乐</NavLink>
<br />
<NavLink to="/my">我的音乐</NavLink>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/my" element={<MyMusic/>}/>
</Routes>
</div>
</div>
)
}
}

index.css代码:

1
2
3
.active{
color: red;
}

image-20230402153943491

注:

  • NavLink元素会自动给当前渲染的a元素添加active类,所以可以通过active去设置当前渲染的样式
  • 而如果active类在其他地方已经使用过了呢,使用active就容易引起冲突,react-router提供了className属性去自定义我们的类名称,属性应该传入函数
  • 同样react-router为我们提供了style属性去定义样式,属性应该传入函数

使用style属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink } from 'react-router-dom'
import Home from './components/Home'
import MyMusic from './components/MyMusic'
export default class App extends PureComponent {
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
{/* 这里给style传入的函数,返回的一个对象,而且会自动传入一个包含isActive的参数,可以通过解构出isActive用于逻辑判断 */}
<NavLink to="/" style={({isActive})=>{ return isActive?{color:"red",fontSize:24}:{}}}>发现音乐</NavLink>
<br />
<NavLink to="/my" style={({isActive})=>{ return isActive?{color:"red",fontSize:24}:{}}}>我的音乐</NavLink>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/my" element={<MyMusic/>}/>
</Routes>
</div>
</div>
)
}
}

image-20230402154824768

使用className属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink } from 'react-router-dom'
import Home from './components/Home'
import MyMusic from './components/MyMusic'
import "./index.css"
export default class App extends PureComponent {
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<NavLink to="/" className={({isActive})=>{ return isActive?"nav-active one":"one"}}>发现音乐</NavLink>
<br />
<NavLink to="/my" className={({isActive})=>{ return isActive?"nav-active two":"two"}}>我的音乐</NavLink>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/my" element={<MyMusic/>}/>
</Routes>
</div>
</div>
)
}
}

image-20230402155112040

  • 如当页面location为/时,自动跳转到/home/页面
  • 这种情况与上面直接element的直接区别为:这种情况下不会出现”/“的情况,因为会直接跳转,而上面是替换匹配到的组件
  • 导航的所有东西都是放在#后面的,#前面在解析时会忽略

image-20230402160154144

image-20230402160916685

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink, Navigate } from 'react-router-dom'
import Home from './components/Home'
import MyMusic from './components/MyMusic'
import "./index.css"
export default class App extends PureComponent {
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<NavLink to="/" className={({isActive})=>{ return isActive?"nav-active one":"one"}}>发现音乐</NavLink>
<br />
<NavLink to="/my" className={({isActive})=>{ return isActive?"nav-active two":"two"}}>我的音乐</NavLink>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path='/' element={<Navigate to="/home"/>}/>
<Route path="/home" element={<Home/>}/>
<Route path="/my" element={<MyMusic/>}/>
</Routes>
</div>
</div>
)
}
}
Not Found页面配置
  • 当所有的Route都未匹配成功时,当前组件内容为空,而有时想给用户提醒输入了错误的url,则需要一个可以匹配所有字符的默认Route

image-20230402160512717

  • react-route中的通配符为’*‘,因此将一个path=’*‘放在最后,那么如果前面的path都没匹配上,最后一个肯定可以匹配,并找到默认页面

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      import React, { PureComponent } from 'react'
      import { Routes, Route, NavLink, Navigate } from 'react-router-dom'
      import Home from './components/Home'
      import MyMusic from './components/MyMusic'
      import NotFound from './components/NotFound'
      import "./index.css"
      export default class App extends PureComponent {
      render() {
      return (
      <div>
      {/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
      <div className='header'>
      <NavLink to="/" className={({isActive})=>{ return isActive?"nav-active one":"one"}}>发现音乐</NavLink>
      <br />
      <NavLink to="/my" className={({isActive})=>{ return isActive?"nav-active two":"two"}}>我的音乐</NavLink>
      </div>
      <div className="content">
      {/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
      <Routes>
      <Route path='/' element={<Navigate to="/home"/>}/>
      <Route path="/home" element={<Home/>}/>
      <Route path="/my" element={<MyMusic/>}/>
      <Route path='*' element={<NotFound/>}/>
      </Routes>
      </div>
      </div>
      )
      }
      }

路由的嵌套

在实际开发中,经常出现路由之间存在嵌套关系——如网易云中的路由

image-20230402161440408

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink, Navigate } from 'react-router-dom'
import Home from './components/Home'
import MyMusic from './components/MyMusic'
import NotFound from './components/NotFound'
import Recommend from './components/Recommend'
import TopList from './components/TopList'
import "./index.css"
export default class App extends PureComponent {
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<NavLink to="/" className={({isActive})=>{ return isActive?"nav-active one":"one"}}>发现音乐</NavLink>
<NavLink to="/my" className={({isActive})=>{ return isActive?"nav-active two":"two"}}>我的音乐</NavLink>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path='/' element={<Navigate to="/home"/>}/>
<Route path="/home" element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/discover"/>}/>
<Route path='/home/discover' element={<Recommend/>}/>
<Route path='/home/toplist' element={<TopList/>}/>
</Route>
<Route path="/my" element={<MyMusic/>}/>
<Route path='*' element={<NotFound/>}/>
</Routes>
</div>
</div>
)
}
}

注:

  • 通过上述写法可以实现嵌套,在Home组件中编写下一层的路由跳转逻辑

    • Home.jsx的代码——Outlet是NavLink元素得到组件的位置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import React, { PureComponent } from 'react'
    import { NavLink, Outlet } from 'react-router-dom'
    import "../index.css"
    export default class Home extends PureComponent {
    render() {
    return (
    <div className='home-container'>
    <div>home</div>
    <div className='home-content'>
    <NavLink to="/home/discover">推荐</NavLink>
    <NavLink to="/home/toplist">排行榜</NavLink>
    </div>
    {/* <Outlet/>是NavLink元素得到组件的位置 */}
    <Outlet/>
    </div>
    )
    }
    }

    image-20230402164901837

  • 第二层路由最终会变成第一层路由的子组件,而如果我们在两个’/‘路由的时候,想让他在第一层,而不出现在第二层呢——可以在第一层时,不写element

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <Routes>
    <Route path='/' element={<Navigate to="/home"/>}/>
    <Route path="/home">
    <Route path='/home' element={<Navigate to="/home/discover"/>}/>
    <Route path='/home/discover' element={<Recommend/>}/>
    <Route path='/home/toplist' element={<TopList/>}/>
    </Route>
    <Route path="/my" element={<MyMusic/>}/>
    <Route path='*' element={<NotFound/>}/>
    </Routes>

    image-20230402165329191

手动实现路由的跳转

  • 上面的跳转实现主要靠NavLink自动生成的a元素,但有时候我们想要使用其他组件跳转,或者在逻辑过程中跳转,这种方法就不太好实现
  • hooks提供了useNavigate函数,返回一个navigate对象,可以通过这个对象在代码中进行跳转
  • 但是hooks只能在函数组件中使用,类组件不能直接使用useNavigate
    • 对于对状态要求不高的类组件,可以更改为函数组件使用useNavigate
    • 对于有很多状态的类组件,可以使用高阶组件,在其外包裹一个函数组件,通过参数的形式,将navigate对象传递给类组件使用

高阶函数代码:

1
2
3
4
5
6
7
8
9
10
11
12
import { useNavigate } from "react-router-dom"

function hooksFunc(Wrapper){
return (props)=>{
const navigate = useNavigate()
const router = {
navigate
}
return <Wrapper {...props} router = {router}/>
}
}
export default hooksFunc

要被包裹的组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink, Navigate } from 'react-router-dom'
import Home from './components/Home'
import hooksFunc from './components/HooksFunction'
import MyMusic from './components/MyMusic'
import NotFound from './components/NotFound'
import Recommend from './components/Recommend'
import TopList from './components/TopList'
import Friend from './components/Frined'
import "./index.css"
class App extends PureComponent {
navigateFriend(){
const {navigate} = this.props.router
navigate("/friend")
}
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<NavLink to="/" className={({isActive})=>{ return isActive?"nav-active one":"one"}}>发现音乐</NavLink>
<NavLink to="/my" className={({isActive})=>{ return isActive?"nav-active two":"two"}}>我的音乐</NavLink>
<button onClick={()=>{this.navigateFriend()}}>关注</button>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path='/' element={<Navigate to="/home"/>}/>
<Route path="/home" element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/discover"/>}/>
<Route path='/home/discover' element={<Recommend/>}/>
<Route path='/home/toplist' element={<TopList/>}/>
</Route>
<Route path="/my" element={<MyMusic/>}/>
<Route path='/friend' element={<Friend/>}/>
<Route path='*' element={<NotFound/>}/>
</Routes>
</div>
</div>
)
}
}
// 这里可以选择导出时用高阶函数处理,也可以选择先导出,在使用前处理
export default hooksFunc(App)

路由参数的传递

  • 动态路由的方式
  • search传递参数

动态路由

  • 比如/detail的path对应一个组件Detail
  • 如果我们将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示
  • 这个匹配规则,我们就称之为动态路由
  • 通常情况下,使用动态路由可以为路由传递参数

App.jsx代码——配置Route:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink, Navigate } from 'react-router-dom'
import Home from './components/Home'
import hooksFunc from './components/HooksFunction'
import MyMusic from './components/MyMusic'
import NotFound from './components/NotFound'
import Recommend from './components/Recommend'
import TopList from './components/TopList'
import Friend from './components/Frined'
import "./index.css"
class App extends PureComponent {
navigateFriend(){
const {navigate} = this.props.router
navigate("/friend")
}
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<NavLink to="/" className={({isActive})=>{ return isActive?"nav-active one":"one"}}>发现音乐</NavLink>
<NavLink to="/my/123/oww" className={({isActive})=>{ return isActive?"nav-active two":"two"}}>我的音乐</NavLink>
<button onClick={()=>{this.navigateFriend()}}>关注</button>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path='/' element={<Navigate to="/home"/>}/>
<Route path="/home" element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/discover"/>}/>
<Route path='/home/discover' element={<Recommend/>}/>
<Route path='/home/toplist' element={<TopList/>}/>
</Route>
{/* Route配置/:id/:name标志这里的id为参数 */}
<Route path="/my/:id/:name" element={<MyMusic/>}/>
<Route path='/friend' element={<Friend/>}/>
<Route path='*' element={<NotFound/>}/>
</Routes>
</div>
</div>
)
}
}
//
export default hooksFunc(App)

hooksFunction代码:——获取参数使用useParams的hook,依然不能直接在类组件中使用,因此与navigate一样,使用高阶组件包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useNavigate, useParams } from "react-router-dom"

function hooksFunc(Wrapper){
return (props)=>{
const navigate = useNavigate()
const params = useParams()
const router = {
navigate,
params
}
return <Wrapper {...props} router = {router}/>
}
}
export default hooksFunc

MyMusic.jsx代码:——获取参数的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { PureComponent } from 'react'
import hooksFunc from './HooksFunction'

class MyMusic extends PureComponent {
render() {
const {params} = this.props.router
console.log(params)
return (
<div>MyMusic-{params.id}-{params.name}</div>
)
}
}
export default hooksFunc(MyMusic)

image-20230403175400198

search传递参数

search传递参数直接在跳转路由时设置(Link/navigate)——如navigate(“/friend?name=oww&age=22”)

App.jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { PureComponent } from 'react'
import { Routes, Route, NavLink, Navigate } from 'react-router-dom'
import Home from './components/Home'
import hooksFunc from './components/HooksFunction'
import MyMusic from './components/MyMusic'
import NotFound from './components/NotFound'
import Recommend from './components/Recommend'
import TopList from './components/TopList'
import Friend from './components/Frined'
import "./index.css"
class App extends PureComponent {
navigateFriend(){
const {navigate} = this.props.router
// search参数传递不需要在Route中配置,而直接在Link或navigate中配置
navigate("/friend?name=oww&age=22")
}
render() {
return (
<div>
{/* 主要是通过路由的配置来更改当前页面的location,从而使下面的Routes匹配结果改变 */}
<div className='header'>
<NavLink to="/" className={({isActive})=>{ return isActive?"nav-active one":"one"}}>发现音乐</NavLink>
<NavLink to="/my/123/oww" className={({isActive})=>{ return isActive?"nav-active two":"two"}}>我的音乐</NavLink>
<button onClick={()=>{this.navigateFriend()}}>关注</button>
</div>
<div className="content">
{/* 这里会con上往下匹配,直至匹配到一个结果,后续就不匹配了*/}
<Routes>
<Route path='/' element={<Navigate to="/home"/>}/>
<Route path="/home" element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/discover"/>}/>
<Route path='/home/discover' element={<Recommend/>}/>
<Route path='/home/toplist' element={<TopList/>}/>
</Route>
{/* Route配置/:id/:name标志这里的id为参数 */}
<Route path="/my/:id/:name" element={<MyMusic/>}/>
<Route path='/friend' element={<Friend/>}/>
<Route path='*' element={<NotFound/>}/>
</Routes>
</div>
</div>
)
}
}
//
export default hooksFunc(App)

hooksFunction代码:要获取传递的参数同样需要hooks,则同样需要用高阶函数包裹

  • useLocation:会获取到当前页面url的一些信息

  • useSearchParams:获取到当前页面的参数相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"

function hooksFunc(Wrapper){
return (props)=>{
const navigate = useNavigate()
const params = useParams()
const location = useLocation()
const [searchParams] = useSearchParams()
const query = Object.fromEntries(searchParams)
console.log("location", location)
console.log("searchParams", query)
const router = {
navigate,
params,
query
}
return <Wrapper {...props} router = {router}/>
}
}
export default hooksFunc

image-20230403181604295

路由的配置文件

  • 之前直接用Route组件定义路由,如果项目中路由很多的情况下,页面会相当混乱
  • 可以通过useRoutes的hook配置到一个地方集中管理
    • 早期,Route没有相关的api,需要借助于react-router-config
    • Router6.x中,为我们提供了useRoutes API完成相关的配置

image-20230403184434151

Redux

为什么要使用Redux

  • JavaScript开发的应用程序,已经变得越来越复杂了
    • JavaScript需要管理的状态越来越多,越来越复杂
    • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示 加载动效,当前分页
  • 管理不断变化的state是非常困难的
    • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化
    • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪
  • React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理
    • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享
    • React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定
  • React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定
  • Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)

Redux三大原则

  • 单一数据源
    • 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中
    • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
    • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改
  • State是只读的
    • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State
    • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state
    • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题
  • 使用纯函数来执行修改、
    • 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State
    • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
    • 但是所有的reducer都应该是纯函数,不能产生任何的副作用

Redux项目搭建与使用

  • 安装redux

    1
    npm install redux --save
  • 创建一个对象,作为我们要保存的状态

  • 创建Store来存储这个state

    • 创建store时必须创建reducer

    • 我们可以通过 store.getState 来获取当前的state

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    const { createStore } = require("redux")
    // 初始化数据——要保存的状态
    const initialState = {
    name:"ouwenwu",
    age:22
    }
    // 定义reducer函数:纯函数
    /**
    * 参数1:store中目前报错的state——在createStore时会调用reducer一次,此时参数state为undefined,因此需要为其定义一个初始值,即我们的初始化数据
    * 参数2:传入的action
    * 返回值:返回值会作为store之后存储的state
    */
    function reducer(state=initialState,action){
    console.log("reducer",state,action)
    // 这里使用switch更好
    switch(action.type){
    case "change_name":
    return {...state,name:action.name}
    case "add_age":
    return {...state,age:state.age+action.age}
    default:
    return state
    }
    }

    //创建store
    const store = createStore(reducer)
    module.exports = store
  • 通过action来修改state

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

    • 这里一定要记住,reducer是一个纯函数,不需要直接修改state
    • 后面我会讲到直接修改state带来的问题
  • 可以在派发action之前,监听store的变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    const store=require("./store");

    // 开启store订阅——这个订阅是在派发action之前订阅的,会在store所监听的state修改时调用
    const unsubscribe = store.subscribe(()=>{
    console.log("订阅数据发生变化",store.getState())
    })

    // actionFunction——将action抽取为函数,那么后面每次需要创建action不用手动编写,调用函数生成
    const changeNameAction = (name)=>({
    type:"change_name",
    name
    })
    const addAgeAction = (age)=>({
    type:"add_age",
    age
    })
    // 修改store中的数据:必须action——手动写action
    const nameAction ={type:"change_name",name:"kobe"}
    store.dispatch(nameAction)
    store.dispatch(changeNameAction("kebi1"))
    // 取消订阅
    unsubscribe()
    store.dispatch({type:"add_age",age:1})
    store.dispatch(addAgeAction(1))

    输出image-20230330092932419

Redux项目抽取

如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护

  • store/index.js文件——利用reducer创建store

    1
    2
    3
    4
    import {createStore} from "redux"
    import reducer from "./reducer"
    //创建store
    export const store = createStore(reducer)
  • store/reducer.js文件——reducer函数和state默认值\

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import * as actionType from "./constants.js"

    const initialState = {
    name:"ouwenwu",
    age:22
    }
    // 定义reducer函数:纯函数
    /**
    * 参数1:store中目前报错的state
    * 参数2:传入的action
    * 返回值:返回值会作为store之后存储的state
    */
    function reducer(state=initialState,action){
    console.log("reducer",state,action)
    // 这里使用switch更好
    switch(action.type){
    case actionType.CHANGE_NAME:
    return {...state,name:action.name}
    case actionType.ADD_AGE:
    return {...state,age:state.age+action.age}
    default:
    return state
    }
    }
    export default reducer
  • store/actionCreator.js文件——将store要派发的action,抽取成函数放在此处

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import * as actionType from "./constants.js"

    export const changeNameAction = (name)=>({
    type:actionType.CHANGE_NAME,
    name
    })
    export const addNameAge = (age)=>({
    type:actionType.ADD_AGE,
    age
    })
  • store/constants.js文件——常量文件,将action中的type抽取出来

    1
    2
    export const CHANGE_NAME = "change_name"
    export const ADD_AGE = "add_age"

Redux与React融合

image-20230330102727040

redux配置代码如上所述

jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React from "react";
import store from "./store/index.js"
import { changeNameAction, addAgeAction } from "./store/actionCreator";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
// 初始化值使用store中的初始化变量
this.state = {
name:store.getState().name,
age:store.getState().age
}
}
// 加载页面前订阅store,使得其更改时调用对应的函数改变state引起页面的刷新
componentDidMount(){
this.unsubScribe = store.subscribe(()=>{
const state = store.getState()
this.setState({
name:state.name,
age:state.age
})
})
}
// 页面卸载前取消订阅
componentWillUnmount(){
this.unsubScribe()
}
changeName(){
store.dispatch(changeNameAction("www"))
}
addAge(){
store.dispatch(addAgeAction(1))
}
render(){
const {name,age} = this.state
return (
<div>
<button onClick={()=>{this.changeName()}}>改变名字</button>
<button onClick={()=>{this.addAge()}}>age+1</button>
<div>
<h2>{name}</h2>
<h3>{age}</h3>
</div>
</div>
)
}
}
export default App

react-redux的使用

  • index.js(整个react的入口):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import React from "react";
    import ReactDOM from "react-dom/client";
    import App from "./App"
    import { Provider } from "react-redux";
    import store from "./store";



    // 编写REACT代码,通过REACT渲染内容
    /**
    * 这里的Provider实际上是基于Context实现的,用它包裹App及其子组件可以使用传入的stero
    * stero在底层是通过value实现的
    */
    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(
    <Provider store={store}>
    <App/>
    </Provider>
    )
  • App.js代码(获取stero并使用代码):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    import React from "react";
    import { connect } from "react-redux";
    import { addAgeAction } from "./store/actionCreator";
    //编写组件
    class App extends React.PureComponent{
    constructor(){
    super()
    }
    addAgeByButton(){
    // store.dispatch(addAgeAction(1))
    this.props.addAge(1)
    }
    render(){
    return (
    <div>
    <button onClick={()=>{this.addAgeByButton()}}>age+1</button>
    <div>
    <h3>{this.props.age}</h3>
    </div>
    </div>
    )
    }
    }
    // 相当于添加监听,会把stero中的state中的对应属性添加到props中
    function mapStateToProps(state){
    // 需要用哪些就使用哪些,会将这个返回对象和本来的props合并
    return{
    age:state.age
    }
    }
    // 设置dispatch,会把return中的函数放到this.pros中去,通过调用这个函数可以发送action给stero
    const mapDispatchToProps = (dispatch)=>{
    return{
    addAge(age){
    dispatch(addAgeAction(age))
    }
    }
    }
    // connect()返回值是一个高阶函数
    /**
    * connect()参数1:store中的哪些数据需要映射到这个组件的props——函数
    * connect()参数2:
    */
    export default connect(mapStateToProps,mapDispatchToProps)(App)
  • stero的代码,index.js/constants.js/actionCreate.js/reducer.js依然像上方写的一样

redux异步处理

react获取网络请求:

  • componentDidMount中发起网络请求,并在结束后赋值给state

    • 这种方式数据管理和react耦合性高,和redux的要求不符
  • 通过redux获取网络请求,由jsx组件中发起action,在action中获取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * 由于网络请求通常是异步的,函数的return中不能直接获得网络请求的值,因此普通的action是不能实现异步的
    */
    export const netWorkAction = ()=>{
    new Promise((resolve, reject) => {
    setTimeout(()=>{
    resolve("oww")
    },5000)
    }).then(res=>{
    return{
    type:actionType.CHANGE_NAME,
    name:res
    }
    })
    return {}
    }

    redux-thunk中间件技术

    • 安装redux-thunk

      1
      npm install react-thunk --save
    • 设置react可以使用thunk,thunk允许dispatch一个函数,之后会自动执行这个函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import { applyMiddleware, combineReducers, createStore } from "redux";
      import countReducer from "./countStore/reducer";
      import homeReducer from "./homeStore/reducer";
      import thunk from "redux-thunk";
      const reducer = combineReducers({
      age:countReducer,
      name:homeReducer
      })

      // combineReducers的底层实现
      function reducerByUs(state = {}, action){
      return {
      /**
      * 第一次执行时(createStore),传入undefined,得到的是默认值对象
      * 后续执行时,每次传入上次的state,得到正确值
      */
      age:countReducer(state.age, action),
      name:homeReducer(state.name,action)
      }
      }
      // 包裹中间件
      export const store = createStore(reducer,applyMiddleware(thunk))
      export default store

      action代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      export const netWorkAction = ()=>{
      console.log("dispatch foo")
      const foo = (dispatch, getState)=>{
      new Promise((resolve) => {
      setTimeout(()=>{
      resolve("oww")
      },5000)
      }).then(res=>{
      dispatch(changeNameAction(res))
      })
      }
      return foo
      }

      jsx中发起dispatch代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      import React from "react";
      import { connect } from "react-redux";
      import { addAgeAction } from "./store/countStore";
      import { netWorkAction } from "./store/homeStore";
      //编写组件
      class App extends React.PureComponent{
      constructor(){
      super()
      }
      addAgeByButton(){
      // store.dispatch(addAgeAction(1))
      this.props.addAge(1)
      this.props.changeName()
      }

      render(){
      return (
      <div>
      <button onClick={()=>{this.addAgeByButton()}}>changeinfo</button>
      <div>
      <h2>{this.props.name}</h2>
      <h3>{this.props.age}</h3>
      </div>
      </div>
      )
      }
      }

      // 相当于添加监听,会把stero中的state中的对应属性添加到props中
      function mapStateToProps(state){
      // 需要用哪些就使用哪些,会将这个返回对象和本来的props合并
      return{
      age:state.age.age,
      name:state.name.name
      }
      }
      // 设置dispatch,会把return中的函数放到this.pros中去,通过调用这个函数可以发送action给stero
      const mapDispatchToProps = (dispatch)=>{
      return{
      addAge(age){
      dispatch(addAgeAction(age))
      },
      changeName(){
      dispatch(netWorkAction())
      }
      }
      }
      // connect()返回值是一个高阶函数
      /**
      * connect()参数1:store中的哪些数据需要映射到这个组件的props——函数
      * connect()参数2:
      */
      export default connect(mapStateToProps,mapDispatchToProps)(App)

redux代码拆分

不拆分的reducer:

  • 一个reducer处理多个页面的数据
  • 将所有状态都放到reducer中进行管理,随着项目的日趋庞大,会造成代码臃肿

对reducer进行拆分:

  • 将对不同页面的数据或造成抽取为不同的reducer
  • 将多个reducer合并为一个

combineReducers函数

事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并

页面一store代码:

  • actionCreator.js

    1
    2
    3
    4
    5
    6
    import * as actionType from "./constants.js"

    export const changeNameAction = (name)=>({
    type:actionType.CHANGE_NAME,
    name
    })
  • constants.js

    1
    export const CHANGE_NAME = "change_name"
  • index.js

    1
    2
    3
    4
    5
    6
    import homeReducer from "./reducer"

    // 统一当初当前模块的数据
    export default homeReducer
    export * from "./actionCreator"
    export * from "./constants"
  • reducer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import * as actionType from "./constants.js"

    const initialState = {
    name:"ouwenwu",
    age:22
    }
    // 定义reducer函数:纯函数
    /**
    * 参数1:store中目前报错的state
    * 参数2:传入的action
    * 返回值:返回值会作为store之后存储的state
    */
    function homeReducer(state=initialState,action){
    console.log("reducer",state,action)
    // 这里使用switch更好
    switch(action.type){
    case actionType.CHANGE_NAME:
    return {...state,name:action.name}
    default:
    return state
    }
    }
    export default homeReducer

页面一store代码:

  • actionCreator.js

    1
    2
    3
    4
    5
    6
    import * as actionType from "./constants.js"

    export const changeNameAction = (name)=>({
    type:actionType.CHANGE_NAME,
    name
    })
  • constants.js

    1
    export const CHANGE_NAME = "change_name"
  • index.js

    1
    2
    3
    4
    5
    6
    import homeReducer from "./reducer"

    // 统一当初当前模块的数据
    export default homeReducer
    export * from "./actionCreator"
    export * from "./constants"
  • reducer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import * as actionType from "./constants.js"

    const initialState = {
    name:"ouwenwu",
    age:22
    }
    // 定义reducer函数:纯函数
    /**
    * 参数1:store中目前报错的state
    * 参数2:传入的action
    * 返回值:返回值会作为store之后存储的state
    */
    function homeReducer(state=initialState,action){
    console.log("reducer",state,action)
    // 这里使用switch更好
    switch(action.type){
    case actionType.CHANGE_NAME:
    return {...state,name:action.name}
    default:
    return state
    }
    }
    export default homeReducer

页面二store代码:

  • actionCreator.js

    1
    2
    3
    4
    5
    6
    import * as actionType from "./constants.js"

    export const addAgeAction = (age)=>({
    type:actionType.ADD_AGE,
    age
    })
  • constants.js

    1
    export const ADD_AGE = "add_age"
  • index.js

    1
    2
    3
    4
    5
    import countReducer from "./reducer"

    export default countReducer
    export * from "./actionCreator"
    export * from "./constants"
  • reducer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import * as actionType from "./constants.js"

    const initialState = {
    age:22
    }
    // 定义reducer函数:纯函数
    /**
    * 参数1:store中目前报错的state
    * 参数2:传入的action
    * 返回值:返回值会作为store之后存储的state
    */
    function countReducer(state=initialState,action){
    console.log("reducer",state,action)
    // 这里使用switch更好
    switch(action.type){
    case actionType.ADD_AGE:
    console.log(state.age,action.age)
    return {...state,age:state.age+action.age}
    default:
    return state
    }
    }
    export default countReducer

合并两个store的代码:

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { combineReducers, createStore } from "redux";
import countReducer from "./countStore/reducer";
import homeReducer from "./homeStore/reducer";

const reducer = combineReducers({
age:countReducer,
name:homeReducer
})

// combineReducers的底层实现
function reducerByUs(state = {}, action){
return {
/**
* 第一次执行时(createStore),传入undefined,得到的是默认值对象
* 后续执行时,每次传入上次的state,得到正确值
*/
age:countReducer(state.age, action),
name:homeReducer(state.name,action)
}
}
export const store = createStore(reducer)
export default store

合并数据的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React from "react";
import { connect } from "react-redux";
import { addAgeAction } from "./store/countStore";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
addAgeByButton(){
// store.dispatch(addAgeAction(1))
this.props.addAge(1)
}
render(){
return (
<div>
<button onClick={()=>{this.addAgeByButton()}}>age+1</button>
<div>
<h3>{this.props.age}</h3>
</div>
</div>
)
}
}
// 相当于添加监听,会把stero中的state中的对应属性添加到props中
function mapStateToProps(state){
// 需要用哪些就使用哪些,会将这个返回对象和本来的props合并
return{
// 这里获取到的stero为合并后的state,而合并后的state.age是一个reducer对象,因此要再加一个.age才能获取到真实值
age:state.age.age
}
}
// 设置dispatch,会把return中的函数放到this.pros中去,通过调用这个函数可以发送action给stero
const mapDispatchToProps = (dispatch)=>{
return{
addAge(age){
dispatch(addAgeAction(age))
}
}
}
// connect()返回值是一个高阶函数
/**
* connect()参数1:store中的哪些数据需要映射到这个组件的props——函数
* connect()参数2:
*/
export default connect(mapStateToProps,mapDispatchToProps)(App)

ReduxToolkit的使用(RTK)

  • 安装

    1
    npm install @reduxjs/toolkit react-redux
  • Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题

Redux Toolkit的核心API主要是如下几个:

  • configureStore:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension

  • createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions

  • createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分 派动作类型的 thunk

createSlice代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { createSlice } from "@reduxjs/toolkit";
import { createAsyncThunk } from "@reduxjs/toolkit";
// @reduxjs/toolkit的异步操作1
/**
* payload是在jsx中dispatch时传入的额外参数
*/
export const getNameAction1 = createAsyncThunk("age/changeName",async(payload, stero)=>{
const {dispatch, getState} = stero
new Promise((resolve)=>{
setTimeout(()=>{
resolve("ouwenwu")
},5000)
}).then(res=>[
dispatch(changeName(res))
])
})
// @reduxjs/toolkit的异步操作2
export const getNameAction = createAsyncThunk("age/changeName",async(payload, stero)=>{
const {dispatch, getState} = stero
const res = await new Promise((resolve)=>{
setTimeout(()=>{
resolve("ouwenwu")
},2000)
})
console.log(res)
return res
})
const ageSlice = createSlice({
name:"ageSlice", // 用户后面标记当前操作类型,与react中action的type类似——但是不再需要写出来各种type,而自动实现
// 初始值
initialState:{
name:"oww",
age:22
},
// 相当于之前的reducer函数,可以添加很多的函数,每一个函数都相当于一个原来的case
reducers:{
addAge(state, action){
state.age += action.payload
console.log(action)
},
subAge(state, action){
state.age -= action.payload
console.log(action)
},
changeName(state,action){
state.name = action.payload
console.log(action)
}
},
// 监听异步dispatch
extraReducers:{
/**
* getNameAction一个有三个状态
* pending:action被发出,但是还没有最终的结果
* fulfilled:获取到最终的结果(有返回值的结果)
* rejected:执行过程中有错误或者抛出了异常
*/
[getNameAction.fulfilled](state, action){
state.name = action.payload
console.log(action)
}
}
})
// createSlice是一个把参数穿进去,返回一个对象,包含之前的reducer、action等,使用方法和之前使用相同
export const {addAge, subAge, changeName} = ageSlice.actions
const ageReucer = ageSlice.reducer
export default ageReucer

configureStore代码

1
2
3
4
5
6
7
8
9
10
import { configureStore } from "@reduxjs/toolkit"
import ageReucer from "./countStore/ageSlice"


const store = configureStore({
reducer:{
age: ageReucer
}
})
export default store

jsx代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React from "react";
import { connect } from "react-redux";
import { addAge, subAge, getNameAction } from "./store/countStore/ageSlice";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
addAgeByButton(){
this.props.addAge(1)
}
subAgeByButton(){
this.props.subAge(1)
}
changeNameByButton(){
this.props.changeName()
}
render(){
return (
<div>
<button onClick={()=>{this.addAgeByButton()}}>addAge</button>
<button onClick={()=>{this.subAgeByButton()}}>subAge</button>
<button onClick={()=>{this.changeNameByButton()}}>changeName</button>
<div>
<h2>{this.props.name}</h2>
<h3>{this.props.age}</h3>
</div>
</div>
)
}
}

// 相当于添加监听,会把stero中的state中的对应属性添加到props中
function mapStateToProps(state){
// 需要用哪些就使用哪些,会将这个返回对象和本来的props合并
return{
name:state.age.name,
age:state.age.age
}
}
// 设置dispatch,会把return中的函数放到this.pros中去,通过调用这个函数可以发送action给stero
const mapDispatchToProps = (dispatch)=>{
return{
addAge(age){
dispatch(addAge(age))
},
subAge(age){
dispatch(subAge(age))
},
changeName(){
dispatch(getNameAction("aaa"))
}
}
}
// connect()返回值是一个高阶函数
/**
* connect()参数1:store中的哪些数据需要映射到这个组件的props——函数
* connect()参数2:
*/
export default connect(mapStateToProps,mapDispatchToProps)(App)

React中的过渡动画

  • 动画插件:react-transition-group
  • npm install react-transition-group —save

Transition

与框架无关,常见的CSS动画使用方法:

1
2
3
4
5
6
7
8
.start{
position: relative;
left:10px;
}
.end{
left:20px;
transition:left 1s ease;
}

CSSTransition

CSSTransition具有三种状态

  • 开始状态,对应的类是-appear、-enter、-exit
  • 执行动画,对应的类是-appera-active、-enter-active、-exit-active
  • 执行结束,对应的类是-appera-done、-enter-done、-exit-done

CSSTransition常见属性:

  • in:触发进入或退出状态

    • 如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉
    • 当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class, 并且添加-enter-done的class
    • 当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并 且添加-enter-done的class
  • classNames:动画class的名称

    • 决定了在编写css时,对应的class名称:比如card-enter、card-enter-active、card-enter-done
  • timeout:动画过渡的时间,指的是动画执行后添加-enter-done的时间,动画真实执行时间由transition决定

  • appear:是否在初次进入添加动画(appear和in都要为true、同时要设置-appear类)

  • CSSTransition的钩子函数:

    • onEnter:在进入动画之前触发
    • OnEntering:在进入动画时触发
    • OnEntered:在应用进入动画结束时触发
    • ……

css代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 /*设置初始状态,不发生动画的状态,可以用appear的三个类替代*/
h2{
position: relative;
opacity: 0;
left: 0;
}
.h2show-enter{
opacity: 0;
left: 0;
}
.h2show-enter-active{
left: 100px;
opacity: 1;
transition: all 2s ease;
}
/*不设置done那么在动画执行完成那一刻会立即恢复到之前的状态,看是否需要恢复编写此类*/
.h2show-enter-done{
opacity: 1;
left: 100px;
}
.h2show-exit{
opacity: 1;
left: 100px;
}
.h2show-exit-active{
left: 0;
opacity: 0;
transition: all 2s ease;
}
/*不设置done那么在动画执行完成那一刻会立即恢复到之前的状态,看是否需要恢复编写此类*/
.h2show-exit-done{
left: 0;
opacity: 0;
}

jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from "react";
import { CSSTransition } from "react-transition-group";
import "./index.css"
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
isShow:false
}
}
change(){
this.setState({
isShow:!this.state.isShow
})
}
render(){
const {isShow} = this.state
console.log(isShow)
return (
<div>
<button onClick={()=>{this.change()}}>变化</button>
<CSSTransition in={isShow} classNames="h2show" timeout={2000} unmountOnExit={true}>
<h2>哈哈哈</h2>
</CSSTransition>
</div>
)
}
}
export default App

SwitchTransition

  • 控件两种状态之间的切换:on和off指定两种状态

  • mode属性:

    • in-out:新组件先进入,旧组件再移除

    • out-in:旧组件先移除,新组件再进入

  • SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹要切换的组件

  • SwitchTransition里面的CSSTransition或Tasnsition组件不再像之前那样接受属性来判断元素状态,取而代之的是key属性

css代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
h2{
position: relative;
left: 100px;
}
.h2show-enter{
left: 0;
}
.h2show-enter-active{
left: 100px;
transition: all 2s ease;
}
.h2show-enter-done{
left: 100px;
}
.h2show-exit{
left: 100px;
}
.h2show-exit-active{
left: 0;
transition: all 2s ease;
}
.h2show-exit-done{
left: 0;
}
  • 起始状态可以没有,那么刚开始就是默认状态
  • out-in模式:先制性exit代码,再执行enter代码,没有h2show-enter-done,动画执行结束会回归初始状态,可以没有h2show-exit-done
  • in-out模式:先执行enter代码,再执行exit代码,没有h2show-exit-done,动画结束会回归初始状态,可以没有h2show-enter-done

jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from "react";
import { CSSTransition,SwitchTransition } from "react-transition-group";
import "./index.css"
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
isShow:false
}
this.nodeRef = React.createRef()
}
change(){
this.setState({
isShow:!this.state.isShow
})
}
render(){
const {isShow} = this.state
console.log(isShow)
return (
<div>
<button onClick={()=>{this.change()}}>变化</button>
<SwitchTransition mode="in-out">
<CSSTransition
nodeRef = {this.nodeRef}
key={isShow?"exit":"login"}
classNames="h2show"
timeout={2000}
>
<h2 ref={this.nodeRef}>{isShow?"哈哈哈":"哈你妹"}</h2>
</CSSTransition>
</SwitchTransition>
</div>
)
}
}
export default App

TransitionGroup:

  • 有一组动画时,可以将这些CSSTransition放入到TransitionGroup中

有些搞不懂,官网看教程吧

React中CSS的编写

CSS组件化!!!:

  • 可以编写局部css:css具备自己的局部作用域,不会污染其他组件的元素
  • 可以编写动态css:可以获取当前组件的一些状态,根据状态生成不同的css样式
  • 支持所有的css特性:伪类、动画、媒体查询等
  • 编写起来简单方便,符合一贯的css风格特点

内联样式

  • style接受小驼峰命名属性的JavaScript对象,而不是字符串
  • 可以接受state中的状态来设置相关的格式

内联样式优点:

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

内联样式缺点:

  • 都需要驼峰标识
  • 某些样式没有提示
  • 大量样式,代码混乱
  • 某些样式无法编写,比如伪类/伪元素

普通CSS

  • 将CSS编写到一个单独的文件,之后进行引入

  • 和普通网页开发中的编写方式一致

    • 按照普通的网页标准去写,那么不会有太大的问题

    • 但是组件化开发中我们总是希望组件是一个独立的模块,样式只在自己内部生效,不会相互影响

    • 但是普通的CSS都属于全局的CSS,样式组件会相互影响
  • 这种编写方式的最大问题是样式之间会相互层叠掉

CSS modules

  • 将.css文件修改为.module.css看就可以引用并且局部使用
  • 但是类名不能使用(.home-title),因为JavaScript不识别-
  • 所有的className都必须使用{style.className}的形式来编写
  • 不方便动态的修改某些样式,依然需要使用内联样式的方式

CSS in Js

CSS in Js是一种模式,其中CSS有javaScript生成而不是在外部文件中定义

  • 传统的前端开发中,HTML、CSS、JavaScript通常分类
  • React认为逻辑和UI是无法分离的,所以才会有了JSX的语法
  • JSX是Html in Js,加上CSS in Js在完整的体现的逻辑和UI无法分离,因此React被称为All in JS

CSS in JS库:styled-components:

  • CSS-in-JS使用JavaScript为CSS赋予一些能力,可以类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修改
  • CSS预处理器也具备某些能力,但获取动态状态依然是一个不好处理的点

ES6标签模板字符串

image-20230329161529306

即函数可以通过字符串的方式来调用

  • 第一个参数是数组,是被模块字符串拆分后的组合
  • 后面的元素是一个个模块字符串传入的内容
  • 注意是反引号

styled-components的使用

基本使用

jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from "react";
import { AppWrapper, StrongWrapper} from "./style";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
render(){
return (
<AppWrapper>
<div className="container">
<span>哈哈哈</span>
<StrongWrapper>我是strong</StrongWrapper>
<div className="context">
<span>嘿嘿嘿</span>
</div>
<div className="context1">
<span>嗯嗯嗯</span>
</div>
</div>
</AppWrapper>
)
}
}
export default App

style.js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import styled from "styled-components";
export const AppWrapper = styled.div`
.container>span{
width:100%;
color: red;
}
.container{
.context1{
color: orange;
}
}
.container .context{
color: blue;
}
.container .context:hover{
color: #cccc;
}
&:hover{
text-decoration: line-through;
}
`
export const StrongWrapper =styled.strong`
color:skyblue
`
  • 样式js的命名不能和组件名称相同,例如不能用App.js
  • 可以像正常CSS一样编写CSS代码
  • 可以用&来获取当前元素
  • 可以通过设置styled.__来设置包裹的类型
  • styled返回的是一个组件,可以按照组件的方法使用

props\attrs属性

  • 获取props需要通过${}传入一个插值函数,props会作为该函数的参数;
  • 这种方式可以有效的解决动态样式的问题

CSS代码:

1
2
3
4
5
6
7
8
9
10
11
12
import styled from "styled-components";
export const AppWrapper = styled.div.attrs({
tcolor:props=>(props.color||"blue")
})`
.container{
width:100%;
color: ${props=>{
return props.tcolor
}};
}

`

jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";
import { Children } from "react";
import { AppWrapper, StrongWrapper} from "./style";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state = {
checked:true
}
}
render(){
const {checked} = this.state
return (
<AppWrapper color={checked?"red":""}>
<div className="container">
<h2>哈哈哈</h2>
</div>
<button onClick={()=>{this.setState({checked:!this.state.checked})}}>改变颜色</button>
</AppWrapper>
)
}
}
export default App

注:

  • tcolor不能与color同名,要不然会循环拿取
  • tcolor传入的是一个函数,括号可以省略,会自动调用生成值
  • tcolor:props=>(props.color||”blue”)是为了处理不传值时给定默认值,没有默认值时可以在下面直接取

styled的继承

CSS代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import styled from "styled-components";
export const AppWrapper = styled.div`
display: flex;
`
export const Button1 = styled.button`
border-radius: 5px;
border:1px solid red;
background-color: gray;
opacity: 0.5;
`
export const Button2 = styled(Button1)`
color: skyblue;
`

jsx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import { Children } from "react";
import { AppWrapper, Button1, Button2} from "./style";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
render(){
return (
<AppWrapper>
<Button1>父Button</Button1>
<Button2>子Button</Button2>
</AppWrapper>
)
}
}
export default App

image-20230329184407490

image-20230329184548981

一、二叉树种类

满二叉树

完全二叉树

二叉搜索树

左节点小于中间节点,右节点大于中间节点

平衡二叉搜索树

左节点小于中间节点,右节点大于中间节点,且左右子树的高度绝对值小于1

c++中map、set、multimap、multiset的底层是通过平衡二叉树实现,那么在插入节点,查询节点的时间复杂度是O(logn)

map中的key和set中的元素都是有序的

二、二叉树存储方式

链式存储

线式存储

左孩子:i*2+1

右孩子:i*2+2

三、二叉树的遍历

深度优先搜索

前序、中序、后序遍历——迭代法、递归法

广度优先搜索

层序遍历——迭代法

对象属性操作的控制

  • Object.defineProperty可以对对象的属性加以限制,例如不能通过delete删除,不能通过for-in/Object.keys遍历
  • Object.defineProperty方法会直接在一个对象上定义一个新的属性,或者修改一个对象的现有属性,并返回该对象

Object.defineProperty参数:Object.defineProperty(obj, prop, descriptor)

  • obj:要定义的对象
  • prop:要定义或修改的属性的名称或Symbol
  • descriptor:要定义或修改的属性描述符

属性描述符分类

image-20230327092658898

Configuratble:属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符

  • 通过对象定义某个属性,其Configurable为true
  • 通过属性描述符定义属性时,属性的Configurable默认为false

Enumerable:属性是否可以通过for-in或者Object.keys()返回属性

  • 通过对象定义时,属性的Enumerable为true
  • 通过属性描述符定义属性时,属性的Enumerable默认为false

Writable:属性是否可以修改值

  • 通过对象定义时,属性的Writable为true
  • 通过属性描述符定义属性时,属性的Writable默认为false

value:属性的value值,读取属性会返回该值,修改属性时,会对其进行更改

  • 默认情况下这个值是undefined

get:获取属性时会执行的函数,默认为undefined

set:设置属性时会执行的函数,默认为undefined

注:get/set与value/Writable不可共存

补充:

同时定义多个属性:Object.defineProperties()

image-20230327093656890

获取对象的属性描述符:

  • getOwnPropertyDescriptor
  • getOwnPropertyDescriptors

禁止对象扩充新属性:preventExtensions Object.preventExtensions(obj)

  • 给一个对象添加新的属性会失败

密封属性,不允许配置和删除属性:seal Object.seal(obj)

  • 实际上是调用preventExtensions
  • 将现有属性的Configurable设置为false

冻结属性,不允许修改现有属性:freeze Object.freeze(obj)

实际上是调用seal

将现有属性的Writable设置为false

一、柯里化函数

柯里化:

把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回 结果的新函数的技术

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数

实现多数字加法的柯里化

加法函数:

1
2
3
var add0 = function(x,y,z){
return x + y + z
}

柯里化函数:

1
2
3
4
5
6
7
var add1 = function(x){
return function(y){
return function(z){
return x + y + z
}
}
}

箭头函数优化:

1
var add2 = x=>y=>z=>x+y+z

考虑会同时传递进去1个或2个参数

1
2
3
4
5
6
7
var add3 = (...args)=>{
if(args.length===3) return args.reduce((prev,curr)=>prev+curr)
return (...other)=>{
return add3(...args,...other)
}
}
// 未考虑传入参数大于3个时的边界情况

如果我们想得到可以获取不同个参数的柯里化函数呢,自动柯里化:

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn = function(x,y,z,g){
return x+y+z+g
}
var curryMyFn= function(fn){
return curryFn = function(...args){
if(args.length>=fn.length) return fn(...args)
return (...newArgs)=>{
return curryFn(...args.concat(newArgs))
}
}
}
var bar = curryMyFn(fn)
console.log(bar(10,20)(40,50))

二、apply、call、bind的手写

this绑定

  • this绑定共4种,默认绑定、隐式绑定、显示绑定、new绑定
  • 实现显示绑定即是通过其余简单绑定方式实现,而默认绑定的this一般为windows,所以这里只能用隐式绑定实现

apply与call的手写实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 通过symbol实现apply
Function.prototype.myApply = function(thisArgs, args){
thisArgs = (thisArgs===null || thisArgs ===undefined)?window:Object(thisArgs)
let symbol = Symbol()
thisArgs[symbol] = this
let fn = thisArgs[symbol](...args)
delete thisArgs[symbol]
return fn
}
// 通过symbol实现call
Function.prototype.myCall = function(thisArgs, ...args){
thisArgs = (thisArgs===null || thisArgs ===undefined)?window:Object(thisArgs)
let symbol = Symbol()
thisArgs[symbol] = this
let fn = thisArgs[symbol](...args)
delete thisArgs[symbol]
return fn
}
// 不通过symbol、定义属性不可枚举
Function.prototype.myApply1 = function(thisArgs, args){
thisArgs = (thisArgs===null || thisArgs ===undefined)?window:Object(thisArgs)
Object.defineProperty(thisArgs, "temp",{
configurable:true,
Enumerable:false,
value:this
})
let fn = thisArgs["temp"](...args)
delete thisArgs["temp"]
return fn
}
// 不通过symbol且不定义属性不可枚举
Function.prototype.myApply2 = function(thisArgs, args){
thisArgs = (thisArgs===null || thisArgs ===undefined)?window:Object(thisArgs)
thisArgs.temp = this
let fn = thisArgs.temp(...args)
delete thisArgs.temp
return fn
}
  • 首先要排除thisArgs为null或undefined的情况,同时对数字或字符串包装为对象(Object(object)还是object)
  • 方法添加到Function的原型上,使得所有函数都可以直接调用
  • 使用symbol是因为Symbol()可以创建出一个独一无二的值,那么肯定不会覆盖掉原来对象中可能存在的值,同时Symbol属性不可以通过简单的属性获取得到,在后续的逻辑编写中不会出现错误
  • 如果不适用Symbol,则需要通过Object.defineProperty使其不可枚举Object.defineProperty

测试:

1
2
3
4
5
6
7
8
let bar = function(name,age){
console.log(this, Object.keys(this),name,age)
}

bar.myApply({method:"myApply-symbol"},["ouwenwu",18])
bar.myApply1({method:"myApply-没有symbol-不枚举"},["ouwenwu",18])
bar.myApply2({method:"myApply-没有symbol-枚举"},["ouwenwu",18])
bar.apply({method:"apply-js自带"},["ouwenwu",18])

image-20230326222135494

综上:直接使用symbol属性来完成手写

bind的手写实现

根据上面的经验,这里直接用symbol

bind与apply、call的区别主要是返回一个函数

1
2
3
4
5
6
7
Function.prototype.myBind = function(thisArgs,...args){
let symbol = Symbol()
thisArgs[symbol] = this
return function(...args){
return thisArgs[symbol](...args)
}
}

三、es5继承的实现

继承:子类继承父类的函数与属性

注:类具有显示原型、对象具有隐式原型

1.原型链继承

父类的实例化对象作为子类的显示原型——就可以通过原型链去访问得到父类的属性与方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./jquery.js"></script>
<style>
</style>
</head>
<body>
<script>
function Person(name,age){
this.name = name,
this.age = age
}
Person.prototype.eating = function(){
console.log("eating")
}
const person = new Person("oww", 23)
function Student(sno){
this.sno = sno
}
Student.prototype = person
Student.prototype.studying = function(){
console.log("studying")
}
const student = new Student("111")
student.eating()
student.studying()
</script>
</body>
</html>

image-20230405183113993

有一个很大的弊端:某些属性其实是保存在p对象上的:

  • 我们通过直接打印对象是看不到这个属性的
  • 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题——其他子类修改会同时改变这边
  • 不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化)——子类的name、age在student中写死了

2.借用构造函数继承

原型链继承子类中的属性是通过父类实例定义的,没有自己的属性,不能传入值更改,借用构造函数可以解决这个问题

  • 通过apply()和call()方法也可以在新创建的对象上执行父类构造函数

事件总线(EventBus)

事件总线是一种观察者模式,包括三个角色

  • 发布者:发布事件
  • 订阅者:订阅事件,并进行响应
  • 时间总线:无论发布者还是订阅者都通过事件总线作为中台

目的:开发过程中不同组件之间通信 组件通信

事件总线的实现

  • 事件的监听方法on
  • 事件的发射方法emit
  • 事件的取消监听off

由于一个EventBus对象会在多个地方使用,所以将其中的属性和函数定义为静态的

也可采用实例化的方法,那样需要把这个实例放在全局变量中,如在React在根节点定义Context

EventBus的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class EventBus{
static eventMap = {}

static on(eventName, eventFn){
(this.eventMap[eventName] || (this.eventMap[eventName] = []))&&this.eventMap[eventName].push(eventFn)
}
static emit(eventName, ...args){
if(!this.eventMap[eventName]){
console.log(eventName,"没有监听")
return
}
for(let fun of this.eventMap[eventName]){
fun.apply(this,args)
}
}
static off(eventName, eventFn){
let eventFns = this.eventMap[eventName]
if(!eventFns){
console.log(eventName,"没有监听")
return
}
for(let i=0;i<eventFns.length;i++){
if(eventFn===eventFns[i]){
eventFns.splice(i,1)
console.log(this.eventMap)
break
}
}
}
}

EventBus的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let function1 = function(...args){
console.log("function1", this.eventMap, args)
}
let function2 = function(...args){
console.log("function2", this.eventMap, args)
}

EventBus.on("click",function1)
EventBus.on("click",function2)

EventBus.on("click2",function1)

EventBus.emit("click", "oww", "18")
EventBus.emit("click2", "oww1", "22")

EventBus.off("click",function2)

image-20230325134633475

堆:

  • 堆是一种完全二叉树,复习一下完全二叉树的定义,完全二叉树的形式是指除了最后一层之外,其他所有层的结点都是满的,而最后一层的所有结点都靠左边。
  • 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

image-20230324192427183

完全二叉树:对于任意一个父节点的序号n来说(这里n从0算),它的子节点的序号一定是2n+1,2n+2,因此我们可以直接用数组来表示一个堆。

堆的创建:

1
2
3
4
5
6
class Heap{
constructor(){
this.array = []
this.length = 0
}
}

image-20230324192636065

大顶堆/最大堆:

对于任意一个父节点来说,其子节点都小于这个父节点

最大堆的插入:

  • 当一个元素要插入时,先放到堆尾,根据堆的特性,对位置进行调整。
  • 父节点要大于直接点,找到插入节点的父节点,比父节点值大则交换位置,否则插入成功
  • 交换后,继续检测该节点与现在父节点的大小,如果大则交换位置,一直上浮到根节点

image-20230324193433568

JavaScript实现:length可以用this.array.length替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
insert(x){
this.array.push(x)
if(this.array.length===0){
return
}
let x_index = this.array.length-1
let parent_index = x_index
while(parent_index!==0){
let parent_index = x_index%2===0?(x_index-2):(x_index-1)
parent_index = parent_index/2
if(this.array[parent_index]<x){
this.array[x_index] = this.array[parent_index]
this.array[parent_index] = x
}else{
break
}
x_index = parent_index
}
return
}
// 循环最大次数k即为树的深度:2^0+2^1+...+2^k = n 即k=logn 时间复杂度为O(logN)

最大堆的删除

  • 堆的删除一般是值删除堆顶元素,先删除堆顶元素
  • 要保证堆一直为完全二叉树,不能直接拿子节点的较大值,而是将末尾元素拿到堆顶,再下沉
  • 下沉即先比较该节点与左子节点,如果子节点更大,则交换,如果子节点更小,则与右节点比较
  • 直至到最深层,或者比左右子节点都大

JavaScript实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pop(){
this.array[0] = this.array[this.array.length-1]
this.array.pop()
let x_index = 0
while(true){
let left_index = 2 * x_index + 1
let right_index = 2 * x_index + 2
left_index = left_index>=this.array.length?x_index:left_index
right_index = right_index>=this.array.length?x_index:right_index
let large_child_index = this.array[x_index]<this.array[left_index]?left_index:x_index
large_child_index = large_child_index!==x_index?large_child_index:(this.array[x_index]<this.array[right_index]?right_index:x_index)
if(large_child_index===x_index) break
let temp = this.array[x_index]
this.array[x_index] = this.array[large_child_index]
this.array[large_child_index] = temp
x_index = large_child_index
}

}
// 最大循环次数k即为树的深度 k = logn 时间复杂度为O(logN)

小顶堆/最小堆

插入与删除与最大堆类似

插入JavaScript实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
insert(x){
debugger
this.array.push(x)
if(this.array.length===0){
return
}
let x_index = this.array.length-1
let parent_index = x_index
while(parent_index!==0){
let parent_index = x_index%2===0?(x_index-2):(x_index-1)
parent_index = parent_index/2
if(this.array[parent_index]>x){
this.array[x_index] = this.array[parent_index]
this.array[parent_index] = x
}else{
break
}
x_index = parent_index
}
return
}

image-20230324210219667

删除JavaScript实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pop(){
debugger
this.array[0] = this.array[this.array.length-1]
this.array.pop()
let x_index = 0
while(true){
let left_index = 2 * x_index + 1
let right_index = 2 * x_index + 2
left_index = left_index>=this.array.length?x_index:left_index
right_index = right_index>=this.array.length?x_index:right_index
let small_child_index = this.array[x_index]>this.array[left_index]?left_index:x_index
small_child_index = small_child_index!==x_index?small_child_index:(this.array[x_index]>this.array[right_index]?right_index:x_index)
if(small_child_index===x_index) break
let temp = this.array[x_index]
this.array[x_index] = this.array[small_child_index]
this.array[small_child_index] = temp
x_index = small_child_index
}

}

image-20230324210321704

解决前K个高频元素/低频元素题

LeetCode链接

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

  • 输入: nums = [1,1,1,2,2,3], k = 2
  • 输出: [1,2]

示例 2:

  • 输入: nums = [1], k = 1
  • 输出: [1]

提示:

  • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 $O(n \log n)$ , n 是数组的大小。
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
  • 你可以按任意顺序返回答案。

步骤:

  • 获取到每个数据出现的频率,使用哈希表,这里可以用map,key是数字,value是次数
  • 对频率排序
  • 找出前K个高频元素

注:

  • 如果使用快速排序的时间复杂度为O(n)
  • 仅维护大小为K的小顶堆,小顶堆的堆顶始终是最小值,如果堆长度小于K则继续往队礼添加,如果长度大于K则弹出堆顶的最小值

image-20230324211848724

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// js 没有堆 需要自己构造
class Heap {
constructor(compareFn) {
this.compareFn = compareFn;
this.queue = [];
}

// 添加
push(item) {
// 推入元素
this.queue.push(item);

// 上浮
let index = this.size() - 1; // 记录推入元素下标
let parent = Math.floor((index - 1) / 2); // 记录父节点下标

while (parent >= 0 && this.compare(parent, index) > 0) { // 注意compare参数顺序
[this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]];

// 更新下标
index = parent;
parent = Math.floor((index - 1) / 2);
}
}

// 获取堆顶元素并移除
pop() {
// 堆顶元素
const out = this.queue[0];

// 移除堆顶元素 填入最后一个元素
this.queue[0] = this.queue.pop();

// 下沉
let index = 0; // 记录下沉元素下标
let left = 1; // left 是左子节点下标 left + 1 则是右子节点下标
let searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;

while (searchChild !== undefined && this.compare(index, searchChild) > 0) { // 注意compare参数顺序
[this.queue[index], this.queue[searchChild]] = [this.queue[searchChild], this.queue[index]];

// 更新下标
index = searchChild;
left = 2 * index + 1;
searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;
}

return out;
}

size() {
return this.queue.length;
}

// 使用传入的 compareFn 比较两个位置的元素
compare(index1, index2) {
// 处理下标越界问题
if (this.queue[index1] === undefined) return 1;
if (this.queue[index2] === undefined) return -1;

return this.compareFn(this.queue[index1], this.queue[index2]);
}

}

const topKFrequent = function (nums, k) {
const map = new Map();

for (const num of nums) {
map.set(num, (map.get(num) || 0) + 1);
}

// 创建小顶堆
const heap= new Heap((a, b) => a[1] - b[1]);

// entry 是一个长度为2的数组,0位置存储key,1位置存储value
for (const entry of map.entries()) {
heap.push(entry);

if (heap.size() > k) {
heap.pop();
}
}

// return heap.queue.map(e => e[0]);

const res = [];

for (let i = heap.size() - 1; i >= 0; i--) {
res[i] = heap.pop()[0];
}

return res;
};

由于key和value是要绑定的,所以元素不能是简单的数组,而应该是包含key和value的二维数组,同时为了使得代码重用,将比较函数抽取出来,当做参数传递

时间复杂度

时间复杂度:时间增长趋势 T(n) = O(f(n))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
for(let i=0;i<=n;i++){
x++
}
// let i=0 1
// i<=n; i++; x++ 各n次
// 结果为 O(1+3n) = O(n) O(n)算n趋近无限大,所以常数忽略

for(let i=0;i<=n;i++){
for(let j=0;j<=n;j++){
x++
}
}
// 时间复杂度:O(n)*O(n)=O(n^2)

for(let i=0;i<=n;i++){
x++
}
for(let i=0;i<=n;i++){
for(let j=0;j<=n;j++){
x++
}
}
// 时间复杂度 O(n+n^2) = O(n^2)

let x = 0
let y = 1
let temp = x
x = y
y = temp
// 时间复杂度不随着任何一个变量增大而增大,复杂度为O(1)


let i = 1
while(i<n){
i = i*2
}
// 循环次数k:2^k = n k=logn O(logn)


for(let j=0;j<=n;j++){
let i = 1
while(i<n){
i = i*2
}
}
// 时间复杂度O(n)*O(logn) = O(nlogn)


for(let i=0;i<=n;i++){
for(let j=0;j<=m;j++){
x++
}
}
// 时间复杂度O(n)*O(m) = O(nm)

空间复杂度

空间复杂度:内存空间增长的趋势

1
2
3
4
5
6
7
8
9
10
11
let x = 0
let y = 1
let temp = x
x = y
y = temp
// x和y无论多大多不会影响空间占用 O(1)
let arr = []
for(let i=0;i<n;i++){
arr.push(i)
}
// 占用空间与n成正比,O(n)

组件化开发(React二)

组件化开发:如果把一个页面的所有处理逻辑放在一起,处理起来会很复杂,因此将一个页面拆分为不同的功能块

  • 一个完整的页面可以划分为很多个组件
  • 每个组件都用于实现页面的一个功能
  • 每一个组件又可以细分
  • 组件本身又可以在很多地方复用

image-20230324172119865

React组件:

  • 类组件与函数组件
  • 根据内部是否有状态需要维护:无状态组件、有状态组件(this.state,不考虑hooks的情况下函数组件是无状态组件,而类组件不定义this.state也可以看做无状态组件)
  • 根据组件的不同职责:展示型组件、容器型组件

组件之间有重叠,但都是为了数据逻辑和UI展示的分离

  • 函数组件、无状态组件、展示型组件主要关注UI的展示
  • 类组件、有状态组件、容器型组件主要关注数据逻辑

类组件与函数组件

类组件

定义要求:首字母大写、继承自React.Component、实现render函数

  • constructor可选,在其中初始化数据
  • this.state中维护组件内部的数据
  • render是类组件唯一必须实现的方法

函数组件(没有hooks的函数组件)

  • 没有生命周期、会被挂载、没有生命周期函数
  • this关键字不能指向组件实例,因为没有组件实例
  • 没有内部状态state
1
2
3
4
5
6
7
function App(){
return (
<div>Hello World</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)

生命周期

生命周期:从创建到销毁的整个过程,了解生命周期在最合适的地方完成想要的功能

React的生命周期、生命周期函数

  • 装载阶段:组件第一次在DOM树种被渲染的过程——装载完成:componentDidMount
  • 更新阶段:组件状态发生变化,重新更新渲染的过程——更新完成:componentDidUpdate
  • 卸载阶段:组件从DOM树种被移除的过程——即将移除:componentWillUnmount

image-20230324173626696

各函数任务及时刻

constructor:

  • 不初始化state或不进行方法绑定,则可以不实现构造函数
  • 作用一:给this.state赋值对象来初始化state
  • 作用二:为事件绑定this——调用super()

componentDidMount:

  • 组件挂载到DOM Tree后立马调用
  • 作用一:进行依赖于DOM的操作
  • 作用二:发送网络请求的最好位置
  • 作用三:添加订阅

componentDidUpdate:

  • 组件更新后立即调用,首次渲染不会执行这个方法
  • 作用一:组件更新后,此处进行DOM相关操作
  • 作用二:当更新前后props发送了变化,此处发送网络请求

componentWIllUnmount:

  • 组件卸载及销毁之前直接调用
  • 作用一:进行必要的清理操作
  • 作用二:清除timer,取消网络请求或取消上面创建的订阅

不常用生命周期函数

  • getDerivedStateFromProps:state 的值在任何 时候都依赖于 props时使用;该方法返回一个对象 来更新state
  • getSnapshotBeforeUpdate:在React更新DOM 之前回调的一个函数,可以获取DOM更新前的一 些信息(比如说滚动位置)
  • shouldComponentUpdate:该生命周期函数很 常用,但是我们等待讲性能优化时再来详细讲解

image-20230324174415395

组件之间的通信

父组件传值到子组件

  • 父组件通过 属性=值 的形式来传递给子组件数据
  • 子组件通过props参数获取父组件传过来的值

默认值设置:ChildComponent.defaultProps

参数类型设置:ChildComponent.propTypes isRequired代表是否必须

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state = {
message: "I am your father!",
info:"You are my child!"
}
}
render(){
let {message,info} = this.state
return (
<div>
<h2>{message}</h2>
<ChildComponent info = {info}/>
</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react'
import { PropTypes } from 'prop-types'
export class ChildComponent extends Component {

render() {
return ([
<div key="1">{this.props.info}</div>,
<div key="2">{this.props.info2}</div>
])
}
}
ChildComponent.propTypes = {
info: PropTypes.string.isRequired
}
ChildComponent.defaultProps = {
info:"Tell me who are you!",
info2:"You are my Dad!"
}
export default ChildComponent

image-20230324180859034

子组件传值到父组件

  • 通过props给子组件传递一个回调函数,在子组件中调用这个函数

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state = {
message: "I am your father!",
info:"You are my child!"
}
}
dataFromChild = function(info){
this.setState({
message:info
})
}
render(){
let {message,info} = this.state
return (
<div>
<h2>{message}</h2>
<ChildComponent info = {info} dataFromChild = {(message)=> this.dataFromChild(message)}/>
</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import { PropTypes } from 'prop-types'
export class ChildComponent extends Component {
sendDataToFather = function(){
this.props.dataFromChild(555)
}
render() {
return ([
<div key="1" onClick = {()=>{this.sendDataToFather()}}>{this.props.info}</div>,
<div key="2">{this.props.info2}</div>
])
}
}
ChildComponent.propTypes = {
info: PropTypes.string.isRequired
}
ChildComponent.defaultProps = {
info:"Tell me who are you!",
info2:"You are my Dad!"
}
export default ChildComponent

注:各个地方函数的使用要注意,是否需要this,一般传递过去的函数都是箭头函数,调用的函数一般为function

1
2
<ChildComponent info = {info} dataFromChild = {(message)=> this.dataFromChild(message)}/>
<ChildComponent info = {info} dataFromChild = {this.dataFromChild}/>

如果直接传递,那么函数的调用由React确定就不知道this是啥

传递过去箭头函数,那么我们不关心什么时候调用这个箭头函数,而箭头函数包含的是隐式绑定的this,所以调用的函数需要function,要不然找不到this

React中实现插槽

为了使组件具有更好的通用性,组件的内容不能限制为固定的div、span等,因此父组件传元素到子组件中,使得元素类型可以多变

image-20230324214907031

通过组件的children子元素实现(父像子传元素)

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'
export class ChildComponent extends Component {
render() {
const {children} = this.props
return (
<div>
<div>
{children[0]}
</div>
<div>
{children[1]}
</div>
<div>
{children[2]}
</div>
</div>
)
}
}
export default ChildComponent

父组件代码1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<ChildComponent>
<button>按钮</button>
<h2>标题</h2>
<span>span</span>
</ChildComponent>
)
}
}
export default App

结果:

image-20230324215146004

更改父组件的子元素

1
2
3
4
5
<ChildComponent>
<span>按钮</span>
<i>标题</i>
<button>按钮</button>
</ChildComponent>

结果:

image-20230324215351358

通过props实现插槽(父向子传元素)

  • 上面描述过通过props传值到子元素,props可以传的值不仅仅有JavaScript对象,React对象也可以作为参数传过去
  • 可以避免通过children获取时索引不能精准的获取传入的元素

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
const spanJSX = <span>按钮</span>
const iJSX = <i>标题</i>
const buttonJSX = <button>按钮</button>
return (
<ChildComponent spanJSX={spanJSX} iJSX = {iJSX} buttonJSX={buttonJSX}/>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'
export class ChildComponent extends Component {
render() {
const {spanJSX,iJSX,buttonJSX} = this.props
return (
<div>
<div>{spanJSX}</div>
<div>{iJSX}</div>
<div>{buttonJSX}</div>
</div>
)
}
}
export default ChildComponent

结果:

image-20230324220143962

作用域插槽

  • 上面两种插槽方法子组件接受到的都是有父组件写死的元素,但某些时候在子组件中需要根据状态更改组件的内容
  • 所以需要实现元素类型父组件决定,数据或状态子组件决定
  • 类似于这种由父组件子组件共同协作完成的,通常使用传递函数实现

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";
import ChildComponent from "./component/ChildComponent";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<ChildComponent itemtyPE = {item=><button>{item}</button>}/>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react'
export class ChildComponent extends Component {
render() {
const {itemtyPE} = this.props
const message = "哈哈哈"
return (
<div>
{itemtyPE(message)}
</div>
)
}
}
export default ChildComponent

image-20230325112140472

非直接父子组件的通信

常见方法:

  • 通过props属性自上而下传递
  • 但是对于某些数据需要在多个组件中共享(如地区、用户信息等等)
  • 如果在顶层App定义这些信息,一层层传递下去,对于一些中间层不需要这些数据的组件来说,是一种冗余的操作

Context方法:

  • context提供了一种在组件之间共享值的方法,而不必显示的通过树的逐层传递props
  • context的目的是共享对于组件树来说是“全局的数据”,如当前用户、主题

注册Context

由于后续会在多个组件中用到这个Context1,所以将Context1的注册放到一个js文件中

1
2
3
4
5
import React from "react";

const Context1 = React.createContext()

export default Context1

Context值的设置

父组件设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
// 通过COntext1.Provider 包围要传递值的子组件,那么这个子组件及其后代组件都可以获取到传递的值
return (
<Context1.Provider value={{color:"red",size:18}}>
<ChildComponent/>
</Context1.Provider>
)
}
}
export default App

Context值的获取

后代类组件Context值的获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react'
import Context1 from '../context/Context1'
import GrandsonComponent from './GrandsonComponent'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.context.color}}>子组件</h2>
<GrandsonComponent/>
</div>
)
}
}
ChildComponent.contextType = Context1 // 通过设置contextType为this.context指定Context,然后通过this.context获取值
export default ChildComponent

后代函数组件Context值获取:

由于函数组件中没有this,所以不能够使用上面类组件获取值的方法,使用Context.Consumer获取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Context1 from "../context/Context1"

export function GrandsonFunComponent(){
return(
<Context1.Consumer>
{
value=>{
return <h2 style={{color:value.color}}>函数孙子组件</h2>
}
}
</Context1.Consumer>
)
}
export default GrandsonFunComponent

多个Context的注册与获取

注册第二个Context:

1
2
3
4
5
import React from "react";

const Context2 = React.createContext()

export default Context2

单组件设置多Context:

父组件采用Context嵌套的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
import Context2 from "./context/Context2";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<Context2.Provider value={{color:"blue"}}>
<Context1.Provider value={{color:"red",size:18}}>
<ChildComponent/>
</Context1.Provider>
</Context2.Provider>
)
}
}
export default App

后代组件获取多Context:

通过设置contextType与Context.Consumer组合获取多个Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component } from 'react'
import Context1 from '../context/Context1'
import Context2 from '../context/Context2'
import GrandsonComponent from './GrandsonComponent'
import GrandsonFunComponent from './GrandsonFunComponent'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.context.color}}>子组件</h2>
<Context2.Consumer>
{
value=>{
return <h3 style={{color:value.color}}>子组件2</h3>
}
}
</Context2.Consumer>
<GrandsonComponent/>
<GrandsonFunComponent/>
</div>
)
}
}
ChildComponent.contextType = Context1
export default ChildComponent

Context默认值的使用

使用了Context但不是Context.Provider的后代元素

Context的注册(带默认值):

1
2
3
4
5
import React from "react";

const Context1 = React.createContext({color:"green"})

export default Context1

Context的设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
import Context2 from "./context/Context2";
//编写组件
class App extends React.Component{
constructor(){
super()
}
render(){
return (
<div>
<Context2.Provider value={{color:"blue"}}>
<Context1.Provider value={{color:"red",size:18}}>
<ChildComponent/>
</Context1.Provider>
</Context2.Provider>
<ChildComponent/>
</div>
)
}
}
export default App

Context的获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import Context1 from '../context/Context1'
import Context2 from '../context/Context2'
import GrandsonComponent from './GrandsonComponent'
import GrandsonFunComponent from './GrandsonFunComponent'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.context.color}}>子组件</h2>
<GrandsonComponent/>
<GrandsonFunComponent/>
</div>
)
}
}
ChildComponent.contextType = Context1
export default ChildComponent

image-20230325122726808

任意组件之间的通信——事件总线(EventBus)

事件总线实现

setState的细节原理

为什么要setState:React没有数据劫持,直接更改this.state时,React不知道数据已经发生了变化,需要通过this.setState()来告知React数据发生了变化,需要刷新页面

setState的异步/同步

一般情况下,setSTate是异步的

设置为异步的原因:

  • 可以显著提升性能,如果每次调用setState都进行一次更新,那么render会频繁的调用,界面重新渲染,这样效率很低(可以联系到回流-重绘),因此设置为异步获取到数据的多次更新,之后批量更新
  • 如果同步更新了state,但还没执行render函数,那么state与子组件的props不能保持同步,可能出现若干问题

setState批量更新方法

  • 获取到一系列setState的更新值,按先后顺序把旧的state与新的state合并(添加新的属性、更新改变了的属性),在最后将合并好的state赋值给this.state并刷新到页面上(SCU优化)
  • 注意:先合并再赋值给this.state

异步结果的获取:

既然是异步,那么我们便不知道更新啥时候结束,但有些时候又想在更新结束后进行一定的操作,这个操作应该在哪里进行?

方法一:setState的回调函数

  • setState接受两个参数:新的state+回调函数
  • 回调执行顺序:render->componentDidUpdate(因此也可以在这个生命周期函数中对更新结束后的值操作)->setState回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React from "react";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
counter:3
}
}
buttonClick(){
this.setState({
counter:this.state.counter + 1
},()=>{
console.log("setState回调",this.state)
})
this.setState({
counter:this.state.counter + 1
},()=>{
console.log("setState回调",this.state)
})
this.setState({
counter:this.state.counter + 1
},()=>{
console.log("setState回调",this.state)
})
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
render(){
console.log("render")
const { counter } = this.state
return (
<div>
<h2>{counter}</h2>
<button onClick={()=>{this.buttonClick()}}>更新state</button>
</div>
)
}
}
export default App

image-20230327111305434

注:

  • 多次setState合并,一次render,一次componentDidUpdate,多次回调
  • 这里多次一个提交了3次+1,但是最后结果只加了1,是因为this.state会在多次提交的state与原始state合并结束后再赋值给this.state,也就是这里获取到的this.state每次都是初始值3,相当于提交了3次 counter=4

this.setState的第一个参数也可以是一个函数:

  • 返回值是要更改的值
  • 参数是已经合并后的state而不是this.state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React from "react";
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
counter:3
}
}
buttonClick(){
setTimeout(()=>{
console.log("setTimeOut宏任务")
},0)
new Promise((resolve)=>{
resolve("")
}).then((res)=>{
console.log("promise微任务")
})
this.setState((state)=>{
console.log("setState",state)
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
render(){
console.log("render")
const { counter } = this.state
return (
<div>
<h2>{counter}</h2>
<button onClick={()=>{this.buttonClick()}}>更新state</button>
</div>
)
}
}
export default App

执行顺序为:this.setState第一个参数函数->render->componentDidUpdate->this回调

image-20230327112819884

注:

  • 多次setState合并,一次render,一次componentDidUpdate,多次回调
  • setState函数执行、render、componentDidUpdate执行都是在微任务后,宏任务前,因此前面的函数应该都是放在微任务队列中
  • 提交3次+1,最后得到的结果也和预期相同,因为将3次setState的第一个参数函数放入微任务队列中,在原来的state基础上进行更改,然后合并,得到新的state,在调用下一个微任务setState时,用前面合并好的state作为参数,再进行更改与合并操作
  • 虽然setState中是在微任务中分3次调用,但setState的回调是在完成componentDidUpdate再放入微队列中,此时this.state已经更改为最后值,所以获取到的都是最后结果

回调的好处:

  • 在数据更改后处理一些state的逻辑
  • 回调函数会把之前的state和props传入进来
  • 由于setState是一个异步函数,所以对更新后值的处理要放入回调中

方法二:组件的生命周期函数componentDidUpdate

上面已讲

setState的同步

  • React18之前,在组件生命周期和React合成事件中,setState是异步
  • React18之前,在setTimeout或者原生dom事件中,setState是同步

注:

  • 由React去决定运行时机的大多是异步
  • 由setTimeout/或者原生Dom的响应函数中调用这种用户编写决定运行时机的一般是同步

React18之后,setState默认都是异步的

React18之后获取同步的setState

使用特殊的flushSync操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import React from "react";
import {flushSync} from "react-dom"
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
counter:3
}
}
buttonClick(){
setTimeout(()=>{
console.log("setTimeOut宏任务")
},0)
new Promise((resolve)=>{
resolve("")
}).then((res)=>{
console.log("promise微任务")
})

flushSync(()=>{
this.setState((state)=>{
console.log("setState",state)
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
this.setState((state)=>{
return {counter:state.counter+1}
},()=>{
console.log("setState回调",this.state)
})
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
render(){
console.log("render")
const { counter } = this.state
return (
<div>
<h2>{counter}</h2>
<button onClick={()=>{this.buttonClick()}}>更新state</button>
</div>
)
}
}
export default App

image-20230327114712050

可以看到使用flushSync中的setState在微任务执行之前就已经运行,说明flushSync确实将setState更改为同步方式运行

React的SCU优化

React更新机制

React渲染流程:

image-20230327115317127

React更新流程:

image-20230327120054249

React在props或state发生改变时,会调用React的render方法,会创建一颗新树,React会基于新树与旧树的不同去判断如何有效的更新UI

  • 如果两棵树进行完全比较更新,算法的时间复杂度为O(n^3),n为树中元素的数量,开销太大,React的更新性能会变得低效

React对diff算法的优化:

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

keys的优化:

  • 在最后位置插入数据,有无key意义不大
  • 在前面插入数据,没有key,所有的li都需要更改,有key,后续的li只需要移动

key的注意事项:

  • key应该是唯一的
  • key不能使用随机数,每次render的随机数不同
  • 不能使用index作为key,index在操作后也可能会改变,要使用一个不会随render改变的值

shouldComponentUpdate优化(即SCU优化)

image-20230327121337396

  • 在上图中,如果App中值发生了改变,那么App的render函数会重新执行,而在App的执行过程中,会执行其所有子组件的render函数,都进行diff算法,性能很低
  • 子组件调用render函数应该有一个前提——依赖的数据state/props发生改变,此时才调用render函数

shouldComponentUpdate函数

  • 参数:nextProps(修改后的pros)、nextState(修改之后的state)
  • 返回值:true(调用render方法)、false(不调用render方法)、默认返回true(只要state改变就会调用render方法)

在值发生变化时,就可以在shouldComponentUpdate监听自己的state和props是否发生变化从而觉得是否进行render

类组件PureComponent

继承自PureComponent自动实现了SCU优化,不需要自己去编写shouldComponentUpdate函数

如果使用Component,只要调用setState就会执行render函数,而PureComponent会帮我们实现比较setState后state是否改变,如果改变才执行render

不可变数据的力量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from "react";
import {flushSync} from "react-dom"
//编写组件
class App extends React.Component{
constructor(){
super()
this.state ={
books:[
{name:"aaa",count:1},
{name:"bbb",count:1},
{name:"ccc",count:1},
{name:"ddd",count:1}
]
}
}
addNewBook(){
const newBook = {name:"eee",count:1}
this.setState({

})
}

render(){
console.log("render")
const { books } = this.state
return (
<div>
<ul>
{books.map((item,index)=>{
return(
<li key={index}>
<span>name:{item.name}-count:{item.count}</span>
<button>+1</button>
</li>
)
})}
</ul>
<button onClick={e=>this.addNewBook()}>添加新书</button>
</div>
)
}
}
export default App

上述在setState中没进行任何操作,但render依然会执行,而改成PureComponent时不会执行

PureComponent中render执行要求:

  • PureComponent在比较时底层会使用shallowEqual方法,shallowEqual是一种浅层比较,如果比较的两个对象是同一个,则不会调用render,即使this.state发生了改变,也不会调用render
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
constructor(){
super()
this.state ={
books:[
{name:"aaa",count:1},
{name:"bbb",count:1},
{name:"ccc",count:1},
{name:"ddd",count:1}
]
}
}
addNewBook(){
const newBook = {name:"eee",count:1}
this.state.books.push(newBook)
const books = this.state.books
this.setState({
books:books
})
}
  • 如果要想在这种情况下调用render,可以利用books创建一个新的对象,用这个对象去更改state,在这种情况下,即使未更改this.state的,也会调用render
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
constructor(){
super()
this.state ={
books:[
{name:"aaa",count:1},
{name:"bbb",count:1},
{name:"ccc",count:1},
{name:"ddd",count:1}
]
}
}
addNewBook(){
const books = [...this.state.books]
this.setState({
books:books
})
}

函数组件memo

函数由于不能继承于PureComponent,可以使用memo替代,即用memo包裹原来的函数组件

image-20230327131630892

获取原生DOM或获取组件

在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:

  • 管理焦点,文本选择或媒体播放;
  • 触发强制动画;
  • 集成第三方 DOM 库;
  • 我们可以通过refs获取DOM;

通过refs获取DOM的方式:

  • 传入字符串:使用时通过 this.refs.传入的字符串 格式获取对应的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
addNewBook(){
console.log(this.refs.button)
}

render(){
return (
<div>
<button onClick={e=>this.addNewBook()} ref="button">查看DOM</button>
</div>
)
}
}
export default App
  • 传入一个对象:通过React.createRef()方式创建一个对象,使用使用对象的current属性来访问DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.titleRef = React.createRef()
}
addNewBook(){
console.log(this.titleRef.current)

}

render(){
return (
<div>
<button onClick={e=>this.addNewBook()} ref={this.titleRef}>查看DOM</button>
</div>
)
}
}
export default App
  • 传入一个函数:该函数会在DOM被挂载时回调,回调函数会传入一个元素对象,我们可以访问并保存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.titleRef = React.createRef()
}
render(){
return (
<div>
<button ref={(e)=>{console.log(e)}}>查看DOM</button>
</div>
)
}
}
export default App

通过refs获取组件的方式:

针对类组件:

  • 上面的ref同样可以加在子组件上,获取得到子组件的实例,就可以在父组件中获取子组件对子组件的函数、变量进行操作

针对函数组件:

  • 函数组件由于没有实例,是不可能获取到它的
  • 但是在某些情况,想获取得到函数实例中的某个DOM元素,可以使用foreardRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from "react";
import { forwardRef } from "react";
//编写组件

const HelloWorld = forwardRef(function(props, ref){
return (
<div>
<h1 ref={ref}>Hello World</h1>
</div>
)
})
class App extends React.PureComponent{
constructor(){
super()
this.HelloWorldRef = React.createRef()
}
render(){
return (
<div>
<HelloWorld ref = {this.HelloWorldRef}/>
<button onClick={()=>{console.log(this.HelloWorldRef.current)}}>查看DOM</button>
</div>
)
}
}
export default App

受控组件与非受控组件

  • HTML中,表单元素(input、textarea、select)通常会自己维护state并根据用户的输入进行更新
  • React中,可变状态通常保存在组件的state属性中,并且只能通过setState来更新
  • 在JSX中,如果给表单元素设置了value属性,显示this.state.value的值,那么表单元素就不会自己维护state,其state来源于React的state,因此当设置了value属性后,input元素输入后输入框里的东西不会变化,相当于未输入
  • 此时只能给input元素添加onChange(e)监听,从而在监听函数中使用输入的值在setState中更改value的值,使得页面刷新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from "react";
import { forwardRef } from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
username:"oww"
}
}
inputChange(e){
this.setState({username:e.target.value})
}
render(){
const {username} = this.state
return (
<div>
<input type="text" name="username" value={username} onChange={(e)=>{this.inputChange(e)}}/>
</div>
)
}
}
export default App

计算属性名

当对多个表单元素变成受控元素时,其更新需要多个监听,此时可用计算属性名将这些监听抽取到一个函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from "react";
import { forwardRef } from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
username:"oww",
password:""
}
}
inputChange(e){
this.setState({[e.target.name]:e.target.value})
}
render(){
const {username,password} = this.state
return (
<div>
<input type="text" value={username} name="username" onChange={(e)=>{this.inputChange(e)}}/>
<input type="text" value={password} name="password" onChange={(e)=>{this.inputChange(e)}}/>
</div>
)
}
}
export default App

checkbox表单多选监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
hobbies:[
{id:"sing",text:"唱",checked:false},
{id:"jump",text:"跳",checked:false},
{id:"rap",text:"rap",checked:false}
]
}
}
inputChange(e,index){
const hobbies = [...this.state.hobbies]
hobbies[index].checked = e.target.checked
this.setState({
hobbies:hobbies
})
}
render(){
const {hobbies} = this.state
return (
<div>
<h2>爱好列表</h2>
{hobbies.map((item, index)=>{
return (
<label htmlFor={item.id} key={item.id}>
<input type="checkbox" id= {item.id} checked={item.checked} onChange={(e)=>this.inputChange(e,index)}/>{item.text}
</label>
)
})}
</div>
)
}
}
export default App

select多选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from "react";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
this.state ={
fruits:[
{id:"apple",text:"苹果"},
{id:"banana",text:"香蕉"},
{id:"orange",text:"橘子"}
],
checked:["apple"]
}
}
selectChange(e){
let checked = Array.from(e.target.selectedOptions).map(item=>item.value)
this.setState({
checked:checked
})
}
render(){
const {fruits,checked} = this.state
return (
<div>
<h2>爱好列表</h2>
<select multiple value={checked} onChange={e=>this.selectChange(e)}>
{fruits.map(item=>{
return <option value={item.id} key={item.id}>{item.text}</option>
})}
</select>
</div>
)
}
}
export default App

高阶组件

高阶函数:满足下列一个条件

  • 接受一个函数或多个函数作为输入
  • 输出一个函数

高阶组件(Higher-Order Components、HOC):

  • 高阶组件的参数为组件,返回值为新组件的函数
  • 高阶组件本身不是一个组件,而是一个函数

高阶组件不是React的API,而是基于React组合特性而形成的一种设计模式

高阶组件应用——props的增强

HOC代码:高阶函数包裹代码

例如本代码可以将用户信息封装传给子组件,不需要像上面的父向子传值,在子组件中写其他代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { PureComponent } from "react"

function enhanceProps(WrapperCpn, otherProps){
class UserComponent extends PureComponent{
constructor(){
super()
this.state = {
userinfo:{
username:"ouwenwu",
age:22
}
}
}
render(){
return <WrapperCpn {...this.state.userinfo} {...otherProps}/>
}
}
return UserComponent
}
export default enhanceProps

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ChildComponent from "./component/ChildComponent";
import enhanceProps from "./HOC/HOC";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
render(){
const EnhancePropsCpn = enhanceProps(ChildComponent,{color:"blue"})
return (
<div>
<EnhancePropsCpn/>
</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.props.color}}>username:{this.props.username}</h2>
</div>
)
}
}
export default ChildComponent

结果:

image-20230327211035646

高阶组件应用——Context的共享

HOC代码:HOC可以返回类组件也可以返回函数组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { PureComponent } from "react"
import Context1 from "../context/Context1"

function enhanceProps(WrapperCpn){
return props=>{
return (
<Context1.Consumer>
{
value=>{
return <WrapperCpn {...value} {...props}/>
}
}
</Context1.Consumer>
)
}
}
export default enhanceProps

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import ChildComponent from "./component/ChildComponent";
import Context1 from "./context/Context1";
import enhanceProps from "./HOC/HOC";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
render(){
const EnhancePropsCpn = enhanceProps(ChildComponent)
return (
<div>
<Context1.Provider value={{color:"red"}}>
<EnhancePropsCpn/>
</Context1.Provider>

</div>
)
}
}
export default App

子组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'
export class ChildComponent extends Component {
constructor(){
super()
}
render() {
return (
<div>
<h2 style={{color:this.props.color}}>username:</h2>
</div>
)
}
}
export default ChildComponent

结果:

image-20230327212419080

注:可以看出对Context不再需要在每一个子组件中写之前提到的获取Context的方法,只需要将所有需要该Context的子组件包裹在高阶组件之中

高阶组件应用——渲染判断定权

与上面的主要差距为,会对传入的参数进行判断来决定返回哪个子组件

image-20230327212646566

高阶组件应用——生命周期劫持

在高阶组件中劫持子组件的生命周期,在自己的生命周期中完成逻辑

image-20230327212743063

总结

高阶组件的主要作用是提高代码的复用性。提高组件之间的复用

缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将产生非常多的嵌套
  • HOC可以劫持props,在不遵从约定的情况下可能造成冲突