组件之间通信
类组件之间通信
搭建如图所示的结构
父类中的代码
render() {
return <div className="box">
<div className="header">
<h2>组件之间通信</h2>
<span>总人数:0</span>
</div>
<VoteMain></VoteMain>
<VoteFooter></VoteFooter>
</div>
}
主体代码
render() {
return <div className="main">
<h3>支持人数:</h3>
<h3>反对人数:</h3>
</div>
}
尾部代码
render() {
return <div className="footer">
<Button type="primary">支持</Button>
<Button type='dashed'>反对</Button>
</div>
}
父子组件之间通信,基于父组件将需要的属性值,方法传递给子组件中的props属性接收使用,在子组件中如果想修改父组件中的某些属性值,可以通过父组件暴露的方法去修改。如果是类组件,父组件还可以给子组件绑定ref
属性以此获取子组件的实例,这样子就父组件就可以直接使用子组件中的属性和方法了。
//父组件中创建状态,并设置修改状态的方法,同时将属性传递给组件接收使用
state = {
sup: 15,
opp: 0
}
change = (type) => {
if (type === '+') {
this.setState({
sup: this.state.sup + 1
})
return
}
this.setState({
opp: this.state.opp + 1
})
}
....
let { sup, opp } = this.state
<VoteMain sup={sup} opp={opp}></VoteMain>
<VoteFooter change={this.change}></VoteFooter>
// 在main组件中,接收父组件传递来的参数,计算出比率后,放置对应位置显示
let { sup, opp } = this.props
let total = sup + opp
let ratio
if (total !== 0 && sup !== 0) {
ratio = ((sup / total) * 100).toFixed(2)
}
// 尾部按钮区域,接收父组件传递来的方法,在组件中使用接收该方法和父组件中的方法使用的是用一个内存地址。
render() {
let { change } = this.props
return <div className="footer">
<Button type="primary" onClick={change.bind(null, '+')}>支持</Button>
<Button type='dashed' onClick={change.bind(null, '-')}>反对</Button>
</div>
}
在初次挂载的时候,父组件中会为状态和函数创建对应的内存地址。如果在执行jsx过程中遇见子组件,则优先会去执行子组件的挂载操作。在主体组件中用的数据是父组件中传递来的,该数据每次在父组件中更新的时候,相应的子组件中用到的地方也需要更新。但是父组件中创建的方法并不会在组件更新的时候再次创建,所以他每次使用的函数都是同一个内存地址,这就导致了每次父组件更新导致底部按钮组件也跟着更新。这会造成浪费性能的问题。所以将底部组件修改为继承PureComponent
即可,这样子对于每次传递的函数都不变的情况下直接不进行渲染操作,大大提高的性能。
函数组件之间通信
基础代码依旧不变,先对部分代码执行优化
// 父组件
let [sup, setSup] = useState(15)
let [opp, setOpp] = useState(0)
const change = useCallback((type) => {
if (type === '+') {
setSup(sup + 1)
return
}
setOpp(opp + 1)
}, [])
// 主体
let { sup, opp } = props
let ratio = useMemo(() => {
let ratio
let total = sup + opp
if (total !== 0 && sup !== 0) {
return ratio = ((sup / total) * 100).toFixed(2)
}
return 0
}, [sup, opp])
const VoteFooter = memo((props) => {
let { change } = props
return <div className="footer">
<Button type="primary" onClick={change.bind(null, '+')}>支持</Button>
<Button type='dashed' onClick={change.bind(null, '-')}>反对</Button>
</div>
})
但是这么做完会出现一个问题,就是视图无法正常更新了。这是因为错误的使用useCallback
导致的,因为在当前代码中,我们给useCallback
设置的依赖条件是组件挂载的时候执行一次,后续组件更新的时候就不会再执行了。这就导致了该函数只存在第一次创建的闭包中,只能使用该闭包中的数据。后续第一次更新的时候,设置状态更新的时候,可以正常更新一次,如果再次点击更新的时候,调用setSup(sup + 1)
或setOpp(opp + 1)
,useState
会监测到两次更新的状态是一个值就不会去执行更新操作,解决方法就是去掉useCallback
。
基于上下文实现组件之间通信(祖先与后代或兄弟组件)
上面的组件通信是基于父子组件之间,这种层级关系低的可以直接使用props传参,但是一旦层级关系跟多,祖先组件想给某一个后代组件传参,如果再次使用props,那么会间接传递多个组件。这种是极为麻烦的。
但是如果采用上下文通信,那么祖先组件只需要将状态和方法保存在上下文中,后代组件只需要从上下文中取出使用即可。修改的时候操作的是祖先组件中的状态从而驱动视图更新。然后祖先组件又将最新的状态放入上下文中,后代组件渲染的时候取出最新上下文中的数据渲染。
类组件
- 在src目录下创建一个
themeContext.js
文件保存上下文信息,同时创建如下代码,调用React.createContext(defalutValue)方法
能让你创建一个 context 以便组件能够提供和读取。defalutValue
当包裹需要读取上下文的组件树中没有匹配的上下文时,你可以用该值作上下文的。 倘若你没有任何有意义的默认值,可指定其为null 。 该默认值是用于作为”最后的手段“的备选值。 它是静态的,永远不会随时间改变。null
import React from "react";
export const themeContext = React.createContext()
- 其次在祖先组件中Vote组件引入创建好的上下文,重点是:当前祖先组件的后代组件如果需要使用祖先组件中的信息,就需要再祖先组件中使用
<themeContext .Provider value={} >..</themeContext .Provide>
包裹住视图,其作用是:让你为被它包裹的组件提供上下文的值。同时如果想将组件的状态和方法保存到上下文中,就需要使用value
属性保存。
import { themeContext } from "../ThemeContext";
...
render() {
let { sup, opp } = this.state
return <themeContext.Provider value={{
sup,
opp,
change: this.change
}}>
.......
<VoteMain ></VoteMain> //未传递参数
<VoteFooter ></VoteFooter>
......
</themeContext.Provider>
}
- 如果想在后代组件中使用上下文中保存的内容有如下几种方法
*:在主体组件中如果想使用上下文文中的内容,首先需要先引入,然后给当前类设置静态属性contextType
,告诉React可以处理上下文中的内容,将所有上下文中的内容全部赋值到当前组件实例的context
属性上(默认继承存在的属性)
*:在底部组件中,采用另一种方式获取上下文中的内容,通过<themeContext.Consumer>...</themeContext.Consumer>
标签获取,其作用是它用于读取上下文的值。同时在该标签内部需要写一个函数用于返回视图,且该函数会默认接收一个参数,即上下文。 这种方式就不需要设置静态属性操作。
// VoteMain组件中
import { themeContext } from "../ThemeContext";
。。。。
static contextType = themeContext
render() {
console.log(this.context);
let { sup, opp } = this.context
return <div className="main">
<h3>支持人数:{sup}</h3>
<h3>反对人数:{opp}</h3>
</div>
}
// VoteFooter组件中
import { themeContext } from "../ThemeContext";
。。。。
render() {
return <themeContext.Consumer>
{theme => {
console.log(theme);
return <div className="footer">
<Button type="primary" onClick={theme.change.bind(null, '+')}>支持</Button>
<Button type='dashed' onClick={theme.change.bind(null, '-')}>反对</Button>
</div>
}}
</themeContext.Consumer>
}
函数组件
在函数组件使用上下文的步骤
- 首先依旧先创建上下文对象并导出
import React from "react";
export const themeContext = React.createContext()
- 在祖先组件中也必须先引入上下文对象,同时使用
<themeContext.Provider value={}>
标签提供上下文中的值
return <themeContext.Provider value={{ sup, opp, change }}>
......
<VoteMain ></VoteMain>
<VoteFooter ></VoteFooter>
.....
</themeContext.Provider>
- 在主体组件中引入上下文对象,在上下文组件中无法传递context属性,所以不能使用挂载静态属性的方式。只能采用
themeContext.Consumer
标签的方式获取上下文中的内容
const VoteMain = () => {
return <themeContext.Consumer>
{context => {
return <div className="main">
<h3>支持人数:{context.sup}</h3>
<h3>反对人数:{context.opp}</h3>
</div>
}}
</themeContext.Consumer>
}
- 在底部组件中使用另一种方法获取上下文中的内容,函数组件提供的
useContext
函数获取上下文中对象,
const VoteFooter = () => {
let { change } = useContext(themeContext) //返回对象{sup:15,opp:0,change:函数}
return <div className="footer">
<Button type="primary" onClick={change.bind(null, '+')}>支持</Button>
<Button type='dashed' onClick={change.bind(null, '-')}>反对</Button>
</div>
}
私有样式处理
如下两段代码,每一个容器都叫box,同时分别在scss文件中设置box样式引入,那么就会存在一个样式冲突问题。在vue中解决样式冲突可以使用scoped
关键字处理,但是在react中无法实现,需要我们手动处理。
export const Demo1 = () => {
return <div className="box-container">
<span>我是Demo1组件</span>
<Demo1_1></Demo1_1>
<Demo1_1_1></Demo1_1_1>
</div>
}
export const Demo1_1 = () => {
return <div className="box"> //设置绿色
<span>我是Demo1_1组件</span>
</div>
}
export const Demo1_1_1 = () => {
return <div className="box"> //设置红色
<span>我是Demo1_1_1组件</span>
</div>
}
类名冲突造成了后来的层叠之前的,因为这里使用的是分别创建scss文件建立样式,最后不经过处理直接汇总到页面中,每一个scss文件都单独编译成一个style结构,如果后面的style中存在重复样式则会层叠
内联样式
内联样式是通过给一个标签设置style
属性,这样子是可以保证样式私有化的。这是最简单的方式。
const spanColor = {
color: "skyblue"
}
return <div style={{ backgroundColor: 'green' }}>
<span style={spanColor}>我是Demo1_1组件</span>
</div>
但是内联样式也存在缺点:样式在之间的复用性太差,可维护性差,并且一个容器中的所有span都是一个样式,那么需要手动添加全部。如果将样式提取为一个对象格式,那么相应的代码提示也会消失。最重要的一点,内联样式无法设置伪元素:如after,hover等。
css样式表
这里在less或scss文件中编写的类名需要遵循一定的规范。保证每一个jsx视图最外围的容器具有唯一的类名。这样子就可以规避一些样式冲突的情况。像之前我们给最外围容器起名字都是box,接下来我们需要按规范命名。
import '../style/Demo1_1.scss'
export const Demo1_1 = () => {
return <div className='Demo1_1-box' > //外围容器命名唯一id
<span >我是Demo1_1组件</span>
<div className="Demo1_1-box-header">
<span>我是头部</span>
</div>
</div>
}
.Demo1_1-box {
background-color: green;
span {
font-size: 30px;
}
.Demo1_1-box-header {
span {
font-size: 20px;
}
}
}
import '../style/Demo1_1_1.scss'
export const Demo1_1_1 = () => {
return <div className="Demo1_1_1-box">
<span>我是Demo1_1_1组件</span>
</div>
}
.Demo1_1_1-box {
background-color: red
}
- 直接在命名类名的时候就规避一些已知的问题,可以大大提高效率,且将样式写在专有的样式文件中,实现了样式和结构相分离,而非内联样式那种合并在一起。
- 这种写法可以使用css的任何语法,包括伪元素,媒体查询等
- 可以使用缓存,对样式文件进行强缓存和协商缓存
- 在专门的样式文件中编写代码具有提示
- 但是也可能会产生冲突,所有的选择器都具有相同的全局作用域,很容易造成样式冲突。
- 性能低,不断嵌套很容易让一个类的前缀过长
- 动态化添加某个样式困难,因为是完全独立的样式文件,无法便捷的受js控制。
Css Modules
CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效;产生局部作用域的唯一方法,就是使用一个独一无二的名字;这就是Css Modules的做法!
如果想使用 css modules,那么命名必须按照规范:文件名.module.css
,同时css文件不支持嵌套写法
创建如图所示的文件
导入该文件必须使用一个变量名接收,查看接收的值是什么。将自己在文件中定义的类名进行了统一处理,其形式是一个对象,对象中的键对应我们文件中的类名,而值对应编译后的类名,该编译后的类名就是最终页面中使用的值。 至于处理的格式配置中已经定义好了。(值的基本处理原则:文件名_类名_哈希值)
import style from '@/style//Demo1_1.module.css'
console.log(style);
return <div className={style['Demo1_1-box']} >
<span >我是Demo1_1组件</span>
<div className={style['Demo1_1-box-header']}>
<span>我是头部</span>
</div>
</div>
如果我们设置一个hover样式使用如下
.spanHover:hover {
color: aqua
}
.spanHoverBorder:hover {
border: 1px solid #333;
}
那么在获取的对象中格式如下
<span className={style.spanHover}>我是头部</span>
如果想同时使用多个被css modules管理类名。那么需要使用模版字符串的格式拼接。这么做的理由是保持转为class的时候,多个类名之间无逗号
<span className={`${style.spanHover} ${style.spanHoverBorder}`}>我是头部</span>
如果我们希望在css modules文件中某一个类名依旧保持原样,不需要经过处理,那么使用:global()
处理。这样子在导入的style对象中就不存在该类名的转换。则该类名会变成全局的
:global(.clearfix) {
clear: both;
}
最后,我们还可以在css modules语法中继承某一个类的样式,格式如下。在接收的style对象中输出如图,发现其不仅有more的样式名还有另一个类的样式名
.more {
/* 继承某一个类 */
composes: Demo1_1-box;
border: 2px solid red;
}
<div className={style.more}>更多</div>
React Jss
Jss是一个CSS创作工具,它允许我们使用JavsScript以生命式、无冲突和可重用的方式来描述样式。JSS是一种新的样式策略。React-JSS是一个框架集成,可以在React应用程序中使用JSS。它是一个单独的包,所以不需要安装JSS核心,只需要React-JSS包即可。React-JSS使用新的 Hooks API将JSS与React 结合使用。
首先需要安装该插件:npm i react-jss
引入插件库中的方法:import { createUseStyles } from 'react-jss'
,同时调用该方法传入配置对象,经过输出发现其本质是自定义hooks函数。
console.log(createUseStyles({}));
在createUseStyles({})
函数中传入的配置中的参数均是需要被使用的类选择器等。每一个选择器都是一个配置对象,包含了各种样式信息。需要注意这里想表达嵌套关系使用&符号表达
let useStyles = createUseStyles({
box: {
width: '200px',
height: '200px'
},
title: {
color: 'lightpink',
'&:hover': {
color: 'lightgrey'
}
},
ulList: {
'& li': {
listStyle: "none"
}
}
})
输出返回的自定义hooks函数调用结果,发现其实和cssmodules差不多,都是给定义的选择器创建了一个唯一标识。
console.log(useStyles());
使用结构操作取出类名,然后应用到标签上使用
let { box, title, ulList } = useStyles()
return <div className={box}>
<div className={title}>
<h1>我是Demo2标题</h1>
</div>
<ul className={ulList}>
<li>首页</li>
<li>个人</li>
<li>订单</li>
</ul>
</div>
最终所有的样式都是以如下格式存放
这么写的好处是我们可以动态的传递样式信息使用,如下,在返回的自定义hooks函数中可以调用传入配置参数,所有的参数最终都可以被createUseStyles()
函数内部获取到。只不过所有的属性值都需要写成函数式,如下代码中props即useStyle方法中传递的参数
let useStyles = createUseStyles({
.....
title: {
color: props => props.color,
'&:hover': {
color: 'lightgrey'
}
},
....
footer: props => {
return {
'&.active': {
color: props.color,
fontSize: props.size + 'px'
}
}
}
})
let { box, title, ulList, footer } = useStyles({
size: 30,
color: "lightpink"
})
上面使用createUseStyles()
是在函数组件中,那么如果想在类组件中使用该如何处理?
高阶组件
高阶组件理由js中的闭包,颗粒化实现组件代理
因为类组件中不能直接使用hooks函数,因此我们需要使用代理组件(高阶组件),将样式通过属性传递给类组件使用。
创建一个Child.jsx组件,并编写如下代码
import React from 'react'
import { createUseStyles } from 'react-jss';
let useStyles = createUseStyles({
box: {
width: '200px',
height: '200px',
border: '1px solid #333',
backgroundColor: "skyblue"
}
})
class Child extends React.Component {
render() {
return <div className={this.props.box}>
我是child组件
</div>
}
}
// 调用该函数,传入需要代理的类组件
// 外层函数负责获取需要代理的类组件,然后在内部返回一个函数
const ProxyFun = (Component) => {
return function HOC(props) { //该高阶组件函数名必须符合组件标签规范
// 最终导出使用的是返回的HOC函数部分,在该函数内部将用到的样式参数传递给类组件使用
let { box } = useStyles()
return <Component {...props} box={box}></Component>
}
}
export default ProxyFun(Child) //导出的时候将函数调用并传入参数组件
在父组件函数组件中引入
import ProxyComponent from './Child'
<ProxyComponent></ProxyComponent>
代理组件部分代码也可以直接如下写法,但是不规范
const ProxyFun = (props) => {
let { box } = useStyles()
return <Child {...props} box={box}></Child>
}
export default ProxyFun
styled component
return <header>
<div>
<h1>我是Demo2标题</h1>
</div>
<ul >
<li>首页</li>
<li>个人</li>
<li>订单</li>
</ul>
</header>
首先需要安装:npm i styled-components
,同时在vscode中安装提示的插件:vscode-styled-components
首先需要创建一个与jsx对应的js文件单独编写样式信息,然后引入该插件库使用:import styled from 'styled-components'
基本格式:一个包装器和一个HTML元素,使用ES6模版字符串附加样式,styled.div``
。同时将返回的结果赋值给一个变量导出使用。
如下是一个简单的实例。创建了一个样式并导出使用。最终header
会被创建为真实的html元素代替导出使用的组件标签。
export const HeaderComponent = styled.header`
h1 {
color:skyblue;
font-size: 30px;
}
`
输出查看是什么内容,可以发现是一个被再次封装了的虚拟DOM,该虚拟DOM最终会根据type的值创建元素。
import { HeaderComponent } from "../styleJS/Demo3"
console.log(HeaderComponent);
使用方式如下,最终这里的HeaderComponent
会被替换为指定的header
标签。并且在页面元素中也是通过创建唯一类名指定样式
<HeaderComponent>
<div>
<h1>我是Demo2标题</h1>
</div>
<ul >
<li>首页</li>
<li>个人</li>
<li>订单</li>
</ul>
</HeaderComponent>
同时该写法还支持同一个文件中导出多个样式使用,比如我们想设置上面的ul中li的统一样式,也可以通过变量的方法设置全局默认配置,比如所以li的字大小均为某个像素
const fontSize = 18 //定义字体大小
export const UlComponent = styled.ul` //创建ul标签
li {
list-style: none;
margin: 10px;
color:pink;
font-size: ${fontSize}px;
}
`
import { HeaderComponent, UlComponent } from "../styleJS/Demo3"
return <HeaderComponent>
<div>
<h1>我是Demo2标题</h1>
</div>
<UlComponent >
<li>首页</li>
<li>个人</li>
<li>订单</li>
</UlComponent>
</HeaderComponent>
由于使用的时候当做组件使用,因此我们可以传递参数使用,既然存在传递参数,那么就可以存在默认值的情况。如下代码中在使用定义的UlComponent
组件的时候传递参数过去使用,需要写成小写,否则报错。那么在样式文件中如何接收使用
<UlComponent hovercolor='#ffe68f'> //未传递size大小,但是却用到了,因此需要设置默认值
<li>首页</li>
<li>个人</li>
<li>订单</li>
</UlComponent>
设置默认值是使用:styled.ul.attrs(函数返回默认值)``
export const UlComponent = styled.ul.attrs(props => {
return {
size: props.size || 25 //设置默认值,如果传递了props.size属性就以该值为准
}
})`
li {
......
&:hover {
color:${props => {
console.log(props);
return props.hovercolor
}};
font-size:${props => props.size}px;
}
}
`
所以用到函数返回参数的props都是如下值
Redux
Redux是React框架中实现公共状态管理的方案,类似vue的vuex或pinia。任何类型的组件之间可以通过Redux实现通信。
React框架中,实现公共状态管理的方案如下
- redux+react-redux
- dva [redux-saga] 或 umi
- MobX
Redux是一个小型js库,可以和如下几种包一起使用
- React-redux:React-Redux是我们的官方库,它让React组件与Redux有了交互,可以从
store
读取一些state
,可以通过dispatch actions
来更新store
! - Redux Toolkit:Redux Toolkit是我们推荐的编写Redux逻辑的方法。它包含我们认为对于构建Redux 应用程序必不可少的包和函数。Redut Toolkit构建在我们建议的最佳实践中,简化了大多数Redux 任务、防止了常见错误,并使编写Redux应用程序变得更加容易。
- Redux DevTools 扩展:Redux DevTools Extension可以显示Redux存储中状态随时间变化的历史记录,这允许您有效地调试应用程序。
下图是redux的基本原理,需要注意的redux如下图所示区域分为两部分:公共状态区域和事件池区域。公共状态区域的值不能直接修改,必须通过指定的方式去修改,一旦公共状态值发生改变,那么就会通知事件池中的事件函数执行视图更新操作。
在如下三个组件中,都需要用到redux,如果都引入的话很麻烦,所以直接在Vote组件引入redux,然后将redux放入上下文中,子组件从上下文中获取即可。
首先需要安装如下插件:npm i @reduxjs/toolkit redux redux-logger redux-promise redux-thunk react-redux
然后在src目录下单独创建一个store文件存储公共状态
import { createStore } from 'redux'
// 初始公共状态,只有调用指定方法才能修改
let initialState = {
sup: 15,
opp: 0
}
const reducer = (state = initialState, action) => {
// 拷贝公共容器中的state值,目的不直接影响公共容器中的内容
state = { ...state } //这里是浅拷贝
// state存储store容器中公共状态,初始值若无可以赋值initialState
// action基于每次派发dispatch,传递过来的行为对象(要求必须具备type属性,存放派发的行为标识)
// 基于type派发的唯一标识进行修改容器中公共状态对应的值
switch (action.type) {
case 'Vote_sup':
state.sup += 1 //如果不进行拷贝操作,这里操作的实际是store公共状态去的state值,那么在返回就无意义
break;
case 'Vote_opp':
state.opp += 1
break;
}
// 返回的状态会更新公共容器中的状态值
return state
}
// 调用创建store的方法创建一个公共容器,必须传入一个reducer函数
export const store = createStore(reducer) //调用返回{getState:..;subscribe:..;dispatch:...}
/*
store.dispatch({
type: 'Vote_sup',
})
*/
红色线是redux默认执行的初始操作,目的是给公共状态赋值,蓝色线是实际我们操作的线
之后store公共状态信息需要被使用的组件获取应用,因此我们可以将store基于上下文放在入口文件index.jsx身上,这样子所以的后代节点都可以使用公共状态中的内容了
首先创建一个上下文对象导出,在入口文件中引入使用
import { createContext } from "react";
export const ThemeContext = createContext()
在index.jsx入口文件中引入并使用
// 设置上下文
import { ThemeContext } from './ThemeContext';
// 引入store公共状态
import { store } from '@/store/index.js'
root.render(
<ConfigProvider locale={ZHCN}>
<ThemeContext.Provider value={store}>
<Vote></Vote>
</ThemeContext.Provider>
</ConfigProvider>
);
首先在vote函数组件中采样hooks函数使用上下文中的内容并输出查看
import { ThemeContext } from "../ThemeContext"
import { useContext } from "react"
export const Vote = () => {
let res = useContext(ThemeContext)
console.log(res);
...
}
在votefootrer类组件中使用上下文中的store如下
import { ThemeContext } from "../ThemeContext"
export class VoteFooter extends React.Component {
static contextType = ThemeContext
render() {
console.log(this.context);
....
}
}
已经为每个组件中导入上下文中的store使用,那么可以在vote和votemain组件中引入公共状态中的数据使用,调用getState()
方法即可获取公共状态中store的值。同时每个组件如果需要用到最新数据,则到初次挂载的时候都需要将能更新组件的方法放入公共状态中的事件池中。调用subscribe()
方法能让更新视图的方法存入事件池中,且该方法每次执行的时候都会返回一个unsubscirbe()
方法,用于将移除每次存入事件池中对应方法。
// vote组件
let store = useContext(ThemeContext)
let { sup, opp } = store.getState() //使用store公共状态中的值
let [num, setNum] = useState(0) //为了更新视图而创建,后期有替代方案
// 将能更新视图的方法存入事件池中
const update = () => {
setNum(num + 1)
}
useEffect(() => {
// 让更新视图的方法存入事件池
store.subscribe(update)
}, [])
// votemain组件
static contextType = ThemeContext
render() {
let { sup, opp } = this.context.getState()
...
}
componentDidMount() {
// 传入事件池中让类组件更新的方法
//其实可以这里不添加,因为父组件更新的时候子组件也会跟着更新,但是为了演示出问题就添加该语句
this.context.subscribe(() => this.forceUpdate())
}
在底部组件中不需要用到数据,所以不需要创建对应的方法放入事件池中,但是底部组件是用来派发dispatch()
去执行reducer操作的,每次派发的时候必须传入type值。该字段和store文件中定义的要求字段需要保持一致。该方法的返回值即传入的参数,也就是传入{type:'abc},那么返回也是这个对象
// votefooter
static contextType = ThemeContext //用于获取上下文中存储store中的dispatc函数来派发
render() {
return <VoteFooterBoxDiv>
<Button type="primary" onClick={() => this.context.dispatch({ type: 'Vote_sup' })}>支持</Button>
<Button type="primary" danger onClick={() => this.context.dispatch({ type: 'Vote_opp' })}>反对</Button>
</VoteFooterBoxDiv>
}
这个时候发现页面中出现了问题,vote组件中的数据没有正常更新了
这是因为我们vote组件中使用如下代码,useEffect
函数其依赖为[]
代表组件初次挂载执行一次,后续更新不再执行,在subscribe
方法中传入事件池中的update
更新方法,但是该函数使用的闭包作用域为初次更新创建的闭包。这就导致了第一次更新的时候,num为0,更新0+1操作的时候,视图执行一次更新,这个时候页面从store中取出最新值为16,但是第二次更新的时候num还是0,0+1操作的时候两次更新值一样,所以不执行更新操作了,依旧是16。但是在votemain组件中,虽然为vote的子组件,但是在这个组件中将forceUpdate()
强制更新视图方法存入了事件池,所以中间内容会更新。
let [num, setNum] = useState(0)
// 将能更新视图的方法存入事件池中
const update = () => {
setNum(num + 1)
}
useEffect(() => {
// 让更新视图的方法存入事件池
let unsubscribe = store.subscribe(update)
}, [])
因此,我们解决的方法就是将依赖条件修改为[num]
,这样每次都会将当前作用域中的update方法存入事件池。这个时候即使votemain组件中没有存入强制更新方法,组件也会跟着更新。
useEffect(() => {
// 让更新视图的方法存入事件池
let unsubscribe = store.subscribe(update)
return () => {
// 每次存入事件池的时候先将上次事件池中更新的方法移除
unsubscribe()
}
}, [num])
redux源码解析
经过测试替换redux中的createStore页面可以正常更新
import _ from '@/utils.js'
export const createStore = (reducer) => {
// 首先传入的reducer必须是一个函数
if (typeof reducer !== 'function') throw new Error('reducer必须是一个函数')
// 创建公共状态,初始值为undefined
let state;
// 创建一个事件池
let listeners = []
// 返回公共状态
const getState = () => {
return state
}
// 往事件池中放入方法
const subscribe = (listener) => {
if (typeof listener !== 'function') throw new Error('listener必须是一个函数')
if (!listeners.includes(listener)) {
// 不包含相同方法的情况下插入该方法
listeners.push(listener)
}
// 返回一个删除对应事件池中方法的函数
return function unsubscribe() {
let index = listeners.indexOf(listener) //这里不需要对index进行负值判断,因为一定是插入一个方法后才会返回对应的删除方法
listeners.splice(index, 1) //数组splice方法删除或替换,影响原数组
}
}
// 通过reducer执行 dispatch({type:....})
const dispatch = (action) => {
// 首先验证action为纯对象,即{ },非数组,函数等
if (!_.isPlainObject(action)) throw new Error('action必须是纯对象')
// 验证为对象后必须验证是否有type字段
if (typeof action.type === 'undefined') throw new Error('action必须包含type字段')
// 支持reducer执行,调用会返回修改后的状态值
state = reducer(state, action) //当state为undefined的时候在reducer中会默认使用默认值
// 执行事件池中的函数
listeners.forEach(listener => listener())
// dispatch函数执行完毕会返回传入的参数
return action
}
// 初始情况,redux内部会执行一次dispatch,目的是初始化state公共状态的值
// 但是需要保证{type:..}type字段是唯一不重复的,不能和reducer中的匹配
// 第一种方法,利用Symbol类型的特征,唯一性
// dispatch({
// type: Symbol()
// })
// 手写方法实现,这是redux源码中的方法,toString是转换为对应进制数
var randomString = function randomString() {
return Math.random().toString(36).substring(7).split('').join('.');
};
dispatch({
type: "@@redux/INIT" + randomString()
})
return {
getState,
subscribe,
dispatch
}
}
redux 工程化
在一个完整的项目中,每个模块都具备各自的状态,如果不对reducer进行拆分处理,那么最终一个reducer会管理多个模块的状态,这里容易造成一个reducer函数的代码成百上千,不容易维护。因此我们需要进行reducer的工程化开发。
拆分reducer
按照模块将reducer进行单独管理,每一个模块对应一个reducer。最后会将所有的reducer合并为一个总的reducer使用。
在上面投票模块的基础上再添加一个person模块。首先需要为每一个模块创建各自的reducer,因此可以在src目录下建立一个管理reducer的文件。
import _ from '@/utils.js'
let initialValue = {
sup: 15,
opp: 0,
num: 9999
}
export const voteReducer = (state = initialValue, action) => {
state = _.clone(true, state) //深拷贝
switch (action.type) {
case 'Vote_sup':
state.sup += 1;
break;
case 'Vote_opp':
state.opp += 1;
break;
}
return state
}
import _ from '@/utils.js'
let initialValue = {
name: 'zq',
age: 18,
num: 1000
}
export const personReducer = (state = initialValue, action) => {
state = _.clone(true, state)
switch (action.type) {
case 'setName':
state.name = action.name;
break
}
return state
}
// store.dispatch({
// type: "setName",
// name: "哈哈哈哈"
// })
建立完成各自reducer后,就可以在index,js文件中实现模块合并,调用redux提供的:combineReducers({})
方法,在配置对象中设置每一个模块的专属名字和其对应的reducer,最终会汇总在总的reducer的状态管理中保存。如图
// 总的reducer,用于合并
import { voteReducer } from "./voteReducer";
import { personReducer } from "./personReducer";
import { combineReducers } from "redux";
export const reducer = combineReducers({
vote: voteReducer,
person: personReducer
})
最终公共状态会合并两个模块的各自状态,因此我们在使用getState
方法获取状态的时候需要注意区分模块,这是需要修改的,其他地方则不动,不需要修改,包裹dispatch
方法的派发。
执行getState
方法获取的状态如下
在vote相关的组件中获取对应的状态信息如下
let { sup, opp } = store.getState().vote
combineReducers源码分析
// 合并模块reducer的函数,其中传入的参数是一个对象,内部如下,
// 每一个属性都是一个小的reducer函数
// {
// vote: voteReducer,
// person: personReducer
// }
export const combineReducers = (reducers) => {
// 获取对象中的属性
let reducerKeys = Reflect.ownKeys(reducers) //['vote', 'person']
// 最终返回一个合并的reducer,最终dispatch触发的是这里返回的reducer,所以基本格式还是传入状态和标识
// state就是最终合并后的公共状态
return function reducer(state = {}, action) {
state = { ...state } //推荐深拷贝
// 执行所有模块的reducer函数
reducerKeys.forEach(key => {
// key 为 vote,person
let reducer = reducers[key] //根据健名取值,这里值是函数
// 执行各自模块reducer的时候,只会修改各种的状态
// state[key] 设置属性,初始情况无该属性所以会进行添加,且当前无值所以进入各自reducer的时候先进行了初始化,并且对所有模块的action.type进行匹配
// 循环完毕后,state上会挂载vote和person两个属性和其对应的值 {vote:{sup:0,opp:0},person:{name:''}}
state[key] = reducer(state[key], action)
})
// 因为总的reducer也会返回一个值修改公共的状态,所以不推荐在内部就直接修改
return state //该值会修改公共区域的值
}
}
// combineReducers({
// vote: voteReducer,
// person: personReducer
// })
dispatch派发行为标识–宏管理
在上面的代码中,并没有对dispatch进行处理。因此会在使用dispatch
方法传递标识的时候,会依次去两个reducer模块中寻找符合条件的标识,并执行对应的逻辑。但是在实际开发中,还会出现一个问题,就是多个模块中出现了同一个标识,那么这个时候再次使用dispatch派发的时候,就会将所有的模块中符合条件的全部触发一遍。但是实际上,我们只想触发对应模块中的标识,那么该如何处理。因此出现了宏管理,让所有派发的行为表示具有唯一性
在store文件下创建一个actionType.js
文件用于管理派发标识
// 统一管理派发标识
// 保证变量名和值相同
// 如果同一个文件出现重复命名也会报错
// 命名规范以模块名起头
export const VOTE_SUP = 'VOTE_SUP';
export const VOTE_OPP = 'VOTE_OPP';
export const PERSON_SETNAME = 'PERSON_SETNAME';
导入使用如下
import * as TYPE from '../actionType'
switch (action.type) {
case TYPE.VOTE_SUP:
state.sup += 1;
break;
case TYPE.VOTE_OPP:
state.opp += 1;
break;
}
同时在点击按钮时修改派发的行为标识
import * as TYPE from '../store/actionType'
dispatch({ type: TYPE.VOTE_SUP })
actionCreator创建
actionCreator并不是某个具体的方法,当我们将dispatch中的派发任务再次提取封装,分模块管理,我们称这一步为actionCreator。这一步骤在react-redux的时候非常有用。
首先在store目录下创建一个action文件,目录结果如图,其作用类似reducer。其中index.js文件用于合并所有模块同时可以区分使用
import * as TYPE from '../actionType'
export const voteAction = {
sup() {
return {
type: TYPE.VOTE_SUP
}
},
opp() {
return {
type: TYPE.VOTE_OPP
}
}
}
import * as TYPE from '../actionType'
export const personAction = {
....
}
// index
import { voteAction } from "./voteAction";
import { personAction } from "./personAction";
// 合并两个action对象
export const action = {
vote: voteAction,
person: personAction
}
同时修改dispatch的调用
import { action } from '@/store/actions/index.js'
dispatch(action.vote.sup())
React-redux
使用React-redux可以帮助我们简化之前使用redux繁琐的步骤(redux核心操作还是需要自己实现),首先我们不需要自己创建上下文,react-store可以帮助自动创建一个上下文对象,我们只需要在使用的地方使用这个上下文并传入store即可。同时,在组件使用中使用的时候,我们也不需要手动使用getState获取store中的状态值了。我们只需要引入对应的函数,然后开箱即用。在组件中,如果想使用上下文中的store状态值,无需使用使用useContext
后再次getState
,直接可以基于使用react-redux提供的connext
函数处理。同时我们也不需要它把组件更新的方法放入事件池,react-redux内部已经处理了
- 使用react-redux提供的
<Provider store={store}>...</Provider>
组件标签快速创建,同时使用store
属性传入上下问对象中需要用到的数据。
import { Provider } from 'react-redux';
root.render(
<ConfigProvider locale={ZHCN}>
<Provider store={store}><Vote></Vote></Provider>
</ConfigProvider>
);
- 在用到store中数据的地方,使用
connext
方法获取。该API的基本格式如下,调用该函数,会返回一个函数,在返回的函数中传入需要处理的组件
mapStateToProps?: Function //可以获取所有模块的状态,在返回值中会将返回的内容传入组件的props中使用
mapDispatchToProps?: Function | Object // 可以获取store.dispathc()方法进行派发,返回的结果作为属性传递给组件的props
mergeProps?: Function
options?: Object
connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(MyComponent)
// vote组件修改如下
const Vote = (props) => {
let { sup, opp } = props
return ...
}
//state= { vote: { sup: 0, opp: 0 }, person: { ... } }
export default connect(state => state.vote)(Vote) //state参数就是最终公共状态中的值
// voteMain组件
render() {
let { sup, opp } = this.props
....
}
export default connect(state => state.vote)(VoteMain)
// voteFooter组件
render() {
let { sup, opp } = this.props
return <VoteFooterBoxDiv>
<Button type="primary" onClick={sup}>支持</Button>
<Button type="primary" danger onClick={opp}>反对</Button>
</VoteFooterBoxDiv>
}
export default connect(null, dispatch => { //dispatch就是派发的行为函数
return {
sup() {
dispatch(action.vote.sup())
},
opp() {
dispatch(action.vote.opp())
}
}
})(VoteFooter)
在使用dispatch
方法派发的时候,因为我们之前统一处理过action,因此在这里还可以继续使用省略写法,如下,这段代码本质也是转换为上面的完整写法。内部会经过bindActionCreators
方法进行处理转换。
export default connect(null, action.vote)(VoteFooter)
该函数最终底层调用是如下格式,可以输出查看,其返回值就上上面完整写法
console.log(bindActionCreators(action.vote, dispatch));
以下是全源码解析
< Provider >函数组件和connect方法源码解析
首先借助react-redux的时候,使用其中提供的Provider
函数组件完成上下文挂载。
import { createContext } from "react";
// react-redux会自己创建上下文
const ThemeContext = createContext();
/*
<Provider store={store}><Vote></Vote></Provider>
执行函数组件,将属性传递给函数组件
*/
export const Provider = (props) => {
let { store, children } = props
// 将组件传递的store挂载到上下文中,同时渲染传递来的子标签
return <ThemeContext.Provider value={store}>
{children}
</ThemeContext.Provider>
}
同时react-redux还提供connect
方法进行获取公共状态中的数据和实现派发的功能
基本的格式如下
// export default connect(state => state.vote, dispatch => {
// return {
// sup() {
// dispatch(action.vote.sup())
// }
// }
// })(Vote)
export function connect(mapStateToProps, mapDispatchToProps) {
// 调用该函数的时候会返回一个函数继续调用,且会传入一个组件
return function currying(Component) {
return function HOC(props) {
return <Component {...props} />
}
}
}
之后指定未传参数情况的默认值
/* 处理默认值情况, 如果只执行connect(null, null)(组件),会向组件只传递一个dispatch函数*/
if (!mapStateToProps) {
// 为null或undefined的情况
mapStateToProps = () => { //要求返回函数
return {} //空对象即可
}
}
if (!mapDispatchToProps) {
mapDispatchToProps = (dispatch) => {
return {
dispatch: dispatch //默认传递一个dispatch
}
}
}
主要代码全在HOC高阶组件中,这里需要注意,像hooks函数只能写在函数组件中,不能写在connect或currying函数内部测试调用
以下是完整代码
import { createContext, useContext, useEffect, useState } from "react";
import { bindActionCreators } from "redux";
// react-redux会自己创建上下文
const ThemeContext = createContext();
/*
<Provider store={store}><Vote></Vote></Provider>
执行函数组件,将属性传递给函数组件
*/
export const Provider = (props) => {
let { store, children } = props
// 将组件传递的store挂载到上下文中,同时渲染传递来的子标签
return <ThemeContext.Provider value={store}>
{children}
</ThemeContext.Provider>
}
// export default connect(state => state.vote, dispatch => {
// return {
// sup() {
// dispatch(action.vote.sup())
// }
// }
// })(Vote)
export function connect(mapStateToProps, mapDispatchToProps) {
/* 处理默认值情况, 如果只执行connect(null, null)(组件),会向组件只传递一个dispatch函数*/
if (!mapStateToProps) {
// 为null或undefined的情况
mapStateToProps = () => {
return {} //空对象即可
}
}
if (!mapDispatchToProps) {
mapDispatchToProps = (dispatch) => {
return {
dispatch: dispatch
} //默认传递一个dispatch
}
}
// 调用该函数的时候会返回一个函数继续调用,且会传入一个组件
return function currying(Component) {
return function HOC(props) {
// 上下文中的对象取出,利用store可以取出state和默认执行事件池
let store = useContext(ThemeContext)
let { getState, dispatch, subscribe } = store
let [, focusUpdate] = useState(0) //处理更新方法,放入事件池
useEffect(() => {
let unsubscribe = subscribe(() => {
focusUpdate(+ new Date()) //保证每次更新的值不一样,一定可以渲染视图
})
return () => {
unsubscribe() //这种情况下,会在组件销毁的时候,执行该清理函数
}
}, [])
// 处理mapStateToProps和mapDispatchToProps
/* connect(state => state.vote)(Vote),第一个参数state => state.vote为箭头函数
不理解可以转换为:function(state){ return{state.vote} } 也就是说传入公共状态,
返回公共状态中某个模块的状态值*/
let state = getState() //获取store容器中的state传入
let nextState = useMemo(() => {
return mapStateToProps(state)
}, [state])
let dispatchProps = {}
/* mapDispatchToProps可能是一个函数:dispatch=>{ return {sup(){dispatch(action.vote.sup())} } }
如果不是函数,传入的是action.vote就需要借助bindActionCreateor方法创建返回上面的格式
*/
if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(dispatch)
} else {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
}
return <Component {...props} {...nextState} {...dispatchProps} />
}
}
}
redux中间件及处理机制
使用redux中间件可以在每次派发dispatch后,即将执行reducer函数前执行某些操作。
常见中间件
redux-logger
:每次派发,在控制台输出派发日志,方便调试redux。如派发前后的状态,行为等redux-thunk
和redux-promise
:实现异步派发,之前的代码中都是同步的redux-saga
applyMiddleware(…middleware)
调用redux提供的applyMiddleware()
方法设置不同的中间件。多个中间件需要设置以逗号隔开,那么该方法在哪里设置?
createStore(reducer, [preloadedState], [enhancer])
方法创建store的时候,可以调用applyMiddleware
方法在enhancer
位置设置中间件。
接下来就测redux-logger
的功能,然后每次点击按钮派发的时候,查看控制台效果
import reduxLogger from 'redux-logger'
export const store = createStore(reducer, applyMiddleware(reduxLogger))
然后我们给actionCreator
添加一个异步效果。查看不使用中间件情况下redux的情况。
// 延迟一秒钟返回
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
})
}
// 基于action.vote 使用bindActionCreators实现
export const voteAction = {
async sup() {
await delay()
return {
type: TYPE.VOTE_SUP
}
},
}
这个时候,再次点击按钮的时候控制台就会报错
先理解下面这段代码,如果立即输出上面的异步函数查看结果console.log(delay());
发现输出是一个promise并且状态是pending。那么要想获取这个状态一秒钟后的结果就需要使用then调用
在上面使用async/await
修饰的函数中,会默认返回一个promise状态,而非dispatch({type:...}})
派发的行为标识字段。因此在点击按钮的时候,执行的是一个dispatch({promise:...})
操作,因此才会报错。所以在actionCreator
中默认不支持异步操作。
如果想使用异步操作就需要引入redux-thunk
中间件,同时修改异步操作部分的代码
import reduxThunk from 'redux-thunk'
export const store = createStore(reducer, applyMiddleware(reduxLogger, reduxThunk))
修改后的代码如下。需要自己在内部实现手动派发一次完成最终效果。控制台一共会派发两次,第一次派发的时候,因为外部return,所以直击返回一个函数,所以格式:dispatch(函数)。但是该函数经过redux-thunk的处理重写dispatch,内部的type标识字段为undefined,但是并不会执行reducer函数操作(未进入执行)。而是在函数内部异步操作await放行后,执行手动实现的派发操作执行reducer更该状态。
sup() {
return async dispatch => { //redux-thunk会将dispatch作为参数传入
await delay()
dispatch({
type: TYPE.VOTE_SUP
})
}
},
当然如果使用redux-thunk
中间件的时候,代码是需要修改的,且很容易出错,那么这个时候就可以使用redux-promise
中间件。
import reduxPromise from 'redux-promise'
export const store = createStore(reducer, applyMiddleware(reduxLogger, reduxThunk, reduxPromise))
不修改任何代码,逻辑都已正常的情况写,符合我们的写法
async opp() {
await delay()
return {
type: TYPE.VOTE_OPP
}
}
redux-promise
中间件也是执行两次,第一次派发的时候也是基于重写的dispatch去派发,不会执行reducer函数。但是会监听函数内部异步操作,当函数内部异步操作结束后,会基于store.dispatch派发任务,执行reducer
redux toolkit
redux toolkit
中间件可以帮助我们简化创建store的操作。react-redux
可以理解为简化操作store。redux toolkit
最大的特点是基于切片机制,将reducer和actionCreator混合一起。
引入@reduxjs/toolkit
库中的configureStore
方法创建store,该方法就是用来代替createStore
方法。两个方法的核心思路都是一样的,都需要传入reducer和配置项,只不过操作的方法不一样。
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore()
以下是基本使用格式。在reduxjs/toolkit
库中使用configureStore
方法创建store的时候,默认配置处理异步中间件的是redux-thunk
。如果手动配置了middleware
配置项,那么就会覆盖默认配置,这个时候异步处理的中间件就需要自己引入。
import reduxLogger from 'redux-logger'
import reduxThunk from 'redux-thunk'
export const store = configureStore({
// 按模块划分reducer
reducer: {
},
// 配置插件
middleware: [reduxLogger,reduxThunk ] //已覆盖默认中间件
})
因其特点是采样切片混合reducer和actionCreator。因此我们在store目录下创建一个features文件,在里面创建每一个模版的切片。
调用@reduxjs/toolkit
库中提供的createSlice
方法快速创建切片。同时导出创建返回的reducer使用,该切片方法中包含了reducer和actionCreator,以下是基本格式
import { createSlice } from '@reduxjs/toolkit'
const taskSlice = createSlice({})
export default taskSlice.reducer
该切片文件的代码如下
import { createSlice } from '@reduxjs/toolkit'
const taskSlice = createSlice({
name: 'task', //切片名,类似各个模块名
initialState: { //该配置项指定store容器中state的初始值
taskList: null
},
reducers: {
//state就是容器中的状态,在这里拿到的都是经过initialState初始化后的值,经过immer库管理且无需再自己克隆
// action 派发的行为对象,在这里不用再考虑type标识问题。无论传递任何洗洗,都是以action.payload的形式获取
// getAllTaskList、removeTask、updateTask类似与派发标识
getAllTaskList(state, action) {
state.taskList = action.payload //payload为list数组对象
},
removeTask(state, action) {
if (Array.isArray(state.taskList)) {
state.taskList = state.taskList.filter(item => {
return +item.id !== +action.payload //payload为id
})
}
},
updateTask(state, action) {
if (Array.isArray(state.taskList)) {
state.taskList = state.taskList.map(item => {
if (+item.id === +action.payload) { //payload为id
item.state = 2
item.complete = new Date().toLocaleString()
}
return item
})
}
}
}
})
export default taskSlice.reducer
然后在configureStore
方法创建的store中已入经过切片处理返回的reducer,
export const store = configureStore({
// 按模块划分reducer
reducer: {
//合并reducer ,最终合并的state={ task:{ tasklist:[] } }
task: taskSliceReducer
},
// 配置插件
middleware: [reduxLogger, reduxThunk]
})
当代码写到这里发现,reducer已经知道了,那么action派发行为标识在哪里获取。这个时候就需要输出查看createSlice()
方法的返回值是什么了。
const taskSlice = createSlice({...})
console.log(taskSlice);
输出发现返回值中不仅包括了reducer还有actions。actions中就是我们需要的派发标识
将actions中提供的方法导出并调用,查看返回值可以看出。actions中的方法仅仅只是和createSlice方法中reducer配置项中的方法同名,并不是同一个。并且这些方法执行完毕返回的都是唯一的派发标识供dispatch使用。我们调用方法传入任何值,最终都是被payload属性接收,如果什么都不传,则payload默认undefined
let { getAllTaskList, removeTask, updateTask } = taskSlice.actions
console.log(getAllTaskList({ id: 1 }));
将用到的action全部导出,并且编写一个异步请求处理服务器响应
export let { getAllTaskList, removeTask, updateTask } = taskSlice.actions
//getAllTaskList()==>{type:'task/getAllTaskList',payload:undefined}
// 编写处理异步派发的处理,基于redux-thunk处理
export const getAllTaskListAsync = () => {
return async dispatch => {
let list = []
try {
let res = await getTaskList()
if (res.code === 0) {
list = res.list
}
} catch (error) {
console.log('网络出错了');
}
dispatch(getAllTaskList(list)) //list传递给action.payload接收
}
}
当reducer和action都准备好的时候,就可以在代码中使用了。首先引入store
<Provider store={store}>
<Task></Task>
</Provider>
之后在其他组件中,使用状态和派发函数的时候也不在使用react–redux中提供的connect
函数了,而是使用新的hooks函数。
useSelector
函数可以获取各自模块中的state值。useDispatch
函数用于获取dispatch派发函数
import { useDispatch, UseSelector } from "react-redux";
获取合并后state中task模块下的taskList 数据
let { taskList } = useSelector(state => state.task)
let dispatch = useDispatch()
引入actionCreator使用
import { getAllTaskListAsync, removeTask as removeTaskAction, updateTask } from '@/store/features/taskSlice.js'
// 初始化获取列表数据
useEffect(() => {
(async () => {
// 初始为空则获取数据
if (!taskList) {
setTableLoading(true)
// getAllTaskListAsync函数不在dispatch中调用是不会执行派发操作的,不会执行reducer
await dispatch(getAllTaskListAsync())
setTableLoading(false)
}
})()
}, [])
useEffect(() => {
if (!taskList) taskList = []
if (selectedIndex !== 0) {
taskList = taskList.filter(item => {
return +item.state === +selectedIndex
})
}
setTableData(taskList)
}, [selectedIndex, taskList]);
同步代码中的执行dispatch情况
dispatch(removeTaskAction(id))
dispatch(updateTask(id))
装饰器decorator
JavaScript的装饰器就是对类,类属性,类方法之类的一种装饰。简单理解就是在原有代码外层有包裹了一层处理逻辑,这样子就可以不直接修改原代码,从而实现某些功能。
如下就是一个简单的类装饰器。但是vscode默认不支持装饰器语法,需要处理。
const test = () => { }
// 类装饰器
@test
class Demo { }
首先打开vscode设置选项找到decorator的配置项,勾选
设置完成vscode识别装饰器语法后,还需要让webpack识别装饰器的语法,因此需要安装两个插件,安装完成后再webpack的插件中使用:yarn add @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
"babel": {
....
//让babel语法能够使用装饰器的语法
"plugins": [
[
"@babel/plugin-proposal-decorators", //支持装饰器语法
{
"legacy": true //使用遗留版本语法
}
],
[
"@babel/plugin-proposal-class-properties", //编译class插件
{
"loose": true //设置true代表直接往对象上设置属性,false代表使用Object.defineProperty()设置属性
}
]
]
}
如果控制台报错:Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (4:0)
就需要安装一个能够让装饰器和babel互相兼容的插件。
需要安装该插件:yarn add roadhog@2.5.0-beta.1
,是babel的版本中的语法能够兼容装饰器中的语法。
装饰器之类装饰器
正常情况下往一个对象身上添加静态私有属性和方法的操作如下。但是这么操作下来会发现一个问题,如果我多个类都需要执行创建同样的静态私有属性和方法,就会出现很多重复的代码,这个时候就可以使用类装饰器简化步骤
class Demo { }
// 添加静态属性
Demo.num = 10
Demo.say = function () { }
console.dir(Demo);
使用类装饰器大大减少重复代码,效果和上图一致。在执行类装饰器的时候,会将类作为参数插入类装饰器函数中使用。
const test = (target) => {
target.num = 10
target.say = function () { }
}
@test
class Demo { }
@test
class Demo2 { }
console.dir(Demo);
但是需要注意如果使用ES5的方法创建类会报错。装饰器只能作用于class类中
装饰器在经过编译和转换结果如下,主要代码是test(_class = class Demo { })
部分,这部分的代码意思是给一个变量赋值一恶搞类,并执行某些操作,重点是这个函数返回值是什么,最终Demo就是什么,不跟原先的class Demo再有关系了。
const test = (target) => {
target.num = 10
target.say = function () { }
}
@test
class Demo { }
/* var _class;
const test = target => {
target.num = 10;
target.say = function () { };
};
let Demo = test(_class = class Demo { }) || _class; // 添加静态属性 */
如果有多个装饰器需要使用的话,则不能添加任何符号进行换行,并且采样就近原则执行装饰器
//函数各打印输出
@sum
@addFun
class Demo { }
装饰器还可以和闭包函数一起操作,代码如下
const f1 = (x, y) => {
return target => {
target.num = x + y
}
}
@f1(1, 2) //会优先执行f1函数,将返回的函数作为装饰器函数
class Demo { }
如果存在多种上诉情况,代码执行顺序是如何进行?则会先将所有的函数执行,获取需要的装饰器函数,然后根据装饰器函数的执行顺序就近执行。
const f1 = (x, y) => {
console.log('f1外');
return target => {
console.log('f1内');
target.num = x + y
}
}
const f2 = () => {
console.log('f2外');
return target => {
console.log('f2内');
target.name = 'fun2'
}
}
@f1(1, 2) //会优先执行f1函数,将返回的函数作为装饰器函数
@f2()
class Demo { }
属性或方法装饰器
const readonly = (target, name, describe) => {
/*
target:{constructor: ƒ, getX: ƒ}类的原型
name:'x'
describe:{configurable: true, enumerable: true, writable: true, initializer: ƒ}属性的配置参数
如果是一个普通属性,那么原先是value就会变成initializer函数,由函数决定该值
当name:'getX'的时候
describe:{writable: true, enumerable: false, configurable: true, value: ƒ}其中value配置项不变,但是
其格式变为函数
*/
console.log(target, name, describe);
describe.writable = false
}
class Demo {
@readonly
x = 10
@readonly
getX() { } //原型添加方法
}
上面的装饰器函数的作用是设置属性或方法为只读,这个时候我们在尝试修值就会报错
let d = new Demo()
// 均报错
// d.x = 20
// Demo.prototype.getX = function () { }
但是可以通过Object.defineProperty
重新设置值
Object.defineProperty(d, 'x', {
value: 20
})
编写一段获取代码执行时间的函数
const loggerTime = (_, name, describe) => {
let fun = describe.value //value存储的就是getX函数
describe.value = function proxy(...params) {
//重新设置改函数的值,尽管该函数被设置为只读,基于defineProperty设置
// 后续再次调用函数d.getX()实际执行重写的函数,this执行实例
console.time('name')
let res = fun.call(this, params) //保持this指向不变,可以正确获取this.x的值
console.timeEnd('name')
return res
}
}
@readonly
@loggerTime
getX() {
for (let i = 0; i <= 9999; i++) { }
return this.x
} //原型添加方法
let d = new Demo()
console.log(d.getX(10, 20));
如果在属性或方法前面使用闭包,那么规则和类装饰器一致,优先执行函数,将返回的函数作为装饰器函数。
但是需要注意返回值的情况和类装饰器不同,在这里有约束条件
如果直接在装饰器函数中返回一个普通数值,那么直接报错。
const getY = (_, name, describe) => {
return 100
}
class Demo2 {
@getY
x = 100
}
let d2 = new Demo2()
报错提示,必须返回一个对象,且必须是一个配置对象。如果返回的普通对象并没有设置属性的配置信息,那么不会被接收。
当返回的是一个类属性描述对象的时候,可以正确赋值,如果想重新制定值,不论函数或者属性都通过initializer
配置函数处理
const getY = (_, name, describe) => {
return {
initializer() {
return '@@'
}
}
}
class Demo2 {
@getY
x = 100
} //x被修改为@@
Mobx5使用
因为mobx5是基于装饰器语法,所以需要让vscode和babel能够识别和兼容装饰器的语法。安装的插件和配置项信息在上文已有。
首先需要知道装饰器的基本是适用于类的,因此类组件可以直接被装饰器修饰,但是函数组件不行,后续介绍如何处理
首先安装库:yarn add mobx@5 mobx-react@6
,确保两个库之间版本兼容
然后分别从两个库中导入创建公共状态的方法和修改函数
import { observable, action } from 'mobx'
import { observer } from 'mobx-react'
然后将装饰器语法应用的类组件中,页面可以正常显示内容,且状态是保存到公共状态区域的
//公共容器
class Store {
@observable num = 100 //公共状态
@action setNum() { //修改公共状态的方法
this.num++
}
}
let store = new Store()
@observer
class DemoMobx extends React.Component {
render() {
return <>
<h1>数值:{store.num}</h1>
{/* 在JavaScript中,函数的this关键字的指向是在运行时确定的,而不是在定义时确定的。
当你直接将store.setNum传递给onClick属性时,实际上是将一个对setNum方法的引用传递给了onClick。当按钮被点击时,
React会调用这个函数引用,但是this关键字在这个上下文中指向的是undefined,而不是store。 */}
{/* onClick事件处理程序的调用方式是函数调用,而不是方法调用 */}
{/* 方法调用是指通过对象来调用函数。函数调用是指直接调用函数,而不是通过对象 */}
{/* 两种解决方法:1:store.setNum.bind(store) 2:() => store.setNum()*/}
<Button type="primary" onClick={() => store.setNum()}>按钮</Button>
</>
}
}
但是在函数组件中需要如何处理。在装饰器的语法中@observer class 类名
会被转换格式。装饰器本质是将类作为参数使用。因此在函数组件中可以直接将组件作为参数调用使用。
const DemoMobx = observer(() => {
return .....
})
常见方法原理解析
autorun()
方法在初次会执行一次,自动建立依赖监测,监测函数内部使用到的状态,后续会根据函数内部依赖的状态值发生变化,会重新去执行回调
import { observable, autorun } from 'mobx'
class Store {
x = 10
}
let store = new Store()
autorun(() => {
console.log('autorun', store.x);
})
那在上面代码的基础上添加一个定时器修改x的值,查看autorun
是否会重新执行。结果如图,发现该函数的回调并没有重新执行,但是当我们给依赖的状态设置装饰器@observable
的时候,该回调就会执行
setTimeout(() => {
store.x = 100
}, 1000);
@observable
:将状态设置为可监测的,只有被设置为可监测的状态值,基于autorun()
或@observer
等监测机制才会生效。其本质是函数,可以传入数据查看返回结果。返回一个Proxy实例。Mobx5基于proxy完成代理操作。 使用Proxy代理的好处是,可以设置配置对象,可以在内部的get或set函数中执行某些操作。
let obj = observable({
x: 1,
y: 10
})
除了autorun()
方法能够实现监听,在mobx
中还有一个observe()
方法也能实现监听,但是语法不同。该方法要求监听的依赖必须是经过observable
处理的,即已经是proxy代理的。初始时候不会执行回调,当依赖对象改变的时候回调函数才会执行,如修改obj.x = 1000
,对应的回调函数会去执行,可以查看输出
let obj = observable({
x: 1,
y: 10
})
observe(obj, change => {
console.log(change);
})
但是如果直接使用observable
方法监视一个基本数据类型就会报错:let x = observable(10)
,在给出的报错提示中要求使用observable.box()
方法修饰基本数据类型
let x = observable.box(10) //返回的结果是一个ObservableValue 对象实例,在该原型上有get和set方法来获取设置x的值
computed
计算属性装饰器,用于创建一个具有缓存效果的属性值,效果和vue的计算属性一值。依赖的值改变了才会重新执行,否则就继续使用缓存中的值。
如下代码中,autorun
方法中只有依赖的x只会变化,但是每次重新执行回调函数的时候,store.x, store.count * store.price
这些值都会重新计算取出。但实际上只有x的值会变化,因此我们可以利用计算属性的缓存机制来完成。
class Store {
@observable x = 10
@observable count = 15
@observable price = 100
}
let store = new Store()
autorun(() => {
console.log('autorun', store.x, store.count * store.price);
})
setTimeout(() => {
store.x = 1000
}, 100)
修改代码如下,计算属性的使用格式如下,注意get不能丢,且必须返回值,格式是方法但是使用的时候当做属性,与vue一致。 计算属性的值会用到的地方一开始计算一次(页面中没有用到计算属性的值则不执行函数),然后后续依赖状态不变的时候,就使用缓存中的值
@computed get total() {
console.log('computed执行');
return this.count * this.price
}
autorun(() => {
console.log('autorun', store.x, store.total);
})
store.x = 1000 //不会执行计算属性,取缓存中的值,store.count=1000,重新计算,将新值替换缓存中值
reaction
也是一个用于监测的方法,区别与autorun
方法,能够提供更细腻化的监视。默认情况情况下不会执行回调。必须是监测的目标发生改变才会执行
基本使用格式如下:reaction( ()=>[监听的依赖],callback )
reaction(() => [store.x, store.total], () => {
console.log('reaction执行', store.x, store.total);
})
action
方法是解决什么问题?请看下面代码,有一个autorun
方法内部依赖x的值,然后在一个定时器中执行两次修改操作。查看输出结果,会发现autorun
方法在更新的时候执行也执行了两次,但是这样子有效率和性能问题。我们希望执行修改store.x
的操作能有和useState
函数一样的批处理效果,这个时候就需要借助action
方法完成。
autorun(() => {
console.log('autorun', store.x);
})
setTimeout(() => {
store.x = 1000
store.x = 123
}, 1000);
将需要被修改的状态写在一个函数中,然后使用action装饰该函数,在别的地方调用该函数即可。打印输出看出,最终在action修饰的方发中,修改状态的操作合并为一起了,最终autorun方法只执行了一次更新操作
@action change() {
this.x = 10
this.x = 1000
}
setTimeout(() => {
store.change()
}, 1000);
如果我们将调用方法修改如下格式,就会出现问题。函数调用的时候this执行运行时候决定,这个时候fun()的this作用域指向window,经过webpack处理转换为undefined了
let fun = store.change //存储函数的引用
fun()
这个时候如果想继续使用这种方法调用函数,那么就需要使用@action.bound change() { }
修饰方法,该修饰符作用确保当前方法的this永远指向当前类实例。
但是如果不愿意在类中定义方法修改状态,而是想在其他地方直接修改状态,又想和action
装饰的方法具有同样的效果,就可以使用runInAction()
方法处理。效果如图,和action
装饰的方法修改状态效果一致。
setTimeout(() => {
runInAction(() => {
store.x = 10
store.x = 1000
})
}, 1000);
既然action
装饰器修饰的方法是修改状态的,那么当语句异步处理的时候该如何解决?直接添加async
关键字即可,代码如下
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(999)
}, 1000)
})
}
class Store {
@observable x = 1
@action.bound async changeX() {
let res = 10
res = await delay()
this.x = res
}
}
let store = new Store()
autorun(() => {
console.log('autorun', store.x);
})
store.changeX()
这个写法如果出现在组件中就无法完成会报错,解决方法如下,使用runInAction
方法将需要更新的内容统一处理。这种写法无论是普通环境下,或者组件中都不会报错
@action.bound async changeX() {
let res = 10
res = await delay()
runInAction(() => {
this.x = res
})
}
基于mobx重构TODOLIST
创建一个store目录结构
import { observable, action } from 'mobx'
export default class PersonStore {
@observable info = 'person模块'
@action.bound resetInfo() {
this.info = '你好'
}
}
import { observable, action } from 'mobx'
export default class TaskStore {
@observable taskList = null
@action.bound async getAllTaskListAction() {
let list = []
try {
let res = await getTaskList(0)
if (+res.code === 0) list = res.list
} catch { }
runInAction(() => {
this.taskList = list
})
}
@action.bound removeTaskAction(id) {
this.taskList = this.taskList.filter(item => {
return +item.id !== +id
})
}
@action.bound updateTaskAction(id) {
this.taskList = this.taskList.map(item => {
if (+item.id === +id) {
item.state = 2
item.complete = new Date().toLocaleString()
}
return item
})
}
}
然后在根Store中引入使用
import TaskStore from "./TaskStore";
import PersonStore from "./PersonStore";
class Store {
constructor() {
this.task = new TaskStore()
this.person = new PersonStore()
}
}
let store = new Store()
console.log(store);
export default store
这样子在根store身上就能获取各个模块的属性和方法
如果想在某一个板块中获取根store或者其他板块的属性和方法该如何获取。思路就是如何传递根store的this。可以在new各个板块的时候将根store的this传递过去初始化保存。
class Store {
constructor() {
// this是当前上下文Store类实例
this.task = new TaskStore(this)
this.person = new PersonStore(this)
}
}
export default class TaskStore {
constructor(root) {
this.root = root //将根store的this实例保存,方便使用
}
...
}
然后在入口文件中引入store使用。如何将store供给所以组件使用需要借助mobx-react
库提供的<Provider>
组件,类似react-redux
的。
import store from './store/index.js';
import { Provider } from 'mobx-react'
// store = { task:{...},person:{} }
<Provider store={store}>
<Task></Task>
</Provider>
如果使用store的组件是一个类组件那么需要从mobx-react
中引入observer
和inject
方法。
@observer
装饰器用于组件视图更新
@inject('store')
装饰器用于获取刚才存放在<Provider store={}>
组件中的属性store 获取后并作为参数传递给组件。
@inject('store')
@observer
class 类组件 {}
如果是一个函数组件,该如何使用装饰器。将inject('store')
装饰器执行获取需要的参数,并返回一个函数,执行传入组件中作为参数使用。类似connect(mapStatetoProps)(Vote)
思想。
export default inject('store')(observer(Task)); //mobx-react中的observer函数必须传入组件
或者
const Task = inject('store')( observer( function Task(props){..} ) )
最终在函数组件的props参数中接收store的值如下。
代码中主要操作store的部分如下
let { task } = props.store
await task.getAllTaskListAction()
task.removeTaskAction(id)
task.updateTaskAction(id)
mobx6
使用mobx6的语法平替上面弄基于mobx5完成的todolist。
安装:yarn add mobx mobx-react
全部为最新版本
然后将原先所有用到的@装饰器
全部去掉,其余原封不动
从mobx6中引入makeObservable, makeAutoObservable
两个方法。两个方法都必须在constructor
构造器中使用
其中makeObservable
方法使用如下
constructor(root) {
this.root = root
//效果和原先写法意思一致
makeObservable(this, {
taskList: observable,
getAllTaskListAction: action.bound,
removeTaskAction: action.bound,
updateTaskAction: action.bound
})
}
makeAutoObservable
方法使用如下,该方法是makeObservable
的增强版,能够自动识别添加装饰器对应的语法。
constructor(root) {
this.root = root
makeAutoObservable(this)
}