看完这篇文章,你也可以写出Redux

最近在网上看了很多关于Redux的文章,感觉很奇怪哈,为什么这么简单的东西会被很多人讲的那么复杂。官方对Redux的描述有以下几个特点:可预测可调试集中管理灵活单一数据流等等。这都能理解,毕竟营销嘛,看着就是一乐,谁开发了一个库,都会去有的没的说一些伪优点。在经历了长期的正反思考后,我得出了以下几个结论,希望能够对你有用:

  • 如果一个博主不能将复杂的理论简单化,那他一定没懂这个理论。因为再复杂的理论也是由单个知识点串起来的,所以一定不存在永远都搞不懂的理论,这只是 精力问题 + 学习方式问题。
  • 前端框架层出不穷,像redux这种第三方库就更不用提了,更新的频率只会越来越快。越是这种时候,就越考验一个人的洞察能力。就比如 状态管理,你觉着有必要使用第三方库吗?第三方库的衍生品你觉着有必要使用吗?所以在这种大环境下,对技术leader的要求就比较高,合格的技术leader更是一票难求。
  • 多读源码,多写code。眼高不手低了,洞察问题的能力自然就上去了。

接下来,我们来看看如何写出一个状态管理。

需求

一想到状态管理,其实需求就是 有一个地方,可以保存我们的状态,在有能力改变状态的同时,我们还要感知到它的变化

这不就是所有的状态管理的库都会去做的事情嘛,剩下的就只是实现的方式不一样,扩展的能力不一样而已。

如何存储状态

2个思路,分别如下:

  • 维护全局变量。
  • 闭包。

在这里我们使用闭包的形式来实现:

javascript 复制代码
function CustomCreateStore(){
    let state = undefined;
    let getState = () => {
        return state;
    }
    return {
        getState
    }
}

export CustomCreateStore;

有了上面的方法,我们就可以使用下面的代码来访问state了。

javascript 复制代码
CustomCreateStore().getState();

规范state

个人认为这一步是很有必要,不能用户放进来啥我们就存啥,想要用我的库,就必须要遵守我的原则。我们希望用户可以用object的形式去保存它想要用到的变量。最简单的方式就是让用户把状态传进来。

所以改造如下:

javascript 复制代码
function CustomCreateStore(obj){
    if( Object.prototype.toString.call(obj) !== '[object Object]' ){
        throw new Error('CustomCreateStore的参数必须是个对象');
    }
    let state = obj;
    let getState = () => {
        return state;
    }
    return {
        getState
    }
}

我们现在可以随心所欲的定义initState了。

javascript 复制代码
CustomCreateStore({count: 1}).getState();

连接state与reducer

最开始讲需求的时候,我们就说过,一个状态管理库要包含2个能力,分别是改变状态的能力监听状态改变的能力

想要实现改变状态的能力,我们可以做一个桥梁,这个桥梁的作用是连接着用户的initState、保存用户改变state的方法。具体思路如下:

如何实现用户派发一个通知,然后状态管理库就知道了用户派发通知了呢?

其实很简单,我们只需要给用户暴露一个方法,只要用户调用了这个方法,状态管理库就认为你要改变状态了。

紧接着,我们如何根据通知的类型,去执行相应的更新呢?

我觉着用户实现这一步就好了。也就是库收到了这个通知,然后去执行这个通知对应的方法,至于通知是什么,通知对应的方法是什么,统统不管,我只负责收到和触发。如下:

jsx 复制代码
let store = CustomCreateStore({count: 1});

// 通知对应的方法
function executeClickAdd(){
    return ...
}

// 点击按钮触发
const clickAddButton = () => {
    // 派发一个通知
    store.dispatch({
        type: 'click-add'
    })
}

<button onclick={ clickAddButton }> +1 </button>

那问题来了,状态管理库如何将 通知click-addexecuteClickAdd函数联系起来?我们可以再随意一点,我哪知道你是哪个通知对应哪个方法,用户去自己想办法吧。

此时用户愤怒到了极点,但又觉得它说的有道理,所以用户维护了一个配置:

javascript 复制代码
function ActionToExecuteOfConfig(state, action){
    switch(action.type){
        case 'click-add':
            return state + 1;
        default:
            return state;
    }
}

于是用户将上述方案交给了状态管理,并且对狠狠的说了一句,你必须要将当前state返回给我,这样我才能依据当前的state去得到下一步的state。状态管理库看到这样的配置就乐了,正符合我意,于是连夜改了createStore方法。

javascript 复制代码
function CustomCreateStore(obj, fn){
    if( Object.prototype.toString.call(obj) !== '[object Object]' ){
        throw new Error('CustomCreateStore的参数必须是个对象');
    }
    let state = obj;
    let getState = () => {
        return state;
    }
    
    // 新增++++++
    let ActionToExecuteOfConfig = fn;
    let dispatch = (action) => {
        state = ActionToExecuteOfConfig(state, action);
    }
    
    return {
        getState,
        dispatch
    }
}

用户看到这样的方案后,也连夜修改了业务代码如下:

jsx 复制代码
function ActionToExecuteOfConfig(state, action){
    switch(action.type){
        case 'click-add':
            return state + 1;
        default:
            return state;
    }
}


let store = CustomCreateStore({count: 1}, ActionToExecuteOfConfig);
let curData = store.getState();

// 点击按钮触发
const clickAddButton = () => {
    // 派发一个通知
    store.dispatch({
        type: 'click-add'
    })
    curData = store.getState();
}


<button onclick={ clickAddButton }> +1 </button>
<div>当前的值是:{ curData.count }</div>

点击button按钮后,页面上的数据也跟着+1了,用户露出了慈祥的笑容。

监听状态的改变

有了上面的基础,相信这个肯定难不住你们,subscribe的时候将callback push到listener队列里,最后在dispatch的末尾将listener里的callback全部依次执行即可。

jsx 复制代码
function CustomCreateStore(obj, fn){
    if( Object.prototype.toString.call(obj) !== '[object Object]' ){
        throw new Error('CustomCreateStore的参数必须是个对象');
    }
    let state = obj;
    let listener = [];
    
    let getState = () => {
        return state;
    }
    
    // 新增++++++
    let ActionToExecuteOfConfig = fn;
    let dispatch = (action) => {
        state = ActionToExecuteOfConfig(state, action);
        // 执行监听函数
        for (let index = 0; index < listener.length; index++){
            listener[index](state);
        }
    }
    
    // 新增++++++
    let subscribe = (callback) => {
        listener.push(callback);
        return function unsubscribe(){
              const index = nextListeners.indexOf(listener)
              nextListeners.splice(index, 1)
        }
    }
    
    return {
        getState,
        dispatch,
        subscribe
    }
}

用户的代码修改如下:

jsx 复制代码
// 其余代码不变

let store = CustomCreateStore({count: 1}, ActionToExecuteOfConfig);

store.subscribe(
    () => {
        console.log('store发生变化了');
    }
);

// 其余代码不变

此时用户再点击button +1时,发现我们注册的listener生效了。

改造state

随着应用的越来越大,做这个项目的人越来越多,因为所有的case都写在了一个ActionToExecuteOfConfig函数里,这就避免不里冲突了。你影响我,我影响你的。所以用户又给状态管理提了一个需求,你要给我想办法,我们不想所有的case都写在一个函数里,限你下班之前给我这个需求写出来,要不然我就不用你了。

于是状态管理就想了一个招,我给每个initState都动态分配一个名称name吧,在createStore的时候,我支持你传入一个对象,但是这个对象我有一个条件,value必须是用户处理action的函数。如果你想要访问自己注册的initState,就必须通过store.getState().name来访问,而这个name的值就是你的处理action得函数名。具体做法如下:

javascript 复制代码
// 新增combineReducers函数 ++++++++++++++++++++++
function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i];
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers);

  return function combination(state = {}, action) {
    let hasChanged = false;
    const nextState = {};
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i];
      const reducer = finalReducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      nextState[key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}

// CustomCreateStore函数修改如下:
function CustomCreateStore(fn){
    if( Object.prototype.toString.call(obj) !== '[object Object]' ){
        throw new Error('CustomCreateStore的参数必须是个对象');
    }
    let state = obj;
    let listener = [];
    let ActionToExecuteOfConfig = fn;
    
    // 获取initState
    let getState = () => {
        return state;
    }

    // 派发通知
    let dispatch = (action) => {
        state = ActionToExecuteOfConfig(state, action);
        // 执行监听函数
        for (let index = 0; index < listener.length; index++){
            listener[index](state);
        }
    }
    
    // store的监听函数
    let subscribe = (callback) => {
        listener.push(callback);
        return function unsubscribe(){
              const index = nextListeners.indexOf(listener)
              nextListeners.splice(index, 1)
        }
    }
    
    // 初始化状态
    // 此时会执行combination函数,这个函数主要就是将所有的key对应的函数都执行一遍,最后再将key,key对应的value全部都拼到一个新的对象里
    dispatch({ type: 'initState-996' })
    
    return {
        getState,
        dispatch,
        subscribe
    }
}

用户修改业务代码如下:

javascript 复制代码
let store  = CustomCreateStore(
    combineReducers({
        count: () => {},
        number: () => {}
    })
);

// 访问count下的initState
let count = store.getState().count.xxx;

// 访问number下的initState
let number = store.getState().number.xxx;

最后,经过这么一顿折腾,用户露出了满意的笑容。

最后

好啦,本篇文章到这里也就结束啦,希望我的文章对您有帮助,2023年12月16日,23点39分,我们下期再见啦,拜拜~~

相关推荐
叁分之一29 分钟前
“我打包又失败了”
前端·npm
tang游戏王12330 分钟前
AJAX进阶-day4
前端·javascript·ajax
无语听梧桐34 分钟前
vue3中使用Antv G6渲染树形结构并支持节点增删改
前端·vue.js·antv g6
如影随从35 分钟前
04-ArcGIS For JavaScript的可视域分析功能
开发语言·javascript·arcgis·可视域分析
go2coding1 小时前
开源 复刻GPT-4o - Moshi;自动定位和解决软件开发中的问题;ComfyUI中使用MimicMotion;自动生成React前端代码
前端·react.js·前端框架
freesharer1 小时前
Zabbix 配置WEB监控
前端·数据库·zabbix
web前端神器1 小时前
forever启动后端服务,自带日志如何查看与设置
前端·javascript·vue.js
才艺のblog1 小时前
127还是localhost....?
javascript·https·浏览器特性
是Yu欸1 小时前
【前端实现】在父组件中调用公共子组件:注意事项&逻辑示例 + 将后端数组数据格式转换为前端对象数组形式 + 增加和删除行
前端·vue.js·笔记·ui·vue
今天是 几 号1 小时前
WEB攻防-XSS跨站&反射型&存储型&DOM型&标签闭合&输入输出&JS代码解析
前端·javascript·xss