背景
项目需要将功能从web端迁移到支付宝小程序端,其中逻辑层是相似的,可以直接迁。但是其中用到的pinia找不到相应的组件,使用global没有setData之类的限制,到处能用赋值语句进行修改的话,页面会很乱,于是有自己写一个类似pinia这样的需求,在此记录一下 github地址可点击
使用方式
首先明确想要的使用方式,大致如下
1、定义一个工具
2、通过data将变量注入页面
3、返回一个变量,可以在js/ts中拿到该store所有的action、getter、state
希望能实现如下使用
在store/module下定义一个工具
import {defineStore} from '../index'
export const useTest = defineStore('textStore',{
state(){
return {
firstName:'first',
lastName:'last'
}
},
getter:{
name(){
return this.firstName + this.lastName
}
},
action:{
setFirstName(data){
this.firstName = data
}
},
})
页面中引用
index.axml
<view class="blank-page">
<view>
<text>{{textStore.name}}</text>
</view>
<button onTap="handerClick" size="default" type="primary">setNewName</button>
</view>
index.js
import {useTest} from '/store/module/test'
// 返回变量会挂载一些函数 不知道data编译的底层逻辑,为避免出错就不给他赋值函数了
let testStore = useTest()
Page({
data: {
SDKVersion: '',
textStore:{}
},
onLoad(query) {
testStore = useTest.call(this)
// 页面加载
console.info(`Page onLoad with query: ${JSON.stringify(query)}`);
},
onReady() {
// 页面加载完成
},
onShow() {
},
onUnload() {
// 页面被关闭
},
handerClick(){
testStore.setFirstName('newFirst')
}
});
相关效果如下
基本实现
reactive和readonly
需要知道的是readOnly返回值后续导出对象,读取操作基本来源它,而reactive更主要是在action中承担了修改值的角色
function reactive(data) {
for (let key in data) {
if (Object.prototype.toString.call(data[key]) === '[object Object]') {
data[key] = reactive(data[key]);
}
}
return new Proxy(data, {
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
// 将收集到的watcher逐一遍历执行
if(easyWatcher[key]){
easyWatcher[key].array.forEach(fn => {
fn()
});
}
// 执行setData之类的操作,让页面能实时响应
resetStore()
return true;
},
});
}
function readOnly(data) {
for (let key in data) {
if (Object.prototype.toString.call(data[key]) === '[object Object]') {
data[key] = readOnly(data[key]);
}
}
return new Proxy(data, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 收集当前getter中使用了的依赖
if(setGetter){
if(easyWatcher[key]){
easyWatcher[key] = new Set([setGetter])
}else{
let setData = easyWatcher[key]
setData.add(setGetter)
}
}
if (typeof result === 'function' && target.hasOwnProperty(key)) {
setGetter = result.bind(target)
// 标记好状态开始收集getter
const resultData = result.call(target);
setGetter = false
return resultData;
}
return result;
},
set() {
throw new Error(
storeName
? `store:${storeName}不能通过外部修改`
: `strore不可通过外部修改`,
);
},
});
}
state、getter、action处理
接下来处理state、action、getter
const { state: stateFn, action: actionData, getter: getterData } = config;
const state = typeof stateFn === 'function' ? stateFn() : stateFn;
期待在外部使用时可以在同一个对象上访问state,getter和action,于是做如下处理。
const getter = getterData || {};
// new StoreExportFn实例时将state和getter属性都挂载上
function StoreExportFn() {
for (let key in state) {
this[key] = state[key];
}
for (let key in getter) {
this[key] = getter[key];
}
}
const action = actionData ? createAction(actionData) : {};
// 将action中的方法挂载到要代理的对象上
StoreExportFn.prototype = Object.create(action);
const storeExport = new StoreExportFn();
const stateProxy = reactive(storeExport);
const stateReadOnly = readOnly(storeExport);
createAction则是简单的将用户声明的所有action挂载上去,同时修改下this指向
function createAction(config) {
const data = {};
for (let key in config) {
if (key === 'state') {
console.error('state为保留字段,请重新命名');
continue;
}
const actionItem = config[key];
data[key] = async function (data) {
return await actionItem.call(stateProxy, data);
};
}
return data;
}
页面响应式挂载
写上resetStore
const resetStore = (instance?) => {
// 挂载数据到页面
if (instance) {
instance.setData({
[storeName]: stateReadOnly,
});
} else {
for (let [key, pageInstance] of pageMap) {
pageInstance.setData &&
pageInstance.setData({
[storeName]: stateReadOnly,
});
}
}
};
其中为if分支为页面上调用时创建的,页面初始化会调用如下useStore
function useStore() {
// 页面上取值的名字 多个页面都要搞
if (this && this.setData && !pageMap.has(this.$id)) {
pageMap.set(this.$id, this);
}
this && this.setData && resetStore(this);
return stateReadOnly;
}
以上只是初步实现,使用中还会面临诸如以下问题
存在问题
ps:git中已解决当下问题
1、若对象的子属性存在形同的属性
文中只是以key值区分watcher,当存在如下结构时,这种简单的方式则无法满足
...
state:{
baby:{
name:'babyOne',
wife:{
name:'babyTwo'
}
}
}
...
如上,name存在两个,我们的处理方式是利用一个bucket的map对象存储对应的target,再用map存对应的key,接着把key对应的函数存成一个set放在其中 具体思路可以参考《vuejs设计与实现》第四章第三节:设计一个完善的响应系统
2、getter未缓存,重复运行
getter由于不知小程序内部运作,可能会多次读取执行。
此处采用类似computed的dirty方式解决,例如上述的name中,当前只当this.firstName或this.lastName发生改变时才会执行getter内容
3、easyWatcher的Set其实会存在重复问题
setGetter = result.bind(target)这操作在每次重新赋值时都是一个新的函数,在set中使用实质上没有意义,因为这块一直是新值,我们可以尝试在挂载getter时修改函数的this指向
function StoreExportFn() {
for (let key in state) {
this[key] = state[key];
}
for (let key in getter) {
// 绑定this指向
this[key] = getter[key].bind(this)
}
}
小插曲:
如下所示,我们如下设置页面js
let data = {
name:'111',
key:'ccc'
}
const dataP = new Proxy(data,{
get(target,key,receiver){
console.log('kkkey',key);
return Reflect.get(target,key,receiver)
}
})
Page<{
dataP:any
},{
handerClick:()=>void
}>({
data: {
textStore:{},
dataP
},
handerClick(){
this.setData({
'dataP.name':'xiix'
})
}
});
<view class="blank-page">
<view>
<!-- <text>{{textStore.name}}</text> -->
<text>{{dataP.name}}</text>
</view>
<button onTap="handerClick" size="default" type="primary">setNewName</button>
</view>
未点击时打印如下
点击后打印如下
也就是在值更新时,页面会对所有值进行读值操作,若业务上存在影响,需要注意一下