具有vue+react开发体验javascript
将dvajs的models状态管理模式, react数据批量更新的特色,以及vue的watch和computed特性, 所有封装为一个小程序的状态管理库,不只实现了小程序的 【全局状态管理】 ,解决跨页通讯,还引入了 【组件圈子】 概念,来实现组件间的数据传递,弥补原生小程序组件系统的先天缺陷,摆脱各类父子、兄弟、姐妹、街坊邻居、七大姑八大姨、远方表兄弟等等组件间的通讯浆糊困扰。html
不论是页面间,仍是组件间,嵌套组件内部,均可以经过简单的dispach来管理全局状态或圈子状态(局部)。 vue
彻底兼容原生代码,已有的业务逻辑代码,即使不适配也可以使用此库,不影响已有业务逻辑。java
//app.js
import {page} from './mpsm/index'
import models from './models/index'
page.init(models, {}, {})
复制代码
第2、三参数分别为页面和组件的公共options,会在每一个页面或组件生效,对于同名的对象和函数,会进行合并处理。react
小:即只需将Page、Component首字母小写。git
尾巴:即尾部多调用一次:github
page({ //component
// ...
})()
复制代码
方法 | 说明 | 备注 |
---|---|---|
page | 用于注册页面 | page()() |
component | 用于注册组件 | component()() |
dispatch | 状态分发 | 参数object, {type, action, lazy}, 默认lazy: true, 懒更新,表示只更新当前展现页面的状态,其它页面待onShow触发后再更新 |
getComponentOps | 获取公共组件配置选项 | 不包含函数,只是简单的JSON格式数据 |
getOps | 获取公共页面配置选项 | 不包含函数,只是简单的JSON格式数据 |
subscribe | 订阅单一数据源 | 参数: 'userInfo/setup' |
unsubscribe | 取消订阅单一数据源 | 参数: 'userInfo/setup' |
方法: unsubscribe, subscribe小程序
取消订阅不使用dva的app.unmodel()
,同时,subscription 必须返回 unlisten 方法,用于取消数据订阅。api
subscribe('userInfo/setup');
unsubscribe('userInfo/setup');
复制代码
import {dispatch, page, component} from '../../mpsm/index'
page({ // 或者 component
watch: {
isLogin(newState, oldState) {
}
},
computed: {
countComputed(data) {
return data.count * 2
}
},
data: {
count: 2
},
onLoad() {},
login() {
dispatch({
type: 'userInfo/save',
payload: {
isLogin: true
}
})
},
changeGroupState() {
this.dispatch({
type: 'group/index-a-1',
payload: {
nameA: 'name'
}
})
},
})(({userInfo}) => {//订阅全局状态
return {
isLogin: userInfo.isLogin
}
}, (groups) => {//订阅圈子状态,page为全局圈子,component为组件所在圈子
return {
nameA: groups.nameA && groups.nameA.name || '--'
}
})
复制代码
dispatch用于分发全局状态,风格与dva保持一致;app
Page和Component实例内置this.dispatch方法,用于分发局部状态。
三、组件可监听page的生命周期函数,无需作版本兼容,只需将想要监听的函数名与page内一直便可,即
// 原生的pageLifetimes可监听的周期太少,且版本要求高,监听了onShow,就不须要监听show了,避免执行两次
component({
pageLifetimes: {
onShow: function () { },
onHide: function () { },
onPageScroll: function () { },
},
})()
复制代码
目前可监听的生命周期 ['onShow', 'onHide', 'onResize', 'onPageScroll', 'onTabItemTap', 'onPullDownRefresh', 'onReachBottom']
有了这个功能,就能够编写不少自控组件了,好比吸顶效果的导航栏等。
export default {
namespace: '',
state: {
},
subscriptions: {
},
effects: {
},
reducers: {
}
}
复制代码
详情参阅 dva
属性 | 说明 | 备注 |
---|---|---|
namespace | 命名空间 | 必须 |
state | 状态 | object |
subscriptions | 单一数据源的订阅,page.init时执行, 暂不具有done参数 | 参数{dispatch, history, select} |
effects | 可进行一些异步操做 | |
reducers | 纯函数 |
详情参阅 dva
export default {
subscriptions: {
setup({dispatch, history, select}) {
const callback = (current, prev) => {
dispatch({
type: 'save',
payload: {
prev, // {route: 'pages/index/index',options: {id: 123}}
current
}
})
}
history.listen(callback)
return () => history.unlisten(callback)
}
}
}
复制代码
对于嵌套组件间的数据通讯,每每存在所谓的父子组件关系,内层组件想要向外层组件或其它分支上的组件传递数据,每每经过外层的组件经过监听函数来接收并派发,层层传递,这种 这种层层转接的模式,繁琐且须要专门的函数去维护,不利于组件的拓展和移植。
而对于小程序的原生组件系统来讲,behaviors, relations, definitionFilter, triggerEvent, getRelationNodes等编写组件须要使用的属性或方法, 真的是辣眼睛!小程序官方组件系统基本丧失了做为组件的意义, 它还在努力地更新升级, 时不时文档中出现"不推荐使用这个字段,而是使用另外一个字段代替,它更增强大且性能更好"
的字眼。
更大更好???我要是没使用过你的这些api,我差点就信了!!!!
什么叫组件?相对独立,具备明确约定的接口,依赖语境,可自由组合,可拓展,可独立部署,亦可复合使用!!!你说你除了相对独立,你说你还有啥,你说你还有个啥!!!
对于一个组件来讲,在最初编写时,不该该去关心本身会被挂载到哪,本身只须要为圈子提供数据, 甚至是数据键名也不用关心,放到圈子里,谁爱用谁用,无需回调函数来协助传递数据, 这才是做为组件可移植可复用的意义,在一个圈子里,不存在父子兄弟的概念,也没有哪一个组件生来是给人当儿子的,组件间的往来只有最纯粹的数据通讯。
<!--组件a in page-->
<a group-name="index-a-1" group-keys="{{ {name: 'nameA1'} }" group-data="{{ {a: 1} }}"></a>
复制代码
只需给组件赋值一个group-name
属性,便给组件分配了一个圈子, 组件内this.dipatch({})
分发地payload
即是该组件给圈子贡献的数据, 也是该组件对外约定的数据值。
group-keys
是用于避免字段名称冲突的,示例中可将a贡献地数据键值更改成nameA1
, 而值不变,因此圈子中地组件很纯粹,只向所在圈子提供必要的数据值, 至于你将组件放置何处,数据给谁用,名称怎么修改,都与我无关。
group-data
是组件所依赖的外部值,是一个内置属性,可在组件的watch
配置中进行监听。
注意: 对配置中的函数,以及setData进行了简单封装,在页面注册或组件注册options的函数中, setData的数据并不会立刻更新,而是合并收集,待当前函数执行完毕后, 先模拟计算出computed,与收集的结果合并,再diff,最后setData一次。 与react更新机制相似,对于函数中出现的异步操做,不会进行数据收集,而是直接模拟计算值,diff,接着setData
对圈子状态的管理拥有最高权限
属性 | 说明 | 备注 |
---|---|---|
$groups | 获取当前页面内的全部圈子状态值 | object, 只读 |
this.$groups['index-a-1']
复制代码
用于强制更新某个圈子中的状态
this.dispatch({
type: 'data/index-a-1',
payload: {
nameA1: 'name'
}
})
复制代码
参数 | 说明 | 备注 |
---|---|---|
type | 格式: 更新类型/圈子名称,更新类型:data 或 group,data表示更新圈子数据的同时,也将payload中的数据更新至this.data, 即this.setData(payload) | string |
payload | 须要更新的状态值 | object |
只更新所在圈子状态
属性 | 说明 | 备注 |
---|---|---|
$groups | 获取所在页面内的全部圈子状态值 | object, 只读 |
$group | 获取所在圈子状态值 | object, 只读 |
this.$group['nameA1']
复制代码
用于更新所在圈子中的状态
this.dispatch({
type: 'data', // group
payload: {
nameA1: 'name'
}
})
复制代码
参数 | 说明 | 备注 |
---|---|---|
type | 格式: 更新类型,更新类型:data 或 group,data表示更新圈子数据的同时,也将payload中的数据更新至this.data, 即this.setData(payload) | string |
payload | 须要更新的状态值 | object |
更新类型
之因此会有data更新类型,是由于组件提供的数据名称,有可能会被改写,因此组件不要去监听本身的数据,那是给外人用的。
有时候组件向圈子贡献了数据,但自身并不须要这些数据更新到data,因此会有group更新类型
腾讯官方的westore库,为小程序定制了一个diff,本想用在本身库里, 但使用中发现diff结果不太对,他会将删除的属性值重置为null,我并不须要为null, 当去遍历对象的属性值,就会多出一个为null的属性,莫名其妙,删了就是删了,没必要再保留, 而且其内部实现,在进行diff前先进行一次同步下key键,这个彻底不必,浪费性能。 westore的diff应该是用在全局状态新旧状态上,其结果违反setData的数据拼接规则, 也不符合原生数据更新的习惯, 因此我得本身写一个diff,先列出基本状况:
一、不改变原生setData的使用习惯;
二、明确diff使用的场景,针对具体场景定制:属性更新时须要diff,setData时须要diff;
三、diff返回结果的格式,setData会用到,须要知足 a.b[0].a
这样的格式,另外若是属性变化,还须要根据键值调用watch;
四、参与对比的两个参数都有一个共同特色,就是都是拿局部变化的数据,去对比全量旧数据,属性的话,二者的键值对更是对等的,因此无需同步键值;
定制的diff:
diff({
a: 1, b: 2, c: "str", d: { e: [2, { a: 4 }, 5] }, f: true, h: [1], g: { a: [1, 2], j: 111 }
}, {
a: [], b: "aa", c: 3, d: { e: [3, { a: 3 }] }, f: false, h: [1, 2], g: { a: [1, 1, 1], i: "delete" }, k: 'del'
})
复制代码
diff结果
const diffResult = {
result: {
a: 1,
b: 2,
c: "str",
'd.e[0]': 2,
'd.e[1].a': 4,
'd.e[2]': 5,
f: true,
g: {
a: [1,2],
j: 111
},
h: [1]
},
rootKeys: {
a: true,
b: true,
c: true,
d: true,
g: true,
h: true,
}
}
复制代码
记录rootKeys是由于属性监听不须要 'a.b'这样的格式,在diff中记录下来,便少了一次遍历筛选拆分。