JS的心灵契约——Mide-Pact.js一个简洁又异常强大的Key-Value管理器

发布于 · 最后修改时间 · 标签: javascript tools

前言

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呢?这个自定义类型的优势如下:

  1. 支持字符串,而且是鸭子类型的数据,这就意味着可以序列化,使用JSON来传递这个动态数据的Getter、Setter。
  2. 更为丰富的上下文信息,JS原始的setter、getter是只能知道自己的this对象,而CustomType在运行getter、setter可以知道自己所在的调用时的具体路径

值得注意的是这里的表达式会比MP.prototype.get/set中的表达式更为强大,因为内置了一些关键字。这里统一讲一下关键字:

关键字支持get/set支持CustomType的getter/setter描述备注
__vm√/√√/√model实例对象本身
__context√/√√/√上下文对象
__global√/√√/√全局对象浏览器环境中指向window
$cur_key×/×√/√当前对象所在this中的keya.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.1a.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才完整。