UNPKG

@fanx/wxstore

Version:

wechat miniprogram store manager

492 lines (469 loc) 14.2 kB
/** * 微信小程序状态管理 * 1、基础库版本2.7.1以上 * 2、如果组件和页面是同时加载时,Component ready时才绑定store attached中可能无法找到this.store */ import deepProxy from './utils/observer' import diff from './utils/diff' import { deepClone, toKeys, toKeyStr, noEmptyObject, reverse, getValue, setValue, defineStatic, type, OBJECT, ARRAY, FUNCTION, STRING } from './utils/utils' import { getCurrentPage, pushTabPage, shiftTabPage } from './utils/instanceUtils' const supportProxy = !!Proxy // 是否支持Proxy const STORES = {} // store 实例集 let storeId = 1 // store id let listenerId = 1 // 监听器id export default class WxStore { constructor ({ state = {}, actions = {}, debug = false } = {}) { this._state = deepClone(state) if (supportProxy) { defineStatic(this, 'state', deepProxy(deepClone(state), this._observer.bind(this))) // 监听对象变化 } else { this.state = deepClone(state) // state } this._id = storeId++ // store id this._binds = [] // 绑定的实例对象 this._diffObj = {} // diff结果 this._observerList = [] // Proxy监听state this._listener = {} // 监听state改变事件 this._debug = debug // 开启debug模式后会输出没吃diff数据 defineStatic(this, 'actions', {}) // 行为方法 Object.keys(actions).forEach((key) => { this.actions[key] = actions[key].bind(this) }) } /** * 监听 */ _observer (keys, value) { let i = 0 const len = keys.length // 判断是否覆盖已有,如果覆盖则移除旧的push新的 while (i < this._observerList.length) { const item = this._observerList[i] if (item.keys.length >= len) { let match = true for (let j = 0; j < len; j++) { if (keys[j] !== item.keys[j]) { match = false break } } if (match) { this._observerList.splice(i, 1) } else { i++ } } else { i++ } } this._observerList.push({ keys, value }) } /** * 将state监听到的变化写入diff对象中用于最终的setData,且将变化同步到_state */ _observerSet () { this._observerList.forEach(({ keys, value }) => { value = deepClone(value) // 根据对象key 提取 state 与对于value比对 const keyStr = toKeyStr(keys, this._state) // 空对象不执行diff update操作 if (noEmptyObject(value, true)) { // 获取diffObj Object.assign(this._diffObj, diff(value, getValue(this._state, keys), keyStr)) } else { Object.assign(this._diffObj, { [keyStr]: value }) } // 同步到_state setValue(this._state, keys, value) }) this._observerList = [] } /** * 设置state * @param {*} obj set数据对象 */ _diffSet () { // 获取diffObj Object.assign(this._diffObj, deepClone(diff(this.state, this._state))) // 写入store state for (const key in this._diffObj) { const keys = toKeys(key) setValue(this._state, keys, this._diffObj[key]) } } /** * 更新视图 * @param {*} obj 更新对象 * @param {*} imm 是否立即更新视图 */ update (obj, imm) { // 支持update写入更改state if (type(obj, OBJECT)) { for (const key in obj) { const keys = toKeys(key) setValue(this.state, keys, obj[key]) } } if (imm) { // 立即执行 return this._set() } else { // 合并执行 return this._merge() } } /** * 合并多次set */ _merge () { // Promise实现合并set // eslint-disable-next-line no-return-assign return this._pendding = this._pendding || Promise.resolve().then(() => { return this._set() }) } /** * 设置映射数据 */ _set () { return new Promise((resolve) => { if (supportProxy) { // 支持Proxy的 this._observerSet() } else { // 用于不支持Proxy this._diffSet() } if (noEmptyObject(this._diffObj)) { let count = 0 const diffObj = { ...this._diffObj } // 实例更新 this._binds.forEach((that) => { // 获取diffObj => 实例的新的diff数据 const obj = this._getMapData(that.__stores[this._id].stateMap) // set实例对象中 if (noEmptyObject(obj)) { count++ that.setData(obj, () => { count-- if (count <= 0) { resolve(diffObj) } }) } }) // 监听器回调 for (const id in this._listener) { // 监听 if (this._isChange(this._listener[id].map)) { // 执行回调 this._listener[id].fn(this._listener[id].map.map((key) => { return deepClone(getValue(this._state, toKeys(key))) })) } } if (count <= 0) { resolve(diffObj) } // debug diff 结果输出 this._debug && console.log('diff object:', this._diffObj) this._diffObj = {} // 清空diff结果 } else { resolve({}) } delete this._pendding // 清除 }) } /** * 绑定对象 * @param {*} that 实例Page/Component * @param {*} map 实例data => state 的映射map * @param {*} extend bind时映射到data的扩展字段 */ bind (that, map, extend = {}) { // 必须为实例对象 if (!type(that, OBJECT)) { console.warn('[wxStore] check bind this') return } // 必须是obj或者arr if (!type(map, OBJECT) && !type(map, ARRAY)) { console.warn('[wxStore] check bind stateMap') map = {} } const stateMap = reverse(map) this._bind(that, stateMap, extend) } /** * 绑定对象 * @param {*} that 实例Page/Component * @param {*} stateMap state => 实例data 的映射map * @param {*} extend bind时映射到data的扩展字段 */ _bind (that, stateMap, extend = {}) { // 必须是obj或者arr if (!type(stateMap, OBJECT) && !type(stateMap, ARRAY)) { console.warn('[wxStore] check bind stateMap') stateMap = {} } // 获取state=>实例data的指向 const id = this._id const setter = diff(Object.assign(setData(stateMap, this._state), extend), that.data) that.setData(setter) // 执行set that.__stores = type(that.__stores, OBJECT) ? that.__stores : {} // 初始化实例对象上的状态映射表 // 映射对象写入实例对象 if (that.__stores[id]) { Object.assign(that.__stores[id].stateMap, stateMap) } else { that.__stores[id] = { stateMap, store: this } } !this._binds.find(item => item === that) && this._binds.push(that) // 实例写入this._binds数组中,用于update找到实例对象 STORES[id] || (STORES[id] = this) // 已bind store 存入STORES集合 } /** * 解除绑定 * @param {*} that 实例Page/Component */ unBind (that) { that.__stores && (delete that.__stores[this._id]) // 清除状态管理对象映射对象 // 移除实例绑定 for (var i = 0; i < this._binds.length; i++) { if (this._binds[i] === that) { this._binds.splice(i, 1) break } } // bind列表空时,在STORES删除store if (!this._binds.length) { delete STORES[this._id] } } /** * 监听状态改变 * @param {*} map 监听的数据映射 * @param {*} fn 监听回调 */ on (map, fn, that) { if (type(map, STRING)) { // eslint-disable-next-line no-useless-escape map = map.split(/\s*\,\s*/g) } // map必须为obj或者arr 且fn必须为function if (!noEmptyObject(map, true) || !type(fn, FUNCTION)) { console.warn('[wxStore] check on params') return } // 获取监听state的映射 map = deepClone(map) // 监听器id const id = listenerId++ // 监听器数据保存到this._listener中 this._listener[id] = { map, fn } if (type(that, OBJECT)) { that.__listener = type(that.__listener, OBJECT) ? that.__listener : {} that.__listener[id] = this } // id用于remove return id } /** * 移除监听状态改变 * @param {*} id listener的id */ remove (id) { if (this._listener[id]) { delete this._listener[id] } } /** * 根据具体diff 及 映射map 获得最终setData对象 * @param {*} map 映射map */ _getMapData (map) { if (!noEmptyObject(map)) return {} const obj = {} // diff结果与映射的双重比对 const reg = RegExp(`^(${Object.keys(map).join('|')})((?=(?:\\.|\\[))|$)`) for (const key in this._diffObj) { let match = false const newKey = key.replace(reg, (s) => { match = true return map[s] || s }) if (match) { obj[newKey] = this._diffObj[key] } } return obj } /** * 监听的对象是否修改 * @param {*} map */ _isChange (map) { // diff结果与映射的双重比对 const reg = RegExp(`^(${map.join('|')})((?=(?:\\.|\\[))|$)`) for (const key in this._diffObj) { if (key.match(reg)) { return true } } return false } /** * 通过字符串key获取value */ getState (key) { if (type(key, STRING)) { return deepClone(getValue(this._state, toKeys(key))) } } } /** * 挂载 * @param {*} ops 实例配置 */ function Attached (ops) { ops.stateMap = ops.stateMap || {} ops.store = ops.store || {} // stores 必须为数组 if (type(ops.stores, ARRAY)) { ops.stores.forEach((item = {}) => { // store必须为WxStore对象、stateMap必须为Object或Array const { store, stateMap, _rstateMap } = item if (store instanceof WxStore && noEmptyObject(stateMap, true)) { _rstateMap ? store._bind(this, _rstateMap) : store.bind(this, stateMap) } }) } // store必须为对象、stateMap必须为Object或Array if (type(ops.store, OBJECT) && (type(ops.stateMap, OBJECT) || type(ops.stateMap, ARRAY))) { const { STOREID = 0 } = this.properties || {} // store 如果是Wxstore的实例则直接使用,否则使用id为STOREID的store,ops.fixed === true 使用页面级store, 否则通过store配置生产新的store this.store = ops.store instanceof WxStore ? ops.store : STORES[STOREID] || (!ops.fixed && getCurrentPage(this).store) || new WxStore(ops.store) // 传入已经是WxStore实例则直接赋值,否者实例化 const extend = ops.fixed ? { STOREID: this.store._id } : {} ops._rstateMap ? this.store._bind(this, this._rstateMap, extend) : this.store.bind(this, ops.stateMap, extend) // 绑定是不初始化data、在实例生产前已写入options中 } } /** * 取消挂载 */ function Detached () { // 解除this上的listener的绑定 if (noEmptyObject(this.__listener)) { for (const id in this.__listener) { this.__listener[id].remove(id) } delete this.__listener } // 解除this上的store的绑定 if (noEmptyObject(this.__stores)) { for (const id in this.__stores) { this.__stores[id].store.unBind(this) } delete this.__stores } delete this.store // 删除store } /** * 重写Page方法,提供自动绑定store自定移除store方法 * @param {*} ops 页面初始化配置 */ export function storePage (ops) { initData(ops) // 初始化data、作用是给data里面填入store中的默认state // 重写onLoad const onLoad = ops.onLoad ops.onLoad = function () { ops.fixed = true Attached.call(this, ops) pushTabPage(this) type(onLoad, FUNCTION) && onLoad.apply(this, [].slice.call(arguments)) } // 重写onUnload const onUnload = ops.onUnload ops.onUnload = function () { type(onUnload, FUNCTION) && onUnload.apply(this, [].slice.call(arguments)) // 执行卸载操作 shiftTabPage(this) Detached.call(this) } Page(ops) } /** * 重写Component方法,提供自动绑定store自定移除store方法 * @param {*} ops 组件初始化配置 */ export function storeComponent (ops) { initData(ops) // 初始化data、作用是给relateddata里面填入store中的默认state ops.properties = type(ops.properties, OBJECT) ? ops.properties : {} Object.assign(ops.properties, { STOREID: Number }) const name = ops.fixed ? 'attached' : 'ready' let lts = ops.lifetimes && ops.lifetimes[name] ? ops.lifetimes : ops // 重写ready const init = lts[name] lts[name] = function () { Attached.call(this, ops) type(init, FUNCTION) && init.apply(this, [].slice.call(arguments)) } lts = ops.lifetimes && ops.lifetimes.detached ? ops.lifetimes : ops // 重写detached const detached = lts.detached lts.detached = function () { type(detached, FUNCTION) && detached.apply(this, [].slice.call(arguments)) // 执行卸载操作 Detached.call(this) } Component(ops) } /** * diff */ exports.diff = diff /** * 初始化数据 * @param {*} map * @param {*} data */ function setData (map, data) { const obj = {} // 初始化实例的data if (noEmptyObject(map) && type(data, OBJECT)) { // 在初始化实例data使用 for (const key in map) { const keys = toKeys(key) obj[map[key]] = deepClone(getValue(data, keys)) } } return obj } /** * 初始化数据 * @param {*} map * @param {*} data */ function initData (ops) { const data = {} if (type(ops.stores, ARRAY)) { ops.stores = ops.stores.map((s) => { if (type(s.store, OBJECT) && (noEmptyObject(s.stateMap, true))) { s._rstateMap = reverse(s.stateMap) Object.assign(data, setData(s._rstateMap, s.store._state || s.store.state)) } return s }) } if (type(ops.store, OBJECT) && (noEmptyObject(ops.stateMap, true))) { ops._rstateMap = reverse(ops.stateMap) Object.assign(data, setData(ops._rstateMap, ops.store._state || ops.store.state)) } ops.data = type(ops.data, OBJECT) ? ops.data : {} Object.assign(ops.data, data) return data }