前言
mind-pact.js
这个东西,以前做过,但是做得不够好,而且是整合在以前开发的MVVM框架里头,作为Model层。
这两天整理了一下,凝练了核心的思想。
这个库,是一个key-value管理器。简单的说就是:
model.set("a.b",1);
model.get("a");//{ b:1 }
最重要的特性:支持表达式:
model.get("a['b']");//1
但我们知道JS的表达式里头是可以支持函数的定义等等的,这里做了一定的限制,但又不会失去灵活性,具体看如下API描述:
API 文档
MP(basedata)
- basedata (except Primitive values) : 除了Primitive values 以外的任意对象
MP.prototype.set(paths, value, context)
- paths (string) :
懒得描述 - value (any) :
懒得描述 - context (object | null) : 表达式的上下文对象,具体怎么用后面描述
MP.prototype.get(paths, context)
- paths (string) :
懒得描述 - context (object | null) : 表达式的上下文对象,具体怎么用后面描述
这里说一下这个context对象的作用把。就是前面说到的“JS的表达式里头是可以支持函数的定义等等的,这里做了一定的限制,但又不会失去灵活性”的一个解决方案,比如说:
// 原始表达式
a+(function(){ return Math.random() })()+b;
// 提取出function部分后变成:
a+foo()+b
//context就是提取出来的对象:
context = {
foo:function(){ return Math.random() }
}
如何深入去用我就不多说了
MP.CustomType(getter [, setter])
自定义数据类型
- getter (string| [string,any] | function($cur_key, $pre_path_str, $full_path_str)) : 支持三种格式:字符串表达式、字符串表达式+上下文,函数
- setter (string| [string,any] | function($cur_key, $new_value, $pre_path_str, $full_path_str)) : 此参数可空
自定义数据类型,为什么不直接使用JS内置的Getter、Setter呢?这个自定义类型的优势如下:
- 支持字符串,而且是鸭子类型的数据,这就意味着可以序列化,使用JSON来传递这个动态数据的Getter、Setter。
- 更为丰富的上下文信息,JS原始的setter、getter是只能知道自己的this对象,而CustomType在运行getter、setter可以知道自己所在的调用时的具体路径
值得注意的是这里的表达式会比MP.prototype.get/set
中的表达式更为强大,因为内置了一些关键字。这里统一讲一下关键字:
关键字 | 支持get/set | 支持CustomType的getter/setter | 描述 | 备注 |
---|---|---|---|---|
__vm | √/√ | √/√ | model实例对象本身 | |
__context | √/√ | √/√ | 上下文对象 | |
__global | √/√ | √/√ | 全局对象 | 浏览器环境中指向window |
$cur_key | ×/× | √/√ | 当前对象所在this中的key | a.b.c = CustomType( * , * ),这里指向的就是"c" |
$pre_path_str | ×/× | √/√ | 看备注 | 上述条件,这里指向"a.b" |
$full_path_str | ×/× | √/√ | 看备注 | 上述条件,这里指向"a.b.c" |
$cur_value | ×/× | √/× | 当前缓存值 | 初始化是null,每运行一次getter都会被更新 |
$new_value | ×/× | ×/√ | setter所要赋予的值 | |
$old_value | ×/× | ×/√ | 当前缓存值 | 同$cur_value,只是这里只能在setter中使用 |
所以围绕这些关键字,来实现一个基础的CustomType对象,就是fullName=firstName lastName
。
实现代码如下:
版本1
这个是单纯的getter,简单直观,但是问题在于"me."这个写死在表达式中了,就意味着如果这个数据发生迁移或者复制到其它地方,就会运行出现问题。
m.set("me", {
firstname: "Gaubee",
lastname: "Bangeel",
fullname: CustomType("me.firstname+' '+me.lastname")
});
版本2
这里用到了
__vm
与$pre_path_str
两个关键字,其中$pre_path_str
用来替代版本1中的"me.",然后由于使用了$pre_path_str
关键字,解析就不会用__vm.get(*)
自动包裹这个关键字,所以需要手动添加这段代码,结果如下:
m.set("me", {
firstname: "Gaubee",
lastname: "Bangeel",
fullname: CustomType("__vm.get($pre_path_str+'.firstname')+' '+__vm.get($pre_path_str+'.lastname')")
});
版本3
这里实现了Setter,要值得注意的是,setter的表达式里头,多个语句的分割是
,
而不是;
,因为这个单个表达式,简而言之就是解析是把表达式处理成return *
,所以如果使用;
就会导致;
后面的语句不运行(PS:不要利用这点来实现局部变量的定义,虽然var声明会自动前置,但是请使用context来声明局部变量,否则如果变量名不在context中,会被直接包裹成__vm.get(key)
进行处理)。另外值得注意的是这里表达式最后返回是null
,如果返回null/undefined/false
,那么就意味着$new_value最后是存储到默认的缓存中;如果返回的不是空值,那么就会被当成是路径,直接将$new_value赋值到这个路径所指定的对象中。
m.set("me", {
firstname: "Gaubee",
lastname: "Bangeel",
fullname: CustomType("__vm.get($pre_path_str+'.firstname')+' '+__vm.get($pre_path_str+'.lastname')",
"($new_value=$new_value.split(' ')),\
__vm.set($pre_path_str+'.firstname',$new_value[0]),\
__vm.set($pre_path_str+'.lastname',$new_value[1]),\
null")
});
技术实现细节
这部分是讲到一些效率优化、解析处理等实现细节。
1
因为不是以往的a.b.c
这样耿直的用.
来分割了,而MP涉及到表达式,即便用了缓存,如果遇到数组a.1
~a.1000
,那么缓存还有用么?
这个是在开始做的时候一个难点,最后实现确实我用了缓存,但是不是耿直用。这里缓存的不是路径:函数
,而是模式:函数
。基于模式的缓存,比如说a.1
与a.1000
,这两者的模式是一样的,所以只用了同一个模式工厂函数,而后将a.1/1000
传入模式工厂函数中,返回一个带着闭包的函数。怎么解释看下面这个实例:
MP.formatKey("a.b[A.B+C-D(E,F)].c");
/*[ 'a', 'b',
function(__vm) {
return PLA(matchs[0], __vm, __context) +
PLA(matchs[1], __vm, __context) -
PLA(matchs[2], __vm, __context)(
PLA(matchs[3], __vm, __context),
PLA(matchs[4], __vm, __context)
)
}
, 'c']
这里返回的数组中的第三个对象,就是模式工厂返回的函数,其中matchs、__context都是处于闭包中,
所以同样模式的表达式(`@+@-@(@,@)`)可以公用这个模式工厂。
*/
因为我认为在一个WebApp中,取值路径可能有上万个,但是模式不过那么几种——应用里头的字符串就那些。所以基于模式的匹配是很靠谱的一种解决方案,即便有动态的模式,使用context也能化繁为简,充分利用模式缓存。
2
另外,看上面那个formatKey返回的数组数据,可以发现其实[ * ]
包裹起来的部分其实是会被解析成函数的,所以[ * ]
内外是有性能区别的,也因此,能在外部写a.1
这种违反JS语法的写法,正常JS写法是a[1]
,而为了性能,我建议是使用a.1
。上面说到模式,这里说道表达式,就不得不说一下解析:
这里是提取x.x.x
作为路径单元,[ * ]
作为模式单元,所以上述例子里头[A.B+C-D(E,F)]
开头的A.B
是被会识别成路径单元,最后模式才不是:@.@+@-@(@,@)
3
动态的赋值。如果一个路径是a.b.c.d
,而a
属性本身就是空的话,那么在model.set('a.b.c.d',value)
的过程中,会动态创建值给a b c d
,如果路径某一部分的key是整数,那么动态创建的就会是数组:
m.set("A.B.1.C","ccc");
console.log(m.get("A"));
//{ B: [ , { C: 'ccc' } ] }
【8/21 6:08】 目前功能有限,但代码就不到400行,接下来会加入动态监听功能,有了这个MP才完整。