从0到1重新撸一个Redux

·  阅读 391
从0到1重新撸一个Redux

文初先问大家一个简单的问题:Redux是什么?答之:在React项目里解决复杂层级的组件数据沟通问题,更新Redux数据,组件视图也会更新。相信这是绝大多数React项目开发者的答案。包括笔者我一开始也是这么想的。初级工作者停留在一个工具/库的运用场景、API用法以及使用配置方式等等,如果要上升到中高级,无可避免的需要对自己使用中的东西进行更加深入的探秘。于是乎笔者决定从自身对Redux的经验和理解出发从0到1从代码层面解析Redux。笔者翻看了需要掘金啊知乎啊语雀上的许多对Redux的源码解析的文章,而本文不是直接开始show和讲解源码,而是从Redux设计思想和目的出发去实现一个Redux,希望本文对刚开始接触Redux或者希望更深入了解Redux的开发者有所帮助和启示。



Redux只是一个提供数据状态的工具

如果要理解Redux,需要把他从脑海中既有的印象和标签撕开,他不属于React,他只是一个数据状态的管理工具而已。切记切记,先把React放在一旁。



一切的一切从Redux的设计思想开始

  • 单一数据流
  • STORE(也可以叫做原始数据)只读
  • 改变数据要求纯函数

所有的代码实现将围绕着这三条设计思想



按照设计思想去假设

对于MV/XX类框架,最魔法的莫过于改动数据便可以同步驱动视图吧。不管在哪个框架下,那么数据都是第一位存在的东西。于是乎我们假设有以下这个数据:

var store = {
    name: 'lemon',
    age: 25

    // 以下还有更多的数据
}

整个应用的数据被储存在上面的store中,并且这样的store在应用中只有一个,这便是单一数据流的思想。比如上面的store.name不仅个人介绍页面被用到了也会被个人空间页面所用到。视图中的name数据只会来自一个store。好啦~不管怎样,我们已经有了一个store,里面有各种的视图所需要的数据,一切都在里面保管就好了,十分美妙。

来了个需求,我们需要在个人页面编辑name,然后个人空间页面的name就被更新了。好家伙,这不简单吗?于是乎我们想着写入以下代码:

store.name = 'lemon zhang'

哦吼,抱歉,别忘了Redux设计思想:STORE(也可以叫做原始数据)只读。这样虽然美妙,但是抱歉我们不可以。于是乎我们想到给store添加方法让这个api去改动我们要的数据就好了。那么这个方法就叫做dispatch吧,于是乎有了以下代码

store.dispatch({name: 'lemon zhang'});

console.log(store.name); // 'lemon zhang'



改变数据要求纯函数,纯函数也就是没有副作用的意思。用以下代码理解
store.dispatch({name: 'lemon zhang'});
store.name === 'lemon zhang' // true
store.name === 'lemon zhang' // 为false 即dispatch方法里有副作用 输入和输出不唯一

总结:按照Redux设计思想,我们需要有个store,只读,只能通过方法dispatch去改动我们的数据,dispatch背后的实现不支持副作用。

按照假设去实现吧

直接看代码一步步来吧

// 首先我们有个初始数据,切记,这个数据不是store哟,只是提供原始数据源
var initState = {
    name: 'lemon',
    age: 25
};

好了,我们需要写一个方法去生成我们需要的store这个方法就起名叫做createStore吧,我们已经假设好了我们心目中store的样子了,让我们在createStore去实现它吧。

var initState = {
    name: 'lemon',
    age: 25
};

function createStore(state = initState) {
    const store = {
        ...state
    };
    function dispatch(key, value) {
        store[key] = value;
    }
    function getStore() {
        return store
    }
    return {
        getStore,
        dispatch
    }
}

var store = createStore();
var user = store.getStore();
user.name === 'lemon'; // true
user.age === 25; // true

store.dispatch('name', 'lemon zhang');
store.dispatch('age', 26');

user.name === 'lemon zhang'; // true
user.age === 26; // true

啊 是不是真简单。看起来Redux也没什么神奇的地方嘛。于是乎我们把以上代码美滋滋得拿去项目使用,问题是一堆堆的。

// 对于多级引用类型的数据,上面dispatch无能为力
var state = {
    detail: {
        user: {
            name: 'lemon',
            age: 25
        }
    }
}
型如store.dispatch('name', 'lemon zhang')的设计将无法使用,无法遍历层级

对于一个复杂的大项目而言,有许许多多的数据需要被状态管理,如果所有的数据全部展开放进store里那么无法避免的会带来key重复的问题。但是如果按照模块去划分,以层级结构的方式去设置store那么dispatch是无法使用的。
除此之外,在前端日益复杂的交互场景下也无法适用,比如分页加载,需要反复进行分页请求将数据按照顺序进行拼接在设置进store里面,将带来反复getStore的地狱。



所以,这些问题让我们需要重新思考如何设计createStore,让其更合理些。于是乎我们引入“对象结构描述”这一概念,他有个更让人熟悉的名字:reducer。reducer描述了对象再各个场景下的结构形态,什么是场景呢?上一页、下一页、增1、减1都是场景,场景也有个更熟悉的名字:action。

// 基于以上思考我们设计形如以下的action
var action = {
    name: 'increase', // 场景名称
    data: 2 // 场景发生时生成的数据
}

好了我们有了一个全新的概念action,让我们来根据action设计reducer

// 还是这个initState,不过他更复杂了
var initState = {
    name: 'lemon',
    age: 25,
    favor: {
        ball: {
            basketBall: 'well',
            footBall: 'good'
        },
        TV: {
            bilibili: 'usually',
            aqiyi: 'never'
        }
    }
};

function reducer(state = initState, action) {
    switch(action.name) {
        case 'change_user_name': 
            state.name = action.data;
            return state;
        
        case 'increae_age':
            state.age += 1;
            return state;

        case 'update_favor_ball_baskteBall':
            state.favor.ball.basketBall = action.data;
            return state;
        ...
        default:
            return state;
    }
}

引入了场景+对象结构描述的概念,使得我们可以更加语义的处理数据的变更,多层级的结构也可以在各种场景下更加细粒度的去改变。来吧,我都迫不及待的想去改造createStore了,完整代码如下

// 写入createStore.js
function createStore(reducer, initState = {}) {
    
    if (typeof reducer !== 'function') {
        throw new Error('reducer must be function')
    }
    let store = initState;

    function getStore() {
        return store;
    }

    function dispatch(action) {

        if (typeof action.name === 'undefined') {
            throw new Error('format of action must exist name and data');
        }

        store = reducer(action)
    }

    // 此处初始化store
    dispatch({ name: 'init'});

    return {
        getStore,
        dispatch
    }
}

让我们来试试看新createStore有多厉害

         var initState = {
            name: 'lemon',
            age: 25,
            favor: {
                ball: {
                    basketBall: 'well',
                    footBall: 'good'
                },
                TV: {
                    bilibili: 'usually',
                    aqiyi: 'never'
                }
            }
        };

        function reducer(state = initState, action) {
            switch (action.name) {
                case 'change_user_name':
                    state.name = action.data;
                    return state;

                case 'increae_age':
                    state.age += 1;
                    return state;

                case 'update_favor_ball_baskteBall':
                    state.favor.ball.basketBall = action.data;
                    return state;

                default:
                    return state;
            }
        }

        function createStore(reducer, initState = {}) {

            if (typeof reducer !== 'function') {
                throw new Error('reducer must be function')
            }
            let store = initState;

            function getStore() {
                return store;
            }

            function dispatch(action) {

                if (typeof action.name === 'undefined') {
                    throw new Error('format of action must exist name and data');
                }

                store = reducer(store, action)
            }

            // 此处初始化store
            dispatch({ name: 'init' });

            return {
                getStore,
                dispatch
            }
        }

        const store = createStore(reducer, initState);

        store.dispatch({
            name: 'change_user_name',
            data: 'lemon zhang'
        });
        store.dispatch({
            name: 'increae_age'
        });
        store.dispatch({
            name: 'update_favor_ball_baskteBall',
            data: 'no good'
        });
        const data = store.getStore();
        console.log(data.name); // lemon zhang
        console.log(data.age); // 26
        console.log(data.favor.ball); // test2.html:92 {basketBall: "no good", footBall: "good"}

一切显得多么的优雅,至少现在来看他是能用起来的了,可以这么说到这里已经是Redux设计的最基础的地方。好了,我又开始把上面的代码投入到使用中去了。继续发现了一些问题。项目非常大,如果所有的涉及到数据状态管理的地方都放在一个Reducer中,那么将会臃肿不堪无法维护,那么还是采取按模块去组织我们的Reducer,那么就会有许许多多的Reducer,这块怎么设计呢?

// 先拆分Reducer 妈妈快看 我们被拆分了。
// author.js
var author = {
    name: 'lemon',
    age: 25,
};

function Authorreducer(state = author, action) {
    switch(action.name) {
        case 'change_user_name': 
            state.name = action.data;
            return state;
        
        case 'increae_age':
            state.age += 1;
            return state;

        default:
            return state;
    }
}

// authorFavor.js
var myfavor = {
    favor: {
        ball: {
            basketBall: 'well',
            footBall: 'good'
        },
        TV: {
            bilibili: 'usually',
            aqiyi: 'never'
        }
    }
}
function MyFavorreducer(state = myfavor, action) {
    switch(action.name) {
        case 'update_favor_ball_baskteBall':
            state.favor.ball.basketBall = action.data;
            return state;

        default:
            return state;
    }
}

按照模块化开发的原则 上面的代码可以分为2个JS文件,后面提到的话别懵逼,记得是这两个东西就好

接下来需要设计一个管理多个Reducer的方法,让他能无缝跟原有的createStore中去使用,我们给这个方法起名为combineReducer吧

// 写入combineReducers.js文件
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)

  // 这里是关键,返回一个Reducer
  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)
      
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}

原理比较简单,以key + Reducer的映射关系组织多个Reducer,最后只返回一个Reducer,dispacth的时候执行一个FOR循环,最后判断前后是否改变决定是否返回一个新数据还是旧数据的state。再来看看我们怎么使用combineReducers吧

import AuthorReducer from './author.js'
import AuthorFavorReducer from './authorFavor.js'
import createStore from './createStore.js';
import combineReducers from './combineReducers.js'
const store = createStore(combineReducers({
    author: AuthorReducer,
    favor: AuthorFavorReducer
}));
store.dispatch({
    name: 'change_user_name',
    data: 'lemon zhang'
});
store.dispatch({
    name: 'increae_age'
});
store.dispatch({
    name: 'update_favor_ball_baskteBall',
    data: 'no good'
});
const data = store.getStore();
console.log(data.name); // lemon zhang
console.log(data.age); // 26
console.log(data.favor.ball); // test2.html:92 {basketBall: "no good", footBall: "good"}

一切简直太完美了,Enjoy这段旅程吧。当我们更优雅的组织Reducer后,项目中还有个问题,就是形形色色的dispatch充斥着各个模块的代码中,要知道,各种各样的action name直愣愣的就是魔术字符串啊,一旦我们要改动一个action name,项目中就有几十个相同名字的action name需要被改动和替换。实在难受。怎么解决呢?首先我们需要统一管理我们的action了,首先用常量去管理各类action name取代魔术字符,再引入工厂模式actionCreator去生成各类的action。

// 以下代码写入bindActionCreator.js
const INCREASEAGE = 'increae_age';
const CHANGENAME = 'change_user_name';
const UPDATEBALL = 'update_favor_ball_baskteBall'

function makeIncreaseAgeActions() {
    return {
        name: INCREASEAGE
    }
}

function makeChangeNameActions(name) {
    return {
        name: CHANGENAME,
        data: name
    }
}
function makeUpdateBasketBallActions(status) {
     return {
        name: UPDATEBALL,
        data: status
    }
}

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

最后我们再来看看我们的代码变啥样了。

import AuthorReducer from './author.js'
import AuthorFavorReducer from './authorFavor.js'
import createStore from './createStore.js';
import combineReducers from './combineReducers.js'
import {makeIncreaseAgeActions, makeChangeNameActions, makeUpdateBasketBallActions, bindActionCreator} from './bindActionCreator';

const store = createStore(combineReducers({
    author: AuthorReducer,
    favor: AuthorFavorReducer
}));

const changeName = bindActionCreator(makeChangeNameActions, store.dispatch);
const increaseAge =  bindActionCreator(makeIncreaseAgeActions, store.dispatch);
const updateBasketBall =  bindActionCreator(makeUpdateBasketBallActions, store.dispatch);

// 以下这些方法可以导出到任意地方使用 更加语义化
changeName();
increaseAge();
updateBasketBall();

const data = store.getStore();
console.log(data.name); // lemon zhang
console.log(data.age); // 26
console.log(data.favor.ball); // test2.html:92 {basketBall: "no good", footBall: "good"}

走到这里,以上代码已经是能完全做到合理并且优雅的管理我们的数据状态了,但是目前他还不够有趣,我们想让他能在更新完数据后进行通知,这样收到通知的接入方就可以拿去干更多有趣的事了,比如更新了redux拿去更新视图,也可以拿去处理其他的业务逻辑,这都是相当好的。我们引入观察者模式,使store拥有被观察的特性。来吧,一起改造createStore。

// 重新写入createStore.js
function createStore(reducer, initState = {}) {
    
    if (typeof reducer !== 'function') {
        throw new Error('reducer must be function')
    }
    let store = initState;
    const listeners = []; // 监听者队列

    function getStore() {
        return store;
    }

    function subscribe(fc) {
        listeners.push(fc);

        return function unSubscribe() {
            listeners.splice(listeners.indexOf(fc), 1);
        }
    }

    function dispatch(action) {

        if (typeof action.name === 'undefined') {
            throw new Error('format of action must exist name and data');
        }

        store = reducer(action);

        listeners.forEach(fc => fc());
    }

    // 此处初始化store
    dispatch({ name: 'init'});

    return {
        getStore,
        dispatch,
        subscribe
    }
}

来看看怎么使用吧

import AuthorReducer from './author.js'
import AuthorFavorReducer from './authorFavor.js'
import createStore from './createStore.js';
import combineReducers from './combineReducers.js'
import {makeIncreaseAgeActions, makeChangeNameActions, makeUpdateBasketBallActions, bindActionCreator} from './bindActionCreator';

const store = createStore(combineReducers({
    author: AuthorReducer,
    favor: AuthorFavorReducer
}));

store.subscribe(() => {
    console.log('更新完成啦', store.getStore())
})
const changeName = bindActionCreator(makeChangeNameActions, store.dispatch);
const increaseAge =  bindActionCreator(makeIncreaseAgeActions, store.dispatch);
const updateBasketBall =  bindActionCreator(makeUpdateBasketBallActions, store.dispatch);

// 以下这些方法可以导出到任意地方使用 更加语义化
changeName();
increaseAge();
updateBasketBall();

我们的Redux开始有趣起来了,但是还不够有趣,我们需要提供一种中间件机制,让他完全开放,让dispatch触发action到最终改变store的过程中去做更多的事情,怎么处理呢。还是回到createStore,将提供第三个参数接入中间件机制。注意,这块中间件机制是Redux中最难理解的部分,需要一步步理解。

// 重新写入createStore.js
function createStore(reducer, initState = {},  enhancer) {
    
    // 此处接入中间件将整个createStore reducer initState传入
    if (typeof enhancer !== 'undefined') {

        if (typeof enhancer !== 'function') {
            throw new Error('Expected the enhancer to be a function.')
        }

        // 中间件核心
        return enhancer(createStore)(reducer, initState)
    }

    if (typeof reducer !== 'function') {
        throw new Error('reducer must be function')
    }
    let store = initState;
    const listeners = []; // 监听者队列

    function getStore() {
        return store;
    }

    function subscribe(fc) {
        listeners.push(fc);

        return function unSubscribe() {
            listeners.splice(listeners.indexOf(fc), 1);
        }
    }

    function dispatch(action) {

        if (typeof action.name === 'undefined') {
            throw new Error('format of action must exist name and data');
        }

        store = reducer(store, action);

        listeners.forEach(fc => fc());
    }

    // 此处初始化store
    dispatch({ name: 'init'});

    return {
        getStore,
        dispatch,
        subscribe
    }
}

我们将createStore reducer initState全部交出,让enhancer自己去玩,背后的设计比较复杂涉及函数式编程思想,不急我们一步步来。先来理解COMPOSE

function compose(funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

reduce谁都认识吧?不认识麻烦去MDN查看文档此处不再叙述哈。实在看得头疼看看以下代码方便理解

function add1(a) {
    return a + 1
}
function add2(a) {
    return a + 2
}
function add3(a) {
    return a + 3
}
const add = compose([add1, add2, add3]);
add(5) === add1(add2(add3(5)))

实在难以理解就要断点去看看啦~接着我们继续 看怎么把所有的中间件组织起来

function applyMiddleware(middlewares) {

  // 返回一个持有createStore的函数
  // 这也是createStore里return enhancer(createStore)(reducer, initState)的设计
  return createStore => (...args) => {
    const store = createStore(...args) // 根据传入的reducer initState生成store

    let dispatch = () => {

      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    // 核心API注入到每个中间件去
    const middlewareAPI = {
      getStore: store.getStore,
      dispatch: args => dispatch(args)
    }

    // 注入每一个中间件 并且返回函数数组 注意 这就要求中间件的写法必须middlewareAPI => args => {// do something}
    const chain = middlewares.map(middleware => middleware(middlewareAPI))

    // 将dispatch放入中间件的链式调用中去 返回一个新的dispatch
    // 这要求compose(chain)返回一个形如 dispatch => (...arags) => {}
    // 所以我们反推回去middleware的正确写法需要形如middlewareAPI => dispatch => args => {// do something}
    dispatch = compose(chain)(store.dispatch)

    // 返回一个store,注意dispatch已经是新的dispatch了
    // 每次执行dispatch将会触发链式的中间件
    return {
      ...store,
      dispatch
    }
  }
}

对于这段核心的方法已经解析完毕,不清楚的话要多看几遍了 基本每段都有注释。让我们开始写中间件试试水吧。先写一个redux-log中间件吧 每次执行都打印日志 方便追踪。

const reduxLog = middlewareAPI => dispatch => args => {
    console.log('log 中间件观察到了action -> ', args);
    dispatch(args);
}

是不是十分简单呢,来测试一下最终效果吧

import {AuthorReducer, AuthorFavorReducer, initState} from './reducer.js'
import combineReducers from './combineReducers.js'
import {makeIncreaseAgeActions, makeChangeNameActions, makeUpdateBasketBallActions, bindActionCreator} from './bindActionCreator';
import {reduxLog} from './middleware';
import applyMiddleware from './applyMiddleware';
import compose from './compose';

const store = createStore(combineReducers({
    author: AuthorReducer,
    favor: AuthorFavorReducer
}), initState, compose(applyMiddleware([reduxLog])));

store.subscribe(() => {
    console.log('更新完成啦', store.getStore())
})
const changeName = bindActionCreator(makeChangeNameActions, store.dispatch);
const increaseAge =  bindActionCreator(makeIncreaseAgeActions, store.dispatch);
const updateBasketBall =  bindActionCreator(makeUpdateBasketBallActions, store.dispatch);

// 以下这些方法可以导出到任意地方使用 更加语义化
changeName();
increaseAge();
updateBasketBall();

看看效果吧

总结

行文至此,已经将redux解析完毕。通过从设计思想 假设实现 逐渐优化和改进。从每一个细节处感受Redux的设计。最后我们再来看看官网Redux的结构组织是怎样的。

可以看出来别无二致,但是官网Redux还是有诸多的细节,比如参数检测等等 本文就没有一一分析了,相比网上许多的源码解析文章话不多说先一通解析,笔者觉得知其然更要知其所以然。如果我的文章有帮助到你,别忘记点个赞好吗? PS(本文所有代码可在github仓库mini-redux中找到),别忘了点个星哦~

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改