theme: channing-cyan
前言
续上篇,没想到很多读者朋友们这么关注,感谢大家的支持和建议,我只是发表个人看法以及自己的一些思考也许不够全面,使用 Vue 举例也仅仅只是作为引路且 Vue 的关注度也是较高的。那有些朋友想听听除 Vuex 的其他方案,今天将从 Redux 入手逐渐拓展(如标题一样浅谈)。
回顾上篇:浅谈前端的状态管理(上)
Redux
作为 React 全家桶的一员,Redux 试图为 React 应用提供可预测化的状态管理机制。和大多数状态管理方案一样,Redux 的思想也是发布订阅模式,我们还是以图书馆为例来简单了解一下 Redux。
Redux 的基础操作大致为:
- Store(图书馆管理员)
- State(书本)
- Action(借书单)
- store.dispatch(提交借书单)
- Reducer(包装书本)
- store.subscribe(接收书本)
Store(图书馆管理员)
Store 可以看作是一个容器,整个应用只有一个 Store。就好比你想要借书只能找图书管理员。
代码语言:javascript复制import { createStore } from 'redux'
const store = createStore(reducer);
State(书本)
对于 State 来说他只能通过 Action 来改变(既你借书只能提交借书单来借),不应该直接修改 State 里的值。
使用store.getState()
可以得到state。
import { createStore } from 'redux'
const store = createStore(reducer)
store.getState()
Action(借书单)
你想借书咋办?那当然是向管理员提交借书单了。那用户是接触不到 State 的,只能通过 View (视图)去操作(如点击按钮等),也就是 State 的变化对应 View 的变化,就需要 View 提交一个 Action 来通知 State 变化。(既通过提交借书单给管理员才会有接下来一系列的其他操作)
Action 是一个自定义对象,其中type
属性是约定好将要执行的操作。
const action = {
type: 'click',
info: '提交借书单'
}
store.dispatch (提交借书单)
store.dispatch 是 View 发出 Action 的唯一方法,他接受一个 Action 对象(既提交借书单),只是把单的信息给了图书管理员,他在根据单子来搜索相应的书本。
代码语言:javascript复制store.dispatch(action)
Reducer(包装书本)
Store 收到一个 Action 后,必须给出一个新的 State ,这样 View 才会发生变化,而新的 State 的计算过程就是 Reducer 来完成的。(既拿到单子将你的书本打包装袋等)
Reducer 是一个自定义函数,它接受 Action 和当前的 State 作为参数,返回一个新的 State。
代码语言:javascript复制const reducer = (state, action) => {
return `action.info: ${state}` // => 提交借书单:红楼梦
}
store.subscribe(接收书本)
当 State 一旦发生变化,那么 store.subscribe() 就会监听到自动执行更新 View。
代码语言:javascript复制const unsubscribe = store.subscribe(() => {
render() {
// 更新view
}
})
// 也可以取消订阅(监听)
unsubscribe()
小结
相信刚接触 Redux 的同学都会觉得 Redux 比较繁琐,这也与他的思想有关:Redux 里的一切应该都是确定的。
尽管在 Redux 里还是没办法做到一切都是确定的(如异步)但是应该保证大多数部分都是确定的包括:
- 视图的渲染是可确定的
- 状态的重建是可确定的
至于为什么要这么做,上一篇我已有提及。他的重要之处在于:便于应用的测试,错误诊断和 Bug 修复。
状态管理的目的
那其实大多数程序员使用 Redux 的最多的场景无非是从 A 页面返回 B 页面 需要保存 B 页面的状态。
倘若项目不大,用 Redux 或 Vuex 是不是会显得有些大?我们知道在 Vue 中有提供 keep-alive 让我们缓存当前组件,这样就可以解决上述的场景。
但是很遗憾在 React 中并没有像 Vue 一样的 keep-alive。社区中的方案普遍是改造路由,但是这种改造对于项目入侵过大且不易维护,另外在 react-router v5 中也取消了路由钩子。于是,对小型项目来说自己封装一个函数也不失为良策。(当然你想用 Redux 也没问题,咱们只是探索更多方式)
还是用图书馆来举例子,现在有一个图书馆管理系统,你从列表页(list)跳入详情页(detail)需要保存列表页的状态(如搜索栏的状态等)。
假设你使用的技术栈是(react antd),来手写一个简单粗暴的(核心是利用context来进行跨组件数据传递):
代码语言:javascript复制// KeepAlive.js
export default function keepAliveWrapper() {
return function keepAlive(WrappedComponent) {
return class KeepAlive extends WrappedComponent { // ps
constructor(props) {
super(props)
// do something ...
}
componentDidMount() {
const {
keepAlive: { fieldsValue },
} = this.context
// do something ...
super.componentDidMount()
}
render() {
// do something ...
return super.render()
}
}
}
}
这里提一下为什么要继承原组件(// ps)。
如果常规写法返回一个类组件(class KeepAlive extends React.Component),那本质上就是父子组件嵌套,父子组件的生命周期都会按秩序执行,所以每当回到列表页获取状态时,会重复渲染两次,这是因为 HOC 返回的父组件调用了原组件的方法,到导致列表页请求两次,渲染两次。
若使 HOC(高阶组件)继承自原组件,就不会生产两个生命周期交替执行,很好的解决这个问题。
代码语言:javascript复制// main.jsx 根组件
import React from 'react'
const appContext = React.createContext()
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
keepAlive: {}, // 缓存对象
isCache: false, // 是否缓存
fieldsValue: {} // 缓存表单值
}
}
componentDidMount() {
// 初始化
const keepAlive = {
isCache: this.state.isCache,
toggle: this.toggleCache.bind(this),
fieldsValue: this.state.fieldsValue,
}
this.setState({ keepAlive })
}
// 这里封装一个清除状态的方法 防止渲染警告(you can't set fields before render ...)
// 比如 list1 => list1/detail => list2 需要将跳转放在以下回调中并清除状态
toggleCache(isCache = false, payload, callback) {
const { fieldsValue = null } = payload
const keepAlive = {
isCache,
fieldsValue,
toggle: this.toggleCache.bind(this),
}
const fn = typeof callback === 'function' ? callback() : void 0
this.setState(
{
keepAlive,
},
() => {
fn
}
)
}
render() {
const { keepAlive } = this.state
<appContext.Provider value={{ keepAlive }}>
// your routes...
</appContext.Provider>
}
}
至于为什么不直接使用 context,而多封装一层 keepAlive,是为了统一处理 context,在组件头部中使用装饰器这种简洁的写法(@keepAlive)你就立马知道这是一个有缓存的组件(方便阅读及维护)。
代码语言:javascript复制// 在页面使用时
import React from 'react'
import keepAlive from '../keepAlive'
// keepAlive的位置需要放在原组件最近的地方
@keepAlive()
class App extends React.Component {
constructor(props){
super(props)
this.state = {
// init something...
}
}
componentDidMount() {
// do something...
if(this.context.keepAlive.fieldsValue) {
const { tableList } = this.context.keepAlive.fieldsValue
console.log('缓存啦:',tableList) // 缓存啦:['1', '2']
}
}
// 查看详情
detail = () => {
this.context.keepAlive.fieldsValue = {
tableList: ['1', '2']
}
// jump...
}
// 当需要跨一级路由进行跳转时,如 list1 => list1/detail(下面这个方法应该在详情页里) => list2,此时需要处理一下警告
toList2 = () => {
this.context.keepAlive.toggle(false, {}, () => {
// jump...
})
}
}
在上述使用了装饰器写法,简单说一下,需要先配置以下 babel 放可使用哦~
npm install -D @babel/plugin-proposal-decorators
在jsconfig.json
中(无则新建)配置一下:
{
"compilerOptions": {
"experimentalDecorators": true
},
"exclude": [
"node_modules",
"dist"
]
}
在 .babelrc 配置:
代码语言:javascript复制{
"plugins": [
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
}
上面方法比较适用刚才说的场景(从 A 页面返回 B 页面 需要保存 B 页面的状态),有人的说,你这样还不如用 Redux 或 Mobx 不就好了?跨路由跳转还得手动清除状态防止警告。。。仁者见仁,智者见智吧。自己封装了也说明自己有所研究,不论他易或难,编程本身不就该是不断探索吗,哈哈。尽管你写的可能不够好或是咋样,虚心接受批评就是了,毕竟厉害的人多着呢。
最后
我已经尽量写的详细,但是众口难调,请大佬轻喷~
都看到这了,不点赞关注一下(或者提些意见)再走吗?
本篇也只是拿 React 作引路一直拓展出下面一系列的问题,就如上篇 Vue 一样。
再说了现在前端两大流行框架不都是这两个吗(当然 js 基础也不能落下)。
最后再次放上上一篇文章,让大家温习一下~
回顾上篇:浅谈前端的状态管理(上)