过年前后一段时间,对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. 更多文章请关注公众号:FullStackPlan 或前往个人主页:www.linbingdong.com

    个人主页:www.linbingdong.com 扫一扫关注公众号: FullStackPlan 获取更多干货哦~

  2. BZOJ 2209: [Jsoi2011]括号序列 [splay 括号]

    2209: [Jsoi2011]括号序列 Time Limit: 20 Sec  Memory Limit: 259 MBSubmit: 1111  Solved: 541[Submit][Statu ...

  3. iOS强制切换横屏、竖屏

    切换横竖屏最直接的方式是调用device的setOrientation方法.但是从sdk3.0以后,这个方法转为似有API,如果要上AppStore的话,要慎用! if ([[UIDevice cur ...

  4. struts2拦截器-自定义拦截器,放行某些方法(web.xml配置)

    一.web.xml配置 <filter> <filter-name>encodingFilter</filter-name> <filter-class> ...

  5. PHP利用数组构造JSON

    问题起因 以往都是直接用构造数组的形式构造json 例子: $arr = array("A"=>"1","B"=>"2 ...

  6. cocoaPods的安装使用 以及 Carthage

    http://cnbin.github.io/blog/2015/05/25/cocoapods-an-zhuang-he-shi-yong/ 按照这个步骤就OK Note:当引入已有的project ...

  7. 阿里云安装wordpress遇到的问题

    在阿里云服务器上安装Nginx,php5.3.3环境,使用阿里云的RDS数据库 1,安装wordpress,提示您的PHP似乎没有安装运行WordPress所必需的MySQL扩展 解决方案:移除已经安 ...

  8. UVa 11129 - An antiarithmetic permutation

    题目大意:给一个正整数n,构造一个0...n-1的排列,使得这个排列的任何一个长度大于2的子序列都不为等差数列. 把序列按照奇偶位置分成两个序列,这样在两个序列间就不会形成等差数列了,然后再对这两个序 ...

  9. 11.TCP的交互数据流

          TCP报文段一般有两类,分别是成块数据和交互数据. 1.交互式输入     Rlogin连接上键入一个交互命令的数据流如下图所示.     每一个交互按键都会产生一个数据分组,每次从客户传 ...

  10. iOS 开源库介绍

    1. Github-iOS备忘 2. iOS 第三方开源库的吐槽和备忘 3. 移动开发的后台服务支持平台 4. iOS 开源库 之 AFNetWorking 2.x 5. iOS 之 二维码 ZXin ...