经过两个星期的性能优化,avalon终于实现在一个页面绑定达到上万个的时候不卡顿的目标(angular的限制是2000)。现在稍作休息,总结一下avalon遇到的一些难题。

首先是如何监控的问题。所有MVVM要将VM中的属性与视图中的绑定属性关联起来大抵有如下三种方式:angular是对函数体取toString进行预编译,将里面的赋值语句,取值语句替换为set,get方法,然后通过特定方法进行脏检测触发,或手动触发;ko是对VM的属性用监控函数外包一层,全事件驱动触发;avalon是通过Object.defineProperties重写内部set,get函数,全事件驱动触发。此外还有emberjs,它是统一使用上帝set,get方法接触所有取值赋值的入口,全事件驱动触发,算是angular的改良。从用户体验来说,avalon的实现是最好的,因为它是不用改变用户习惯,emberjs次之,强制使用set,get方法,ko让函数、数组变成了函数,让用户感到非常违和,angular最差,一大堆恶心的限制,无法直接操作VM。出现这局面是因为Object.defineProperty不好兼容,它虽然是IE8支持,但那只在元素节点上存在。除非你摒弃IE8了。直接我找到VBScript,这问题才不算问题。我的优势是,我至一开始就知道有VBS这东西存在,在avalon实现之初就开始动用这东西。

ms-if的实现,说到底是生命周期的设计问题,如何销毁一个绑定及在特殊情况还让它继续存活。VM与视图的关联点在于绑定属性,绑定属性会转换为求值函数,求值函数将与它的上下文环境(比如它所在的元素节点,它原来的绑定属性的名字,值,类型,过滤器定义情况等等)组成一个对象,放到一个数组中。这就是订阅者数组。数VM中的属性发生变化时(通过内部set方法被调动时得知),就会执行这个求值函数将其他东西一起执行,从而实现视图的最小化局部刷新。问题是,我们的页面有时很大,上面拥有许多绑定属性,这意味着这些中间生成的求值函数与对象将一直放在各个订阅者数组中,占用着大量内存。如果再出现像瀑布流或或定时刷新的情况,这内存占用将越来越大,让页面运行缓慢。因此就必需考虑回收内存的情况了。avalon给出的方案时,当某一个节点将出DOM树,它自身或底下的节点的原来所有绑定属性所生成的求值函数将从订阅者数组中移除。

    function notifySubscribers(accessor) { //通知依赖于这个访问器的订阅者更新自身
var list = accessor[subscribers]
if (list && list.length) {
var args = aslice.call(arguments, 1)
for (var i = list.length, fn; fn = list[--i]; ) {
var el = fn.element,
remove
if (el && !avalon.contains(ifSanctuary, el)) {
if (typeof el.sourceIndex == "number") { //IE6-IE11
remove = el.sourceIndex === 0
} else {
remove = !avalon.contains(root, el)
}
if (remove) { //如果它没有在DOM树
list.splice(i, 1)
log("Debug: remove " + fn.name)
}
}
if (typeof fn === "function") {
fn.apply(0, args) //强制重新计算自身
} else if (fn.getter) {
fn.handler.apply(fn, args) //处理监控数组的方法
} else {
fn.handler(fn.evaluator.apply(0, fn.args || []), el, fn)
}
}
}
}

上面就是这一操作的实现,VM要对视图进行同步都必须经过此方法notifySubscribers。每次执行时,它都会取得求值函数上的元素节点,然后判定它是否在DOM树上。在IE6-11中,我们可以判定sourceIndex 属性是否为零得知,标准浏览器可以通过根节点是否包含当前元素得知(这个contains方法内部需要做一番兼容)。

但ms-if的出现打破这和谐局面,要考虑此绑定主动移出DOM树的情况,还要判定考虑此元素什么时候插回DOM树。在循环绑定中,元素节点在一开始是在文档碎片中动态生成的,这个更麻烦。从0.6-1.1,我一直陷入这噩梦中。之前我有的方案是采用定时器,不断轮询此节点是否插入DOM,插入了才开始对它扫描。后来又发掘出DOMNodeInserted这个事件,对一些高级一点的浏览器做一些优化。到了0.982,干脆就直接假设它们一开始都没加入DOM,添加一个类名,防止移入移出时颤动,再把当前的VM列表绑定在元素上,然后判定元素是否在DOM树(又是轮询操作)。再后来是改写循环绑定部分,在ms-each, ms-repeat等执行后,再执行一个回调,扫描当前部分,这样就可以消去轮询操作了。再再后来的改进是,确保循环生成时,元素都集中一个文档碎片中,然后整体插入DOM时,这时才进行扫描。换言之,第一次它总是在DOM树里。于是就能消去contains判定,ms-if的代码大大减少。现在的ms-if今非昔比,还加入了按需加载功能。它的子元素扫描被它的绑定属性所控制,对大页面的性能优化非常有用。

批量生成与监控数组的实现,这俩是相辅相成的。早期的监控是直接在原数组中改,因此原工厂函数非常庞大。后来直接把这些要覆盖的函数放到一个对象上,然后工厂方法里直接mix一下就行了。还简接让所有监控数组共享了这些方法,节省内存。在绑定的实现,之前是有许多分支,什么push, unshift, pop, shift, set, reroder, splice, clear一大堆,那个视图刷新函数太苦逼了。后来对数组的操作进行深入分析,发现所有操作无疑是做以下几种操作,添加元素,删除元素,改写元素对应的索引值,移动元素到某一位置,直接替换元素。于是改写监控数组的方法,根据add. del, index, move这四种操作进行组合(0.9.0),后来还加了clear,因为批量处理一个数组或一个子对象都用到此操作。这些操作里面都会通过notifySubscribers方法,将操作名与相应参数传到视图刷新函数,从而分配到不同分支上做DOM处理。这算是成功了一大步。内部其实还涉及到代理VM的生成算是处理,于是有了createItemModel的内部函数,然后出现了ms-with,于是它们改名了createWithModel, createEachModel。这两个方法的实现也不断改进,后来更名为createEachProxy, creatWithProxy,在ms-with里还使用了对象池技术(withMapper ,0.96),重用所有同名的键值对生成的代理对象。

到0.9.8,偷偷引入一个ms-repeat绑定。avalon早期的参考对象是knockout,它实现循环绑定时需要用到两个元素,一个父元素作容器,它下面的所有节点作模板,或者用一个虚拟节点(真实名字是两个一前一后的注释节点)圈定作用范围,里面的那些子点作模板。由于注释节点在IE6-8的UL,OL元素上会发生错乱,需要手动处理,avalon就没有更进。但在许多场合,总要外套一个父节点是非常难办,或做不到,于是移目于angular上。angular的ng-repeat只循环元素自身是一个非常好的方案,加之它又带来了$first, $last, $remove等好东西,于是avalon开始模仿。但这工程量与难度非常大,一直跌跌撞撞,在1.2时才基本算完工。其间要处理的问题是,如何让ms-repeat如何同时遍历数组与对象,对象的键值对的输出顺序(data-with-sorted回调的引进),批处理后的回调(data-*-rendered回调),回滚机制(rollback函数),如何判定子元素已经被渲染(需要在元素上添加一个标记,放便在scanAttr时执行一个回调)。回调是同事在做私自人项目提出的,最初没参数,现在能明确是add, del, index等操作了。生成代理VM与绑定标记后来抽象成一个shimController,实现批量插入与批量处理。对象池(更名为withProxyPool)也大大优化,它在一开始时就生成所有键值对代理VM,不再在求值函数里判定了。并且VM加了一个withProxyCount,进行优化。

目前批处理涉及到的内部方法与对象

  • createWithProxy 创建循环对象时的代理VM
  • createEachProxy创建循环数组时的代理VM
  • updateWithProxy 更新某一键值对的代理VM
  • withProxyPool createWithProxy生成的对象统一放在这里管理,防止重复生成
  • removeView 批量移除一堆节点
  • getLocatedNode 定位要插入的位置
  • shimController 为ms-each, ms-with, ms-repeat要循环的元素外包一个msloop临时节点,ms-controller的值为代理VM的$id,同时是实现批量插入插量移除临时节点的关键
  • removeFromSanctuary 将通过ms-if移出DOM树放进ifSanctuary的元素节点移出来,以便垃圾回收
  • queryComments 得到某一元素节点或文档碎片对象下的所有注释节点
  • iteratorCallback 通过它执行data-*-rendered回调

最后一个也是最难一个至少也没有搞定,只在不断改良中,这就是UI绑定的设计。之前有一个绑定叫ms-ui,已经夭折。现在的ms-widget还是不够好。有时我想参考angular的那种方式,但又嫌它添加了太多莫名其妙的符号。但主要是因为我的框架对用户是非常放纵,不喜欢那种改配置的设计。不过就是放着现在的不管,它还有一个重大的缺陷,没有生命周期管理。这个在项目中已经暴露出来,需要用户自己定义一个destroy方法,手动销毁。我认为这是框架的份内事。接下来几星期,我就着手这方面的改进,希望能把这痛点解决掉。

最后总结一下:

  1. VM实现,如何内置与V的同步机制。
  2. ms-if,ng-if, data-bind="if:xxx"这样插入移除的绑定,会中断之前无悬念一直扫到底的思路,之前想好的生命周期管理也要出岔子了!
  3. ms-each, ms-repeat, ng-repeat,data-bind="foreach:xxx"这样的批量生成的绑定,这种绑定最容易引起性能问题,并且需要界定其作用范围
  4. ms-widget, <widget></widget>这样转换元素为一个控件的绑定,这也最复杂最麻烦的绑定,需要有通盘的设计观。

当然对于刚接触这领域的人可以还有许多麻烦事,如不使用jQuery的情况如何摆平那一大堆兼容问题,如何写一个parser解析绑定属性的值,加载器,路由器,动画引擎等一大堆配套设施……这要你自求多福,好之为之了,但跨过这道坎,你就是另一个级别的人物了!

迷你MVVM框架 avalonjs 实现上的几个难点的更多相关文章

  1. 迷你MVVM框架 avalonjs 0.95发布

    迷你MVVM框架 avalonjs 0.95发布 本版本最主要的改进是ms-with 深层绑定的实现,至少,avalon1.0所有重要的feature已经开发完毕,之后就是小补小漏,性能优化了. ms ...

  2. 迷你MVVM框架 avalonjs 0.85发布

    迷你MVVM框架 avalonjs 0.85发布 本版本对循环绑定做了巨大改进,感谢@soom, @limodou, @ztz, @Gaubee 提供的大量测试文件. fix scanNodes, 在 ...

  3. 迷你MVVM框架 avalonjs 0.82发布

    迷你MVVM框架 avalonjs 0.82发布 本版本最大的改进是启用全新的parser. parser是用于干什么的?在视图中,我们通过绑定属性实现双向绑定,比如ms-text="fir ...

  4. 迷你MVVM框架 avalonjs 1.3.9发布

    本次升级,avalon改进了许多内部方法,大大提升性能,并且带来异步刷新视图的新功能. ms-html内部不再使用异步 head元素中的avalon元素加入ms-skip指令 重构计算属性,现在超级轻 ...

  5. 迷你MVVM框架 avalonjs 1.3.8发布

    avalon1.3.8主要是在ms-repeat. ms-each. ms-with等循环绑定上做重大性能优化,其次是对一些绑定了事件的指令添加了roolback,让其CG回收更顺畅. 重构ms-re ...

  6. 迷你MVVM框架 avalonjs 1.3.7发布

    又到每个月的15号了,现在avalon已经固定在每个月的15号发布新版本.这次发布又带来许多新特性,让大家写码更加轻松,借助于"操作数据即操作DOM"的核心理念与双向绑定机制,现在 ...

  7. 迷你MVVM框架 avalonjs 1.3.6发布

    本版本是一次重要的升级,考虑要介绍许多东西,也有许多东西对大家有用,也发到首页上来了. 本来是没有1.36的,先把基于静态收集依赖的1.4设计出来后,发现改动太多,为了平缓升级起见,才减少了一部分新特 ...

  8. 迷你MVVM框架 avalonjs 0.81发布

    本版本最大的改进是将视图的操作全鄣变成异步.详情如下: 管道符与短路与相混淆的BUG 重构on绑定,省得每次都重复绑定同一个事件回调.虽然addEventListener或attachEvent会忽略 ...

  9. 迷你MVVM框架 avalonjs 0.8发布

    本版本最重要的特性是引进了AMD规范的模块加载器,亦即原来mass Framework 的并行加载器, 不同之处,它引进了requirejs的xxx!风格的插件机制,比如要延迟到DOM树建完时触发,是 ...

随机推荐

  1. pdf2htmlEx安装及测试

    pdf2htmEx转换效果优秀,可以将pdf转换为html文件,转换速度很快 有两种输出形式, 1.一个pdf对应一个html文件,转换出来的html文件较大 2.一个pdf对应多个html页面,且可 ...

  2. Domino----The Address Book does not contain a cross certificate capable of validating the public key.

    The Address Book does not contain a cross certificate capable of validating the public key. 地址本不包含交叉 ...

  3. select2插件不兼容ie7,ie7下样子显示错位问题

    1.源文件(未修改) select2.min.css.select2.min.js 2.ie7下显示样式: 3.ie8下显示样式: 4.经查看发现ie7下对一些属性的解析和ie8不同,需对ie7另作h ...

  4. xpath使用

    一.安装(win7 64) 1.安装lxml,pip install lxml 2.如果安装出错,下载lxml-3.5.0b1.win-amd64-py2.7.exe: 地址:http://www.l ...

  5. [题解+总结]NOIP2010-2015后四题汇总

    1.前言 正式开始的第一周的任务--把NOIP2010至NOIP2015的所有D1/2的T2/3写出暴力.共22题. 暴力顾名思义,用简单粗暴的方式解题,不以正常的思路思考.能够较好的保证正确性,但是 ...

  6. AR初体验:宣传G20

    最近Pokemon Go太火,它基于LBS(Location Based Service)+AR(Augmented Reality)的一款游戏,这股风,一定会让国内的公司纷纷效仿,你懂的.不可否定的 ...

  7. canvas小结

    前段时间在公司没什么事干,研究了一下canvas,在实际开发中还没正式应用过,但是已经深深感觉到其魅力之处.下面写一写我认为canvas中比较重要的点,如有理解错误,欢迎指正. 首先canvas是h5 ...

  8. RN 项目导入WebStorm 组件没有依赖

    你需要在项目根目录   $ npm instal 恭喜你完成 但是依然报错 npm config set registry="registry.npmjs.org"  重新配置了一 ...

  9. ruby开发环境配置

    环境:win7 64位 软件:Ruby2.2.5,devkit对应版本,rubygems,rails 一:安装Ruby 1.在这个网站:http://rubyinstaller.org/downloa ...

  10. int.Parse()之异常

    首先对不起各位读者,我必须发泄一下,为了清明节能顺利的去洛阳玩,我周四赶出了一篇博客,就在快写完提交的时刻,11:30分断网了!!断网了!!断!!网!!了!!是的,你没有听错,他就这样毫无征兆的断网了 ...