如何在小程序中使用redux toolkit

本文的主旨在于讨论如何在原生小程序中实现状态管理,在开始之前,你需要了解下,redux toolkit, 以及到底,你是否需要接入它?

如果你确实需要用它来管理状态,那我们开始吧。

准备出发

我们知道redux是一个独立的状态管理库, 可以与其他描述的ui框架一起使用,例如react , vue , 简单回顾一下

以最简单的counter为例

counterSlice.js

ts 复制代码
import { createSlice } from '@reduxjs/toolkit';

// 创建一个名为 "counter" 的 Redux Slice
export const counterSlice = createSlice({
  name: 'counter', // Slice 的名称
  initialState: {
    value: 0, // 初始状态值
  },
  reducers: {
    // 增加操作
    increment: (state) => {
      state.value += 1;
    },
    // 减少操作
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

// 导出 action creators,即 reducers 中定义的操作
export const { increment, decrement } = counterSlice.actions;


// 下面的函数称为 "selector",允许我们从状态中选择一个值。
// 选择器也可以在使用它们的地方内联定义,而不是在 Slice 文件中定义。
// 例如:`useSelector((state) => state.counter.value)`
export const selectCount = (state) => state.counter.value;

// 导出 reducer,用作 Store 中的一个 reducer
export default counterSlice.reducer;

创建store并使用

ts 复制代码
import { configureStore  } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

store.subscribe(() => {
  const state = store.getState();
  console.log('宝宝订阅了 {number} 的变化', state.counter)
})


// action1  加一
store.dispatch(increment())
// action2  减1
store.dispatch(decrement())



// 以下是打印的结果

宝宝订阅了 {number} 的变化 {value: 1}
宝宝订阅了 {number} 的变化 {value: 0}

我们使用subscribe来订阅了状态的变化 , 当我们 dispatch 某个动作时, 订阅者将会收到通知。 从而更新视图或者其他操作

Give me a more concrete example ???

当然ok

还是上面的计数器例子,这次我们要将它跟我们的视图结合起来, 我们使用 Parcel 来搞个简单的例子

我们在本地安装parcel

yarn add --dev parcel

新建如下结构

ts 复制代码
demo
├─ package.json
├─ src
│  ├─ counterSlice.mjs
│  ├─ index.html
│  ├─ render.mjs
│  ├─ store.mjs
│  └─ templateEngine.mjs
└─ yarn.lock

这里只需要关注 render.mjs 文件。 他长这样

js 复制代码
// 1) 引入创建的store, 并且有个counter的领域slice
import store from "./store.mjs";
import { increment, decrement } from './counterSlice.mjs'

// 2) 订阅 store 更新
store.subscribe(render);

const valueEl = document.getElementById('value');

// 3. 当订阅回调运行时:
function render() {
    // 3.1) 获取当前 store 状态
    const state = store.getState();

    // 3.2) 提取你需要的数据 selectorXXX
    const newValue = state.counter.value;

    console.log('selectorCounter', newValue)
    // 3.3) 使用新值更新 UI
    valueEl.innerHTML = newValue;
}

// 4) 显示初始 UI,根据初始 store 状态
render();


// 5) 根据用户界面输入分发操作
document.getElementById("increment")
    .addEventListener('click', () => {
        store.dispatch(increment())
    });
document.getElementById("decrement")
    .addEventListener('click', () => {
        store.dispatch(decrement())
    });

当我们运行 yarn parcel src/index.html 时

每当我们点击新增1, 减少1按钮时, 中间的value部分 UI将会变更, 你可以把这个例子看作是 redux 与 UI 层进行一个结合的最简单例子。

当然,不止于此,我提供了一个更为复杂的模版引擎的例子,你可以下载运行试试,这个不是重点demo.zip

事实上 每个 Redux UI 集成层只是这些步骤的一个更高级的版本。

以我们熟知的react-redux connect为例, 它用于将redux状态跟我们的组件关联起来, 它通过mapStateToProps 这个函数从store里面提取组件需要的数据 ,从mapDispatchToProps 获取到dispatch, 从而可以派发动作来更新状态。 👀 进而自动更新新的视图

我向chatGPT询问了connect的设计思想后, 它给出了一个简单的实现,如下

js 复制代码
function connect(mapStateToProps, mapDispatchToProps) {
  // 返回一个高阶组件
  return function (WrappedComponent) {
    // 返回一个新的组件类
    return class Connect extends React.Component {
      constructor(props, context) {
        super(props, context);
        // 获取 Redux store
        this.store = context.store;
        // 初始化组件的 state
        this.state = mapStateToProps(this.store.getState());
      }

      componentDidMount() {
        // 订阅 Redux store 的变化
        this.unsubscribe = this.store.subscribe(this.handleChange);
      }

      componentWillUnmount() {
        // 取消订阅 Redux store
        if (this.unsubscribe) {
          this.unsubscribe();
        }
      }

      handleChange = () => {
         // 当 Redux store 的状态变化时,更新组件的 state
        const newProps = mapStateToProps(this.store.getState());
        
        // 比较新旧数据是否相同
        if (!shallowEqual(this.state, newState)) {
          this.setState(newState);
        }
 
      };

      render() {
        // 将 Redux store 的 state 和 dispatch 作为 props 传递给 WrappedComponent
        const { dispatch } = this.store;
        const mergedProps = {
          ...this.props,
          ...this.state,
          dispatch,
        };

        return <WrappedComponent {...mergedProps} />;
      }
    };
  };
}

并给出了原理说明

js 复制代码
`connect` 函数的原理是通过高阶组件(Higher Order Component,HOC)的方式,将 React 组件与 Redux store 连接起来,实现数据的同步和重新渲染。

以下是 `connect` 函数的基本原理:

1. `connect` 函数接受两个参数:`mapStateToProps` 和 `mapDispatchToProps`。这两个参数决定了如何将 Redux store 中的状态映射到 React 组件的 `props` 上。

2. `connect` 函数返回一个函数,这个函数接受一个 React 组件作为参数(通常是要连接到 Redux store 的组件),并返回一个新的组件类。

3. 在新的组件类(通常称为 `Connect`)的 `constructor` 方法中,获取了 Redux store 的引用,并初始化了组件的 `state`。这个初始的 `state` 是通过调用 `mapStateToProps` 函数来从 Redux store 中获取的。

4. 在新组件 `Connect` 的 `componentDidMount` 生命周期中,订阅了 Redux store 的变化,即通过 `this.store.subscribe(this.handleChange)` 来监听 Redux store 的更新。这样,当 Redux store 中的状态发生变化时,会触发 `handleChange` 方法。

5. 在 `handleChange` 方法中,通过再次调用 `mapStateToProps` 函数来获取最新的 Redux store 状态,并将其更新到组件的 `state` 中。

6. 在 `render` 方法中,`Connect` 组件将 Redux store 的 `state` 和 `dispatch` 作为 `props` 传递给了被包装的 React 组件(通常称为 `WrappedComponent`)。这样,`WrappedComponent` 就可以访问 Redux store 中的数据,并通过 `dispatch` 方法触发 action。

7. 最终,`Connect` 组件返回了 `WrappedComponent`,这样,`WrappedComponent` 就具备了访问 Redux store 中数据和触发 action 的能力。

通过这种方式,`connect` 函数实现了 React 组件与 Redux store 的连接,使得 Redux 中的状态变化可以自动同步到 React 组件的 `props` 中,从而实现了数据的同步和重新渲染。

总结:`connect` 函数的原理是通过高阶组件,在组件的生命周期中监听 Redux store 的变化,将 Redux store 中的数据映射到组件的 `props` 上,实现了 React 组件与 Redux store 的连接。这样,组件可以访问 Redux 中的数据并触发 action,实现了数据的同步和重新渲染。

已经说的很棒了, Dan 老师之前也写过一篇文章解释过 connect.js, 可以点击这里查看

so ? 似乎与标题脱节了, 别急, 铺垫了这么多,一是为了回顾基本的 API 用法, 二则是借鉴经典的设计,有了这两点,我们来开始正文

正文来了

我们的目标是在小程序中使用状态管理工具 redux, 这其实非常简单。 在开始之前我还是想回顾一下小程序,小程序作为一个新的开发者应用载体,在2023年的今天我们可以说无时无刻不在使用,从微信,到支付宝,再到涂鸦中的一个个设备控制小程序。 尤其是涂鸦智能家居体系,真的很适合小程序。 app就是你的家, 家里的每个设备就对应app里面的一个小程序。

那说起小程序,现在的架构都大差不差,对齐微信, 核心是双线程模型:小程序的渲染层和逻辑层分别由 两个线程管理:视图层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS脚本; 两个线程之间互相通信,并使用虚拟dom来描述界面, 逻辑层向视图层通信传递数据 (setData), 视图层用最新的数据生成新的虚拟dom, 并将差异应用在真正的dom上去, 也就是我们现在常见的数据驱动视图的开发模式。

视图层页面事件触发时, 向逻辑层传递消息, 逻辑层去执行对应页面的方法。

下面是两个线程之间的交互机制

简单说明下, 两个层同时执行,例如 (视图层view.js , 逻辑层service.js)

视图层 -> 逻辑层(page_init)

1.当微信小程序的视图层(Webview)启动后,它会向逻辑层发送一个名为 "page_init" 的消息。

2.逻辑层接收到这个消息后,会创建页面实例,并获取该页面实例的数据(data)。

3.接着,逻辑层会执行页面实例的 onShow 和 onLoad 方法。

逻辑层 -> 视图层(page_render)

1.逻辑层准备好数据后,会向视图层发送一个消息,通知视图层可以开始渲染页面了,并将原始的数据传递给视图层。

2.视图层接收到消息后,根据页面的渲染规则(如 WXML 等),创建虚拟 DOM 树。

3.将虚拟 DOM 树转换为真实的 DOM 节点树,并将其插入到页面的根元素中,从而完成首次渲染。

视图层 -> 逻辑层(page_method)onReady

1.视图层在首次渲染完成后,向逻辑层发送一个消息,通知逻辑层页面已经渲染完毕,并可以执行 onReady 方法。

2.在逻辑层的 onReady 方法中,可能会执行一些异步操作,如获取云端数据。

3.如果逻辑层在 onReady 中执行了数据变更操作,它会调用页面实例的 setData 方法,将最新的数据传递给视图层。

逻辑层 -> 视图层(page_update)

1.当逻辑层调用 setData 方法更新数据后,它会向视图层发送一个消息,通知视图层需要更新页面的数据。

2.视图层接收到 page_update 消息后,会使用最新的数据重新生成虚拟 DOM 树。

3.然后,视图层会将新的虚拟 DOM 树与旧的进行比较,找出需要变化的部分,并进行相应的更新,以保持页面显示的最新状态。

上面大概描述一个简易的通信过程, 可以简单了解,不用在意

这下真的来了

直接开门见山, 如何使用, 还是以counter为例

小程序页面page/counter.tyml

js 复制代码
// 省去了创建,引入store的代码
const { dispatch, subscribe, getState } = store;
Page({
  
  data: { value: 0 },
	
  onLoad() {
  	if (!this.unsubscribe) {
        this.unsubscribe = subscribe(() => {
        	const state = getState();
          	
          	// 提取你需要的数据 selectorXXX
          	const newValue = state.counter.value;
          
          	// 你可能要比较一下提取的最新数据跟之前数据对比是否变了, 不变的话不更新,变了的最好能局部更新(只更新变了的)
          
          	// 调用小程序更新UI的方法
          	this.setData({
            	value: newValue
            })
        });
      }
  },
  
  onUnload() {
      if (this.unsubscribe) {
        this.unsubscribe();
        this.unsubscribe = undefined;
      }
  },
  
  handleIncrement() {
  	dispatch(increment())
  }
})

就是这么简单, 你就几个页面的话这样完全可以, 但是这个过程其实是很重复的, 像 订阅存储、检查更新数据和触发重新渲染的过程可以变得更加通用和可重用, 就像react-redux的connect一样。

createConnectPage

我们仿照react-redux connect api 的设计思路, 创建一个 createConnectPage 的函数, 我们整比较简单点,顾名思义,就是创建连接过的Page , 传入三个参数, pageConfig, mapStateToData , mapStateToMethods , 不用说,都知道什么意思吧

js 复制代码
import { globalStore } from './index';
import getStateFromData from './utils/getStateFromData';
import { shallowEqual } from './utils/shallowEqual';
import { diffData } from './utils/diff';
import { isEmptyObject } from './utils/isEmptyObject';

const defaultMapStateToData = () => ({});
const defaultMapDispatchToMethods = dispatch => ({ dispatch });


export default function createConnectPage(Page) {
    const __OriginalPage = Page;

    return function connectPage(pageConfig, mapStateToData, mapDispatchToMethods, store?) {
        const { subscribe, getState, dispatch } = globalStore || store;
        const shouldSubscribe = Boolean(mapStateToData);
        const finalMapStateToData = mapStateToData || defaultMapStateToData;
        const finalMapDispatchToMethods = mapDispatchToMethods || defaultMapDispatchToMethods;


        function computeMappedState(originalData?) {
            const state = getState();
            const mappedState = finalMapStateToData(state);
            return mappedState;
        }

        return __OriginalPage({
            ...pageConfig,

            data: {
                ...pageConfig.data,
                ...computeMappedState()
            },

            onLoad: function(options) {
                // todo 页面参数传入mapStateToData  
                this.trySubscribe();
                this.bindDispatchMethodsToPage()
                pageConfig.onLoad && pageConfig.onLoad.bind(this)(options);
            },

            onUnload: function() {
                pageConfig.onUnload && pageConfig.onUnload.bind(this)(...arguments);
                this.tryUnsubscribe()
            },

            onReady: function () {
                pageConfig.onReady && pageConfig.onReady.bind(this)(...arguments);
            },

            bindDispatchMethodsToPage() {
                if(typeof finalMapDispatchToMethods === 'function') {
                    const dispatchMethods = finalMapDispatchToMethods(dispatch)
                    for (const methodName in dispatchMethods) {
                        if (dispatchMethods.hasOwnProperty(methodName)) {
                            // 绑定每个 dispatch 方法到页面实例
                            this[methodName] = dispatchMethods[methodName];
                        }
                    }
                }
            },

            handleChange() {
                const mappedState = computeMappedState();
                const currentState = getStateFromData(mappedState, this.data);
        
                console.log('新的mappedState', mappedState);
                console.log('旧的state',  currentState);
                console.log('diffData', diffData(mappedState, currentState))
                // 比较旧的从redux映射过来state,跟最新的selector state , 如果有差异,则setData局部更新
                // diffData 引用微信官网westore diffdata
                if(!shallowEqual(mappedState, currentState)) {
                    const patch = diffData(mappedState, currentState)
                    if(!isEmptyObject(patch)) {
                        this.setData(patch);
                    }
                }
            },


            trySubscribe() {
                if(shouldSubscribe && !this.unsubscribe) {
                    this.unsubscribe = subscribe(this.handleChange)
                }
            },

            tryUnsubscribe() {
                if (this.unsubscribe) {
                  this.unsubscribe();
                  this.unsubscribe = null;
                }
              }
        })
    }
}

globalStore

js 复制代码
export let globalStore = null;
export function setGlobalStore(store) {
    globalStore = store;
}

整体比较简单,就是扩展和修改页面的配置对象,在页面加载的时候订阅store的更新, 并且将mapDispatchToMethods返回的方法挂在在this, 也就是当前页面的实例上面,供其他页面方法调用或者在wxml中调用, 在页面卸载的时候取消监听

每当我们更新store数据时,我们在handleChange方法中获取到最新的state并根据选择器函数mapStateToData去选择当前页面需要的状态, 并且在这里做了一个diffData的操作,将差异使用页面实例的setData方法来更新,从而最小化的更新页面

值得注意的是,diffData采用微信官方提供的westore里面的diff函数,专门用于深比较两个对象,然后找出布局变更的数据,也算是一个小优化,当然,一些场景可能并不需要。

局部更新数据参考智能小程序 运行时优化(setData、事件)

createConnectComponent

这个跟上面代码差不多,因为组件跟页面的配置不太一样,所以单独抽了出来,感兴趣可以看模版代码

如何使用

www.npmjs.com/package/min...

github地址: github.com/baohuse/min...

相关推荐
陈随易42 分钟前
农村程序员-关于小孩教育的思考
前端·后端·程序员
云深时现月43 分钟前
jenkins使用cli发行uni-app到h5
前端·uni-app·jenkins
昨天今天明天好多天1 小时前
【Node.js]
前端·node.js
2401_857610031 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
雾散声声慢1 小时前
前端开发中怎么把链接转为二维码并展示?
前端
熊的猫1 小时前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
天农学子1 小时前
Easyui ComboBox 数据加载完成之后过滤数据
前端·javascript·easyui
mez_Blog1 小时前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪1 小时前
vue文本高亮处理
前端·javascript·vue.js
开心工作室_kaic2 小时前
ssm102“魅力”繁峙宣传网站的设计与实现+vue(论文+源码)_kaic
前端·javascript·vue.js