过年前后一段时间,对 link 库的代码进行的大量的重构,代码精简了许多,性能也得到了很大的改善,写此文记录期间所做的改进和重构,希望对看到此文的 js 程序员有所帮助。
1. 代码构建
最初代码使用 gulp 结合 concat 等插件组合文件生成库文件, 现在用的是 rollup , 号称是下一代 js 模块打包器, 结合 buble 插件将 es6 代码编译为 es5 , 和 cleanup 插件删除不必要的注释和空行。因为后面大部分代码迁移到了 es6 和标准的模块化语法 (import ,export) , 使用 rollup 会自动分析哪些模块甚至模块中的哪个方法是否需要打包入最终的库文件,这样后面新建模块或添加方法,如果后面因为重构导致模块或方法不再使用的时候 ,rollup 会使用 tree-shaking 技术将其剔除。 对 rollup 感兴趣的可以参考
2. 类型定义使用 es6 class
此前都是使用 function 结合 prototype 定义类型和原型方法,es6 class 其实本身也是 function 结合 prototype 的语法糖,但是使用 class 所有原型,静态,getter,setter 都包含在 class 中,代码更清晰可读。
View Code
- 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 提醒参考链接
(The function invocation can be written by
and
- Function.prototype.call()
. But
- Function.prototype.apply()
and
- Function.prototype.call()
are slower than the normal function invocation). 目前整个源码大概只有一处不得已使用了 apply。
- Function.prototype.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 过后的数组依然可以和转变前一样使用其他未经拦截的原型方法。
View Code
- 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();
- };
View Code
- 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 多行,有兴趣的可以学习并自行扩展 , 下面配上在我电脑上跑的性能测试结果
性能测试代码
来源: http://www.cnblogs.com/leonwang/p/6400941.html