过年前后一段时间,对link库的代码进行的大量的重构,代码精简了许多,性能也得到了很大的改善,写此文记录期间所做的改进和重构,希望对看到此文的js程序员有所帮助。
1. 代码构建
最初代码使用gulp 结合concat 等插件组合文件生成库文件, 现在用的是rollup ,号称是下一代js模块打包器, 结合buble 插件将es6代码编译为es5 , 和cleanup插件删除不必要的注释和空行。因为后面大部分代码迁移到了es6和标准的模块化语法(import ,export) ,使用rollup 会自动分析哪些模块甚至模块中的哪个方法是否需要打包入最终的库文件,这样后面新建模块或添加方法,如果后面因为重构导致模块或方法不再使用的时候 ,rollup会使用tree-shaking技术将其剔除。 对rollup感兴趣的可以参考 http://rollupjs.org/
2.类型定义使用es6 class
此前都是使用function结合prototype定义类型和原型方法,es6 class 其实本身也是function结合prototype的语法糖,但是使用class 所有原型,静态,getter,setter都包含在class中,代码更清晰可读。
export default class Link { constructor(el, data, behaviors, routeConfig) { this.el = el; this.model = data; this._behaviors = behaviors; this._eventStore = []; this._watchFnMap = Object.create(null); this._watchMap = Object.create(null); this._routeEl = null; this._comCollection = []; this._unlinked = false; this._children = null; // store repeat linker this._bootstrap(); if (routeConfig) { this._routeTplStore = Object.create(null); configRoutes(this, routeConfig.routes, routeConfig.defaultPath); } if (glob.registeredTagsCount > 0 && this._comCollection.length > 0) { this._comTplStore = Object.create(null); this._renderComponent(); } } _bootstrap() { var $this = this; if (!this.model[newFunCacheKey]) { Object.defineProperty(this.model, newFunCacheKey, { value: Object.create(null), enumerable: false, configurable: false, writable: true }); } this._compileDOM(); this._walk(this.model, []); this._addBehaviors(); } _walk(model, propStack) { var value, valIsArray, watch, $this = this; each(Object.keys(model), function (prop) { value = model[prop]; valIsArray = Array.isArray(value); if (isObject(value) && !valIsArray) { propStack.push(prop); $this._walk(value, propStack); propStack.pop(); } else { watch = propStack.concat(prop).join('.'); if (valIsArray) { interceptArray(value, watch, $this); $this._notify(watch + '.length', value.length); } $this._defineObserver(model, prop, value, watch, valIsArray); $this._notify(watch, value); } }); } }
3.尽可能少的使用Function.prototype.call, Function.prototype.apply .
如果可以避免,尽量不要使用call和apply执行函数, 此前link源码为了方便大量使用了call和apply , 后面经过eslint提醒加上自己写了测试, 发现普通的函数调用比使用call ,apply性能更好。 eslint 提醒参考链接
http://eslint.org/docs/rules/no-useless-call (The function invocation can be written by Function.prototype.call()
and Function.prototype.apply()
. But Function.prototype.call()
andFunction.prototype.apply()
are slower than the normal function invocation). 目前整个源码大概只有一处不得已使用了apply。
4. 使用Object.create(null)创建字典
通常我们使用var o={} 创建空对象,这里o其实并不是真正的空对象,它继承了Object原型链中的所有属性和方法,相当于Object.create(Object.prototype), Object.create(null) 创建的对象,原型直接设置为null, 是真正的空对象,更加轻量干净, link中所有字典对象都是通过这种方式创建。
5. 删除了所有内置filter
内置的phone ,money, uppper,lower 4个filter被移除, 就目前自己开发这么久的经验, 觉得angular 等库根本就不需要提供自带的filter , 因为每个公司都是不同的业务, 基本上所有的filter还是会全新自定义一套,为了库更加精简,果断删除,并保留用户自定义filter的接口。
6. 缓存一切需要重复创建和使用的对象。
之前link在遍历dom扫描事件指令时, 直接使用new Function生成事件函数,但是对于列表,其实每一列html完全相同,所以会重复生成逻辑一致的事件处理函数,当列表数据量增大时,这种重复工作会极大的影响性能,其实在生成第一个html片段时所有事件都已经生成过一次,后面只需复用即可,唯一需要处理的每个事件函数绑定的model 不一样, 所有这里可以用闭包保存一份model引用即可。
在改进之前我的电脑跑/demo/perf.html渲染300行列表数据的大概需要300ms, 改进后大概只需130ms左右。
function genEventFn(expr, model) { var fn = getCacheFn(model, expr, function () { return new Function('m', '$event', `with(m){${expr}}`); }); return function (ev) { fn(model, ev); } }
7. 如果可能,尽可能的延迟创建对象
还是以以上事件处理为例子, 其实在用户点击某个按钮触发dom事件前, 事件处理函数fn 本身是不存在的,用户点击时会通过new Function动态创建事件处理函数并保存在Object.create(null)创建的字典中,然后才执行真正的事件处理函数, 下次用户再点击按钮,则会从字典中取出函数并执行, 对于其他的列表项, 对于相同的指令定义的事件,都会复用以上用户第一次点击时创建的那个处理函数,我们要相信用户打开一个页面后,通常不会把所有可点击的东西都点击一次的,这样未被用户碰过的事件处理函数就根本不会创建:)
8. 用===代替==
大家应该都知道用===性能优于== , ==会隐式的进行对象转换,然后比较, link源码全部使用===进行相等比较。
9. 操作文档片段进行批量DOM插入
对于列表渲染,如果每次生成一个DOM元素就立即插入到文档,那么会导致文档大量的进行重绘和重排操作,大家都知道DOM操作是很耗时的, 这时可以创建DocumentFragment对象,对其进行DOM的增删改查, 处理到最后, 再并入到真实的DOM即可, 这样就可避免页面做大量的重复渲染。
var docFragment = document.createDocumentFragment(); each(lastLinks, function (link) { link.unlink(); }); lastLinks.length = 0; each(arr, function (itemData, index) { repeaterItem = makeRepeatLinker(linkContext, itemData, index); lastLinks.push(repeaterItem.linker); docFragment.appendChild(repeaterItem.el); }); comment.parentNode.insertBefore(docFragment, comment);
10 对数组处理的改变
此前在对model进行observe的时候,碰到数组,会将其转换为WatchArray , WatchArray会重新定义'push', 'pop', 'unshift', 'shift', 'reverse', 'sort', 'splice' 这些会改变数组的操作方法,后面删除了WatchArray, 直接对数组对象定义这些方法,以拦截
数组对象直接调用Array原型方法,并通知改变,这样Observe过后的数组依然可以和转变前一样使用其他未经拦截的原型方法。
function WatchedArray(watchMap, watch, arr) { this.watchMap = watchMap; this.watch = watch; this.arr = arr; } WatchedArray.prototype = Object.create(null); WatchedArray.prototype.constructor = WatchedArray; WatchedArray.prototype.notify = function (arrayChangeInfo) { notify(this.watchMap, this.watch, arrayChangeInfo); }; WatchedArray.prototype.getArray = function () { return this.arr.slice(0); }; WatchedArray.prototype.at = function (index) { return index >= 0 && index < this.arr.length && this.arr[index]; }; each(['push', 'pop', 'unshift', 'shift', 'reverse', 'sort', 'splice'], function (fn) { WatchedArray.prototype[fn] = function () { var ret = this.arr[fn].apply(this.arr, arguments); this.notify([fn]); return ret; }; }); WatchedArray.prototype.each = function (fn, skips) { var that = this.arr; each(that, function () { fn.apply(that, arguments); }, skips) }; WatchedArray.prototype.contain = function (item) { return this.arr.indexOf(item) > -1; }; WatchedArray.prototype.removeOne = function (item) { var index = this.arr.indexOf(item); if (index > -1) { this.arr.splice(index, 1); this.notify(['removeOne', index]); } }; WatchedArray.prototype.set = function (arr) { this.arr.length = 0; this.arr = arr; this.notify(); };
each(interceptArrayMethods, function(fn) { arr[fn] = function() { var result = Array.prototype[fn].apply(arr, arguments); linker._notify(watch, arr, { op: fn, args: arguments }); linker._notify(watch + '.length', arr.length); return result; }; });
11. 尽量使用原生函数
比如字符串trim, Array.isArray等原生函数性能肯定会优于自定义函数,前提是你知道你的产品要支持的浏览器范围并进行合适的处理。
经过大量的重构和改写,目前link 性能已经大幅提高,代码行数也保存在990多行,有兴趣的可以学习并自行扩展 https://github.com/leonwgc/link, 下面配上在我电脑上跑的性能测试结果
性能测试代码 https://github.com/leonwgc/todomvc-perf-comparison