自制react-redux艺术行为分析

402 阅读4分钟

前言

    作为一名合格的前端童鞋,应该对redux、vuex之类的状态管理插件耳熟能详。自react16.8引入hooks特性后,博主一直有利用react自身特性实现redux的想法!本文主要探究如何利用react自身特性去实现redux相关功能,欢迎点评!

正文

1 redux解决了什么问题

    我想很多人关于这个问题的答案是:状态管理、数据视图分离。也对,redux的确解决了状态管理的问题,而且做到了数据视图分离。那么,将数据和视图分离究竟是为了什么?我想会有许多人答便于维护、规范化云云,关于这些答案,个人认为,数据视图分离大多数场景会带来更多的维护上的开销,而且团队没有同一编写规范的话,代码只会显得更加凌乱不堪!实际上,分离数据视图的主要动机是为了作数据/状态共享!再看下官方对redux的定义:Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。细想一下,官方定义这种state特性其实vux/react内部已经具备了的,比如vue或react都支持组件状态以及通过props控制子组件状态。所以说,redux的核心还是在于数据/状态共享,可以让组件之间共享状态,避免为了共享状态而持续状态提升。

    而react context也恰好具备作数据/状态共享的特性,看下context的官方定义:Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

2 利用context实现redux功能

2.1 基本思路以及带来的两个问题

    将context值托管给外部hooks管理(contextHook),并将context provider注册到应用根组件上,当消费者通过消费行为更改contextHook状态时, context provider和context consumer对应组件会更新。但是这样做会带来以下问题:

(1)当更改contextHook状态时,context provider组件直接子组件也会更新,导致额外的组件更新;

(2)使用多个context,需要显式在根组件之上反复注册,状态都声明在一个文件中,比较麻烦,类似:

const [value1, dispatch1] = useReducer(reducer, {})
const [value2, dispatch2] = useReducer(reducer, {})
<Context1.Provider value={{vlaue1, dispatch1}}>
    <Context2.Provider value={{vlaue2, dispatch2}}>
       <div>Root</div>
    </Context2.Provider>
</Context1.Provider>
2.2 问题一解决思路

    可以使用React.memo和高阶组件开启组件记忆功能,示例如下:

// 根组件
const Root = memo((props) => <div>{{props.name}}</div>)
const wrapRoot = () => {
    return (props) => {
        const [value1, dispatch1] = useReducer(reducer, {})
        return (
             <Context1.Provider value={{vlaue1, dispatch1}}>
                <Root {...props}/> 
            </Context1.Provider>
        )    
    }
}
2.3 问题二解决思路

    利用高阶组件特性进行抽象化封装,示例如下:

const wrapStore = (Comp: React.ComponentType, contextName: string) => {
    return memo((props: any) => { //这里用memo可以防止context provider之间相互影响
        const initial = {}
        const context = React.createContext(contextName, initial)
        const [value, dispatch] = useReducer(reducer, initial)
        const Child = memo(Comp)
        return (
            <Context.Provider value={value}>
                <Child {...props} />
            </Context.Provider>
        )
    })
}
const Index = wrapStore(wrapStore(()=> <div>Root</div>,'store1'), 'store2')
// app.tsx
<Index/>

3 模块化方案

    鉴于redux有模块化方案,而且的确对开发带来了便利,因此加入模块化功能!下面讲解基本思路:     第一步,利用闭包将需要用作状态管理的context集中管理起来,context的获取必须通过闭包。示例如下:

export const contextFactory = (() => {
    const contextObj: { [module: string]: React.Context<any> } = {}
    return {
        getContext: (module: string) => {
            if (!(module in contextObj)) {
                contextObj[module] = React.createContext({})
            }
            return contextObj[module]
        },
    }
})()

    第二步,将contextHook、模块名、初始值以及reducer用特殊数据结构做声明,然后存储到context闭包作用域下,便于后续获取context相关信息。

export interface StoreType<T> {
    name: string
    entryHook: Function
    initial: T
    reducers?: {
        [key: string]: (state: any, payload: any) => any
    }
}
export const contextFactory = (() => {
    const contextObj: { [module: string]: React.Context<any> } = {}
    const contextDataObj: { [key: string]: StoreType<any> } = {}
    return {
        getContextData(name: string) {
            return contextDataObj[name]
        },
        getContextDataObj() {
            return contextDataObj
        },
        registerContextData(store: StoreType<any>) {
            if (Object.values(contextDataObj).includes(store)) return store
            if (Object.keys(contextDataObj).includes(store.name))
                throw Error(`store ${store.name} has existed!`)
            contextDataObj[store.name] = store
            return store
        },
        getContext: (module: string) => {
            if (!(module in contextObj)) {
                contextObj[module] = React.createContext({})
            }
            return contextObj[module]
        },
    }
})()

    第三步,改造高阶函数,应用上第二部声明的闭包

const wrapStore = (Comp: React.ComponentType, name: string) => {
    const Context = contextFactory.getContext(name)
    const store = contextFactory.getContextData(name)
    return memo((props: any) => {
        console.log(`withContext ${name} rerendered`)
        const state = store.entryHook()
        return (
            <Context.Provider value={state}>
                <Comp {...props} />
            </Context.Provider>
        )
    })
}

export const withContext = (
    EntryComp: React.ComponentType,
    storeObj: { [key: string]: StoreType<any> }
) => {
    if (
        !Object.keys(storeObj).length ||
        !Object.values(storeObj).filter(Boolean).length
    )
        return EntryComp
    Object.values(storeObj).forEach((store) => {
        contextFactory.registerContextData(store)
    })
    let Comp: React.ComponentType = memo(EntryComp)
    const names = Object.keys(contextFactory.getContextDataObj())
    return names.reduce((AccComp, curName) => wrapStore(AccComp, curName), Comp)
}

    最后,使用给定的withContext高阶函数

const store1 = {
    name: 'test1',
    entryHook: useTestStore1,
    initial : {
        count1: 20,
    },
    reducers: {
        add(state) {
            return {...state, count1: state.count1 + 1}
        }
    },
}

function useTestStore1() {
    const {reducer, initial} = yxl_rhs.reducerProvider(store)
    const [state, dispatch] = useReducer(reducer, initial)
    return {
        state,
        dispatch,
    }
}

const store2 = {
    name: 'test2',
    entryHook: useTestStore2,
    initial : {
        count2: 20,
    },
    reducers: {
        add(state) {
            return {...state, count2: state.count2 + 1}
        }
    },
}

function useTestStore2() {
    const {reducer, initial} = yxl_rhs.reducerProvider(store)
    const [state, dispatch] = useReducer(reducer, initial)
    return {
        state,
        dispatch,
    }
}

const Index = () => <>hello,world</>
const App = withContext(Index, {store1, store2})

    提示:推荐在hooks中获取闭包作用域内的context完成订阅

4 库文件

    上述方案已被作者封装成npm库'yxlolxy-open-api',欢迎各位安装试用!

5 国际惯例

    原文链接