@wxmp/mstore
Version:
weixin miniprogram nonintrusive memory storage & state management tool
511 lines (472 loc) • 13.6 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.mlodash = factory());
}(this, function () { 'use strict';
function getTag(src) {
return Object.prototype.toString.call(src)
}
function hasOwnProperty(obj, keyName) {
return Object.prototype.hasOwnProperty.call(obj, keyName)
}
function isObject(obj) {
return getTag(obj) === '[object Object]'
}
function isString(str) {
return getTag(str) === '[object String]'
}
function isFunction(func) {
return getTag(func) === '[object Function]'
}
function isArray(arr) {
return getTag(arr) === '[object Array]'
}
function is(x, y) {
// inlined Object.is polyfill to avoid requiring consumers ship their own
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
if (x === y) {
// Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
// Added the nonzero y check to make Flow happy, but it is redundant
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y
}
}
function isShallowEqual(objA, objB) {
if (is(objA, objB)) {
return true
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false
}
let i = 0;
while (i < keysA.length) {
if (
!hasOwnProperty(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false
}
i += 1;
}
return true
}
function has(obj, keyName) {
return obj !== null
&& obj !== undefined
&& hasOwnProperty(obj, keyName)
}
function reduce(src, func) {
let i = 0;
let acc = arguments[2];
if (isArray(src)) {
if (arguments.length !== 3) {
acc = src[0];
}
while(i < src.length) {
acc = func(acc, src[i], i, src);
i += 1;
}
return acc
} else if (isObject(src)) {
const keys = Object.keys(src);
if (arguments.length !== 3) {
acc = src[keys[0]];
}
while(i < keys.length) {
const key = keys[i];
acc = func(acc, src[key], key, src);
i += 1;
}
return acc
}
return acc
}
function forEach(src, func) {
let i = 0;
if (isArray(src)) {
while(i < src.length) {
const rst = func(src[i], i, src);
if (rst === false) {
break
}
i += 1;
}
} else if (isObject(src)) {
const keys = Object.keys(src);
while(i < keys.length) {
const key = keys[i];
const rst = func(src[key], key, src);
if (rst === false) {
break
}
i += 1;
}
}
}
function findIndex(src, func) {
let rst = -1;
forEach(src, (item, index, obj) => {
if (isFunction(func)) {
if (func(item, index, obj) === true) {
rst = index;
return false
}
} else {
if (isShallowEqual(item, func)) {
rst = index;
return false
}
}
});
return rst
}
const charCodeOfDot = '.'.charCodeAt(0);
const reEscapeChar = /\\(\\)?/g;
const rePropName = RegExp(
// Match anything that isn't a dot or bracket.
'[^.[\\]]+' + '|' +
// Or match property names within brackets.
'\\[(?:' +
// Match a non-string expression.
'([^"\'].*)' + '|' +
// Or match strings (supports escaping characters).
'(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
')\\]'+ '|' +
// Or match "" as the space between consecutive dots or empty brackets.
'(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
, 'g');
function stringToPath(string) {
const result = [];
if (string.charCodeAt(0) === charCodeOfDot) {
result.push('');
}
string.replace(rePropName, (match, expression, quote, subString) => {
let key = match;
if (quote) {
key = subString.replace(reEscapeChar, '$1');
}
else if (expression) {
key = expression.trim();
}
result.push(key);
});
return result
}
function toPath(value) {
if (!isString(value)) {
return []
}
return stringToPath(value)
}
function get(object, path, defaultValue) {
if (object == null) {
return defaultValue
}
if (!Array.isArray(path)) {
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
const reIsPlainProp = /^\w*$/;
const isKey = function(value, object) {
const type = typeof value;
if (type == 'number' || type == 'boolean' || value == null) {
return true
}
return reIsPlainProp.test(value)
|| !reIsDeepProp.test(value)
|| (object != null && value in Object(object))
};
if (isKey(path, object)) {
path = [path];
} else {
path = stringToPath(path);
}
}
let index = 0;
const length = path.length;
while (object != null && index < length) {
object = object[path[index]];
index += 1;
}
if (index && index === length) {
return object === undefined ? defaultValue : object
} else {
return defaultValue
}
}
const _name = Symbol('name');
const _watchMap = Symbol('watchMap');
const _eventMap = Symbol('eventMap');
function defineProperty(obj, key, val, notify) {
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
const value = getter ? getter.call(obj) : val;
return value
},
set: (newVal) => {
const value = getter ? getter.call(obj) : val;
if (isShallowEqual(newVal, value)) {
return
}
// #7981: for accessor properties without setter
if (getter && !setter) {
return
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
if (isFunction(notify)) {
notify(value);
}
}
});
}
function isNotReservedKeyword(key) {
const keywords = ['event', 'state'];
if (keywords.indexOf(key) !== -1) {
console.error(`${keywords.join()} 是store对象的保留关键字,请勿声明使用`);
return false
}
return true
}
class Store {
constructor(option, store) {
option = option || {};
let events = option.event || [];
let getters = option.getter || {};
let setters = option.setter || {};
let methods = option.method || {};
let states = option.state || {};
this[_name] = option.name;
// 缓存完整的store对象
Object.defineProperty(this, '_$store', {
get() {
return store
}
});
// 事件映射表
this[_eventMap] = new Map();
// 注册事件名称
events = reduce(events, (acc, name) => {
if (isString(name)) {
acc[name] = name;
}
return acc
}, {});
Object.freeze(events);
Object.defineProperty(this, 'event', {
get() {
return events
}
});
// 注册getters setters
const keys = {};
forEach(getters, (func, key) => {
keys[key] = keys[key] || {};
if (isFunction(func)) {
keys[key].get = () => {
return func.call(this)
};
}
});
forEach(setters, (func, key) => {
keys[key] = keys[key] || {};
if (isFunction(func)) {
keys[key].set = (...args) => {
return func.apply(this, args)
};
}
});
forEach(keys, (item, key) => {
if (has(this, key)) {
console.error(`模块(${this[_name]})中出现重复的字段(key = ${key}),`
+ '请检查state, getter, setter, method'
);
} else if (isNotReservedKeyword(key)){
Object.defineProperty(this, key, item);
}
});
// 注册methods
forEach(methods, (func, key) => {
if (has(this, key)) {
console.error(`模块(${this[_name]})中出现重复的字段(key = ${key}),`
+ '请检查state, getter, setter, method'
);
} else if (isNotReservedKeyword(key)) {
if (isFunction(func)) {
Object.defineProperty(this, key, {
get() {
return (...args) => {
return func.apply(this, args)
}
}
});
}
}
});
// 注册states
this.state = {};
forEach(states, (value, key) => {
if (has(this, key)) {
console.error(`模块(${this[name]})中出现重复的字段(key = ${key}),`
+ '请检查state, getter, setter, method'
);
} else if (isNotReservedKeyword(key)) {
defineProperty(this, key, value);
this.state[key] = key;
}
});
// watch相关参数
this[_watchMap] = new Map(); // watch时map表
}
on(name, func, ctx = null) {
if (!this.event[name] || !isFunction(func)) {
return
}
if (!this[_eventMap].has(name)) {
this[_eventMap].set(name, []);
}
this[_eventMap].get(name).push({ func, ctx });
}
off(name, func) {
if (!this[_eventMap].has(name)) {
return
}
if (func) {
const funcs = this[_eventMap].get(name);
const index = findIndex(funcs, item => item.func === func);
if (index !== -1) {
funcs.splice(index, 1);
}
} else {
this[_eventMap].delete(name);
}
}
emit(name, ...args) {
if (!this.event[name]) {
return
}
if (this[_eventMap].has(name)) {
const funcs = this[_eventMap].get(name);
forEach(funcs, item => {
try {
item.func.apply(item.ctx, args);
} catch(e) {
console.error(e);
}
});
}
}
/**
* watch states, 发生变化执行回调,
* 当watch单个state时,发生变化立即回调
* 当watch多个states时,需要手动执行notify触发回调
*
* watch的回调函数参数:func(newVal, oldVal, store)
* newVal: 变化后的值
* oldVal: 变化前的值
* store: 完整的store对象
*
* watch回调函数应尽量使用箭头函数,因函数this会发生变化
*
* 例子:
* import { test } from 'store'
* test.watch('stateA', (newVal, oldVal, store) => {
* console.log(newVal, oldVal, store)
* console.log(test.stateA === newVal)
* console.log(store.test === test)
* })
*
*/
watch(state, func, ctx = null) {
if (!state || !isString(state)) {
console.error(`模块(${this[_name]}): watch第一个参数必填且必须是有效字符串`);
return
}
if (!isFunction(func)) {
console.error(`模块(${this[_name]}): watch第二个参数必填且必须是函数`);
return
}
const path = toPath(state);
let key = path[0];
let obj = this;
if (path.length > 1) {
key = path.splice(-1);
obj = get(this, path);
if (!isObject(obj)) {
console.error(
`模块(${this[_name]})中state[${path.join('][')}]不是object类型,`
+ `,不能被watch`
);
return
}
}
if (!this[_watchMap].has(state)) {
this[_watchMap].set(state, [{ func, ctx }]);
defineProperty(obj, key, obj[key], oldVal => {
const funcs = this[_watchMap].get(state);
forEach(funcs, item => {
item.func.call(item.ctx, obj[key], oldVal, this._$store);
});
});
} else {
// 去重保护
const funcs = this[_watchMap].get(state);
const index = findIndex(funcs, item => item.func === func);
if (index === -1) {
funcs.push({ func, ctx });
}
}
}
unWatch(state, func) {
if (!this[_watchMap].has(state)) {
return
}
if (func) {
const funcs = this[_watchMap].get(state);
const index = findIndex(funcs, item => item.func === func);
if (index !== -1) {
funcs.splice(index, 1);
}
} else {
this[_watchMap].delete(state);
}
}
}
function initStore(modules) {
const rst = {};
forEach(modules, (item, moduleName) => {
if (!has(item, 'name')) {
console.error(`模块注册失败(${moduleName}),缺失必要字段:name`);
} else if (has(rst, item.name)) {
console.error(`模块注册失败,模块名称重复: name=${item.name}`);
} else {
rst[item.name] = Object.freeze(new Store(item, rst));
}
});
return rst
}
return initStore;
}));