过年前后一段时间,对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

link js重构心得的更多相关文章

  1. JS重构分页

    JS重构分页 很早以前写过一个Jquery分页组件,但是当时写的组件有个缺点,当时的JS插件是这样设计的:比如:点击  -->  查询按钮 ---> 发ajax请求 返回总页数和所有数据, ...

  2. 微信小程序js学习心得体会

    微信小程序js学习心得体会 页面控制的bindtap和catchtap 用法,区别 <button id='123' data-userDate='100' bindtap='tabMessag ...

  3. 用vue.js重构订单计算页面

    在很久很久以前做过一个很糟糕的订单结算页面,虽然里面各区域(收货地址)使用模块化加载,但是偶尔会遇到某个模块加载失败的问题导致订单提交的数据有误. 大致问题如下: 1. 每个模块都采用usercont ...

  4. 【学习笔记】node.js重构路由功能

    摘要:利用node.js模块化实现路由功能,将请求路径作为参数传递给一个route函数,这个函数会根据参数调用一个方法,最后输出浏览器响应内容 1.介绍 node.js是一个基于Chrome V8引擎 ...

  5. js学习心得之思维逻辑与对象上下文环境(一)

    html5 canvas矩形绘制实例(绘图有js 实现) html: <canvas id="myCanvas" width="200" height=& ...

  6. JS读书心得:《JavaScript框架设计》——第12章 异步处理

    一.何为异步   执行任务的过程可以被分为发起和执行两个部分. 同步执行模式:任务发起后必须等待直到任务执行完成并返回结果后,才会执行下一个任务. 异步执行模式:任务发起后不等待任务执行完成,而是马上 ...

  7. Node.js学习心得

    最近花了三四周的时间学习了Node.js ,感觉Node.js在学习过程中和我大学所学的专业方向.NET在学习方法上有好多的相似之处,下面就将我学习的心得体会以及参考的资料总结归纳如下,希望对于刚入门 ...

  8. echarts.js使用心得--demo

    首先要感谢一下我的公司,因为公司需求上面的新颖(奇葩)的需求,让我有幸可以学习到一些好玩有趣的前端技术. 废话不多时 , 直接开始. 第一步: 导入echarts.js文件 下载地址:http://e ...

  9. 新手入门学习angular.js的心得体会

    看了一天的angular.js,只要记住这是关于双向数据绑定 和单向数据绑定就可以,看看开发文档,短时间内还是可以直接入手的,看个人理解能力(我是小白). 这几天开始着手学习angularjs的有关知 ...

随机推荐

  1. Spring学习---JPA学习笔记

    用了一段时间的Spring,到现在也只是处于会用的状态,对于深入一点的东西都不太了解.所以决定开始深入学习Spring. 本文主要记录JPA学习.在学习JPA之前,需要了解一些ORM的概念. ORM概 ...

  2. Warning: Attempt to present on whose view is not in模态跳转问题

    错误分析:            controller A present controller B ,前提是A的view要存在,如果不存在,就会报这个错.   解决方法:             将 ...

  3. ios UIKit动力 分类: ios技术 2015-07-14 12:55 196人阅读 评论(0) 收藏

    UIkit动力学是UIkit框架中模拟真实世界的一些特性. UIDynamicAnimator 主要有UIDynamicAnimator类,通过这个类中的不同行为来实现一些动态特性. 它一般有两种初始 ...

  4. 查找 SqlServer死锁

    use master if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[sp_who_lock]') ) dr ...

  5. redhat在线安装chrome浏览器

    开始的时候是参考吹尽黄沙始到金的文章http://www.cnblogs.com/effective/archive/2012/03/18/2405189.html 1.创建一个文件/etc/yum. ...

  6. RAMOS系统 WIN7+VHD+GURB map

    转载(并未验证) 前段时间加了一个内存条,将笔记本内存升级到了6G,由于之前用的是32位的win7不可以直接使用6G内存,便装了64位的系统.网上找资源的时候发现,大内存可以使用RamOS,从内存中虚 ...

  7. 用scala实现一个基于TCP Socket的快速文件传输程序

    这是用scala实现的一个简单的文件传输程序. 服务端 package jpush import java.io.{DataInputStream, File, FileOutputStream} i ...

  8. PHP文件上传主要代码讲解

    导读:在php开发过程中,文件上传也经常用到,这里简单介绍下. 在php开发过程中,文件上传也经常用到,这里简单介绍下. 代码如下: <?php    if($_FILES['myfile'][ ...

  9. Emmet插件详解

    http://www.ithao123.cn/content-10512551.html   (webstorm的css编写插件)Emmet:HTML/CSS代码快速编写神器 [摘要:Emmet的前身 ...

  10. 字符集 ISO-8859-1(3)

    详细见 http://www.w3school.com.cn/tags/html_ref_urlencode.html