clusterize.js 开源的长列表渲染库

1 前言

1.1 主要思想

  1. 不渲染所有的 DOM
  2. 它将列表拆分为 集群,然后显示当前滚动位置的元素
  3. 并在列表的顶部和底部添加额外的行来模拟表格的全高度(这样浏览器就会像显示完整列表一样显示滚动条 )

1.2 用法

<div id="scrollArea" ...>
  <table>
    <tbody id="contentArea" ...>
      <tr>...</tr>
    </tbody>
  </table>
</div>
var data = ['<tr>…</tr>', '<tr>…</tr>', …];
var clusterize = new Clusterize({
  rows: data,
  scrollId: 'scrollArea',
  contentId: 'contentArea'
});

1. options

Name Require 描述
rows 取决于 1. 存在现有的标记就不需要;2. 通过接收数据渲染则需要
scrollId 必需 用作滚动区域的父标签的 Id 或 DOM 节点
contentId 必需 将放置内容的标签的 ID 或 DOM 节点
rows_in_block 可选的 块中的行数 默认50
blocks_in_cluster 可选的 集群中的块数 默认4
tag 可选的 支持的元素:间隔额外的行,空数据行 默认空
show_no_data_row 可选的 无数据时是否显示“空”占位符行 默认true
no_data_class 可选的 无数据时的占位符内容 默认“无数据”
no_data_text 可选的 无数据时的占位符类名 默认“clusterize-no-data”
keep_parity 可选的 添加额外的标签以保持行的奇偶性 默认 true
callbacks 可选的
  • callbacks 的属性
    • clusterWillChange:新集群替换旧集群之前触发
    • clusterChanged:新集群替换旧集群后触发
    • scrollingProgress:滚动时触发,返回滚动进度位置

2. methods

Name 参数 描述
.update() array 数据更新时列表更新
.append() array 追加新数据到列表中
.prepend() array 添加新数据到列表中
.refresh() boolean 刷新行高 Clusterize必须始终知道当前行高
.getRowsAmount() 获取总行数
.getScrollProgress() 获取当前滚动进度
.clear() 清除列表
.destroy() boolean 销毁 clusterize实例 参数为true时从列表中删除所有数据,不指定or false则将所有隐藏数据插入列表

2 源码分析

2.1 准备工作

1. IIFE 立即执行函数 -- 避免污染全局环境

  • 自执行匿名函数 (function() {})()
  1. 匿名函数 function() {} 拥有独立的词法作用域
  2. 再一次使用 () 创建了一个立即执行函数表达式,js 引擎到此将直接执行函数
;(function() {})()

闭包前写 ; 是为了防止代码压缩时,前面代码没写 ; 造成报错

2. 匿名函数 -- 定义一个适应所有环境的 js 模块

function(name, definition) {
  if(typeof module != 'undefined') {
    module.exports = definition()
  } else if(typeof define == 'function' && typeof define.amd == 'object') {
    define(definition)
  } else {
    this[name] = definition()
  }
}
  • typeof module != 'undefined' :使用 Commonjs 规范导出,能在 nodejs 中引入
  • typeof define == 'function' && typeof define.amd == 'obj':使用 AMD 规范导出,能在浏览器引入
  • 其它:直接放在全局对象上

3. 传入的 definition 函数

function () {
  // 检测ie版本
  var ie = (function(){ /** ... */ } ())
  // 检测mac版本
  var is_mac = ...

  //  核心代码
  var Clusterize = function(data) {..}
  Clusterize.prototype = {..}

  // 解决兼容问题的函数
  // 用于 绑定事件 的函数
  function on(evt, element, fnc) { /** ... */ }
  // 用于 解绑事件 的函数
  function off(evt, element, fnc) { /** ... */ }
  // 用于判断是否数组
  function isArray(arr) { /** ... */ }
  // 用于获取元素的样式
  function getStyle(prop, elem) { /** ... */ }
}

2.2 核心代码

1. 自定义构造函数方式创建对象

var Clusterize = function(data) {}
Clusterize.prototype = {}
  • 构造函数上主要定义 -- 被实例对象访问的属性和方法
  • 原型对象上主要定义 -- 被构造函数访问的方法

2. Clusterize 构造函数

流程
  1. 定义全局属性 options -- 使用传入的值 | 设置默认值
  2. 获取传入的 scrollId contentId 绑定的元素
  3. tabindex 令浏览器关注滚动列表
  4. 定义私有参数 rows cache scroll_top
  5. 添加初始数据
  6. 恢复滚动位置
  7. 添加 scroll 事件 resize 事件
  8. 定义全局方法
预备
  1. this instanceof Clusterize
  • 通过 this instanceof xxx 来判断有没有用 new 关键词调用

Clusterize 的原型对象在 this 的原型链上

  • 为了防止不是 new Clusterize() 方式创建对象而是直接 Clusterize() 调用函数出现错误
if(!(this instanceof Clusterize)) return new Clusterize(data);
  1. var self = this;
  • 很常见的避免 this 指向不明确不能直接拿到实例上的属性
属性
  1. self.options
    • 为实例创建时没设置的属性设置默认值 + 设置的属性值 写入 self.options
  2. self.scollId_elem self.contentId_elem
    • 获取传入的 scrollId contentId 绑定的元素
  3. self.contentId_elem 元素设置属性 tabindex
    • 使用 tabindex 使得用户能够通过键盘 tab 键关注(聚焦)滚动列表
  4. scrollTop 获取 self.contentId_elem 元素顶部与可视区顶部的距离
  1. scroll 事件

    • 修复了Mac上的滚动问题--使用防抖 只触发50ms内的最后一次滚动
    • 集群号发生变化时 -- 检测数据变化并插入到DOM
    • 有传入滚动时触发回调,则返回滚动进度位置
  2. resize 事件

    • 设置开关 -- 只触发 100ms 内的最后一次
方法
  1. destory()

    • 事件解绑
    • 置空列表数据 | 将所有隐藏数据插入列表
  2. refresh()

    • 单行高有变化时 || 强制刷新行高时 --> 更新列表

怎样判断行高是否变化呢?

  1. update()

    1. 获取滚动进度暂存
    2. 行数 * 单行高 < 元素顶部与可视区顶部的距离 时
      • 重置滚动距离为0 重置当前集群号为0
    3. 检测数据变化并插入到DOM
    4. 恢复滚动进度
  2. 其他方法

  • clear()
  • getRowsAmount()
  • getScrollProgress()
  • append()
  • prepend()

3. Clusterize.prototype 原型对象

1. getClusterNum() 获取当前集群块编号
/**
 * 获取当前集群块编号
 * @param {array} rows 构造函数传入的数据列表
 * @return {string} 当前集群块编号
 * */ 
  1. 当前集群号 = 滚动距离 / (集群总高度 - 单块的高度)
  2. 最大的集群数 = (行数 * 单行高) / (集群总高度 - 单块的高度)
  3. 返回 min(当前集群号, 最大的集群数)

每滚动4-1块,集群编号+1

2. insertToDOM() 检测数据变化并插入到DOM

初始化时 翻页 页码发生改变时 集群号改变时 触发

/**
 * 检测数据变化并插入到DOM
 * @param {array} rows 接收到的数据列表
 * @param {object} cache 当前展示的数据
 * @return {object} 
 * */ 
  1. 获取一个集群的高度
  2. 比较 当前展示的数据接收到的数据 的变化
    • 当前集群的数据是否变化
    • 集群以上未渲染数据的高度 是否变化
    • 集群以下未渲染数据的高度 是否变化
  3. 当前集群的数据变化 || 集群以上未渲染数据的高度变化时 写入 content_elem 的后代元素
    1. 存在 集群以上未渲染数据的高度变化 时-- 创建标签
      • 需要添加额外的标签时,创建额外的行
      • layout.push(<tr class="clusterize-extra-row clusterize-top-space" style="集群以上未渲染数据的高度"></tr>)
    2. 创建标签 <tr>当前集群的数据</tr>
    3. 存在 集群以下未渲染数据 时,创建标签
      • layout.push(<tr class="clusterize-extra-row clusterize-top-space" style="集群以下未渲染数据的高度"></tr>)
    4. 触发 传入的回调的函数 -- 替换前触发函数
    5. 设置 content_elem 元素的后代元素:this.html(layout.join(''))
    6. css 相关
    7. 触发 传入的回调的函数 -- 替换后触发函数
  4. 集群以下未渲染数据的高度变化 时 写入 content_elem 的后代元素
    • content_elem 元素追加子节点 <tr style="集群以下未渲染数据的高度"></tr>
  1. 怎样写入后代元素
  2. 怎样获取 集群以上未渲染数据,集群以下未渲染数据,需要展示的数据 ?
3. html() 设置 content_elem 元素的后代元素
/**
 * 将数据列表写入tbody -- 适配 ie <= 9不允许对表元素使用 innerHTML
 * @param {array} data 数据列表
 * @return void
 * */ 
  • content_elem.innerHTML = data
兼容 ie9以下版本

ie <= 9不允许对表元素使用 innerHTML

  1. 使用 ele.removeChild()content_elem 的子元素全部移除
  2. 将包含所有子元素的字符串截取出来 --> 转化为数组
  3. 使用 ele.appendChild()content_elem 的子元素写入
4. generate() 为当前滚动位置生成集群
/**
 * 为当前滚动位置生成集群
 * @param {array} rows 接收到的数据列表
 * @return {object} { 集群以上未渲染数据,集群以下未渲染数据,开始的行数(不确定),需要展示的数据 }
 * */ 
  1. 数据行数 < 一块的行数 时,返回 {0, 0, 0, 当前数据 || 生成空行}
  2. 否则
    1. 开始的行数(序列号) = (总行数 - 单块行数) * 当前集群块编号 || 0
      • 开始行数为0时,序列号为1
    2. 结束的行数 = 开始的行数 + 单块行数
    3. 隐藏的上半部分高度 = 开始的行数 * 单行高 || 0
    4. 隐藏的下半部分高度 = (总行数-结束的行数) * 单行高 || 0
    5. 返回 {, , , rows[开始的行数--结束的行数]}
/* Clusterize.js - v0.19.0 - 2021-12-19
 http://NeXTs.github.com/Clusterize.js/
 Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */

;(function(name, definition) {
  // 定义一个适应所有环境的js模块
  if (typeof module != 'undefined') module.exports = definition();
  else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);
  else this[name] = definition();
}('Clusterize', function() {
  "use strict"

  // 检测ie9及以下版本 检测mac -- 为后续兼容判断做准备
  var ie = (function(){
    for( var v = 3,
             el = document.createElement('b'),
             all = el.all || [];
         el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
         all[0];
       ){}
    return v > 4 ? v : document.documentMode;
  }()),
  is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
  var Clusterize = function(data) {
    if( ! (this instanceof Clusterize))
      return new Clusterize(data);
    var self = this;

    var defaults = {
      rows_in_block: 50,
      blocks_in_cluster: 4,
      tag: null,
      show_no_data_row: true,
      no_data_class: 'clusterize-no-data',
      no_data_text: 'No data',
      keep_parity: true,
      callbacks: {}
    }

    // 001 定义全局属性 -- 使用传入的值 | 设置默认值
    self.options = {};
    var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];
    for(var i = 0, option; option = options[i]; i++) {
      self.options[option] = typeof data[option] != 'undefined' && data[option] != null
        ? data[option]
        : defaults[option];
    }
    // 002 获取传入的 scrollId contentId 绑定的元素
    var elems = ['scroll', 'content'];
    for(var i = 0, elem; elem = elems[i]; i++) {
      self[elem + '_elem'] = data[elem + 'Id']
        ? document.getElementById(data[elem + 'Id'])
        : data[elem + 'Elem'];
      if( ! self[elem + '_elem'])
        throw new Error("Error! Could not find " + elem + " element");
    }

    // 003 tabindex 能够使用 tab 键选中滚动列表
    if( ! self.content_elem.hasAttribute('tabindex'))
      self.content_elem.setAttribute('tabindex', 0);

    // 私有参数
    var rows = isArray(data.rows)
        ? data.rows
        : self.fetchMarkup(),
      cache = {},
      scroll_top = self.scroll_elem.scrollTop;

    // 004 添加初始数据
    self.insertToDOM(rows, cache);

    // 005 恢复滚动位置
    self.scroll_elem.scrollTop = scroll_top;

    // last_cluster 当前的集群号
    var last_cluster = false,
    scroll_debounce = 0,
    pointer_events_set = false,
    scrollEv = function() {
      // 修复了Mac上的滚动问题--使用防抖 只触发50ms内的最后一次滚动
      if (is_mac) {
          if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';
          pointer_events_set = true;
          clearTimeout(scroll_debounce);
          scroll_debounce = setTimeout(function () {
              self.content_elem.style.pointerEvents = 'auto';
              pointer_events_set = false;
          }, 50);
      }
      // 集群号发生变化时,检测数据变化并插入到DOM
      if (last_cluster != (last_cluster = self.getClusterNum(rows)))
        self.insertToDOM(rows, cache);
      // 有传入滚动时触发函数,返回滚动进度位置
      if (self.options.callbacks.scrollingProgress)
        self.options.callbacks.scrollingProgress(self.getScrollProgress());
    },
    resize_debounce = 0,
    resizeEv = function() {
      clearTimeout(resize_debounce);
      resize_debounce = setTimeout(self.refresh, 100);
    }
    // 006 添加scroll事件 resize事件
    on('scroll', self.scroll_elem, scrollEv);
    on('resize', window, resizeEv);

    // 007 定义全局方法
    self.destroy = function(clean) {
      off('scroll', self.scroll_elem, scrollEv);
      off('resize', window, resizeEv);
      // clean为true则置空, 否则将所有隐藏数据插入列表
      self.html((clean ? self.generateEmptyRow() : rows).join(''));
    }
    self.refresh = function(force) {
      // 单行高有变化时(初始化时) || 强制刷新行高时 --> 更新列表
      if(self.getRowsHeight(rows) || force) self.update(rows);
    }
    self.update = function(new_rows) {
      rows = isArray(new_rows)
        ? new_rows
        : [];
      var scroll_top = self.scroll_elem.scrollTop;  // 获取滚动距离
      // 行数 * 单行高 < 滚动距离 时,即单屏可展示完所有数据
      if(rows.length * self.options.item_height < scroll_top) {
        // 重置滚动距离为0 重置当前集群号为
        self.scroll_elem.scrollTop = 0;
        last_cluster = 0;
      }
      // 检测数据变化并插入到DOM
      self.insertToDOM(rows, cache);
      // 恢复滚动进度
      self.scroll_elem.scrollTop = scroll_top;
    }
    self.clear = function() {
      self.update([]);
    }
    self.getRowsAmount = function() {
      return rows.length;
    }
    self.getScrollProgress = function() {
      return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;
    }

    var add = function(where, _new_rows) {
      var new_rows = isArray(_new_rows)
        ? _new_rows
        : [];
      if( ! new_rows.length) return;
      rows = where == 'append'
        ? rows.concat(new_rows)
        : new_rows.concat(rows);
      // 检测数据变化并插入到DOM
      self.insertToDOM(rows, cache);
    }
    self.append = function(rows) {
      add('append', rows);
    }
    self.prepend = function(rows) {
      add('prepend', rows);
    }
  }

  Clusterize.prototype = {
    constructor: Clusterize, // 构造者对象指向 Clusterize
    /**
     * 不是通过rows参数传入数据时,获取子标签列表
     * this.content_elem: <tbody>...</tbody>
     * rows: [<tr>...</tr>, ..., <tr>...</tr>]
     */
    fetchMarkup: function() {
      var rows = [], rows_nodes = this.getChildNodes(this.content_elem);
      while (rows_nodes.length) {
        rows.push(rows_nodes.shift().outerHTML);
      }
      return rows;
    },
    /**
     * 获取标签名 内容标签名称 标签高度 计算集群高度 
     * @param {array} rows 数据列表
     * @param {array} cache 缓存数据--上一秒的渲染数据
     * @return {object} void
     * */ 
    exploreEnvironment: function(rows, cache) {
      var opts = this.options;
      // 标签名 -- toby
      opts.content_tag = this.content_elem.tagName.toLowerCase();
      if( ! rows.length) return;
      // 内容标签名 -- tr
      if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
      // 行数 <= 1时,缓存中写入 3条重复的
      if(this.content_elem.children.length <= 1) cache.data = this.html(rows[0] + rows[0] + rows[0]);
      // 内容标签名 -- tr
      if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();
      // 计算集群总高 
      this.getRowsHeight(rows);
    },
    /**
     * 写入单行高、单块高、总行数、集群高度+ 判断单行高是否变化
     * @param {array} rows 组件接收到的数据列表
     * @return {boolean} 集群高度变化则返回true
     * */
    getRowsHeight: function(rows) {
      var opts = this.options,
        prev_item_height = opts.item_height;
      opts.cluster_height = 0;
      if( ! rows.length) return;
      var nodes = this.content_elem.children;
      if( ! nodes.length) return;
      // 将子标签分为两部分 node是第二部分的第一个节点
      var node = nodes[Math.floor(nodes.length / 2)];
      opts.item_height = node.offsetHeight;
      /**
       * 考虑表的 border-spacing collapse 合并 separated 分隔 -> 获取相邻单元格边框之间的距离
       * 一行数据的高度 item_height = 元素高度 + 相邻单元格边框之间的距离 = 30
      */
      if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')
        opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0;
      
      /**
       * 考虑外边距
       * 一行数据的高度 item_height = 元素高度 + max(top外边距, bottom外边距)
      */
      if(opts.tag != 'tr') {
        var marginTop = parseInt(getStyle('marginTop', node), 10) || 0;
        var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0;
        opts.item_height += Math.max(marginTop, marginBottom);
      }
      // 一个块的高度 block_height = 单行高度 * 块中的行数 = 30 * 50 = 1500
      opts.block_height = opts.item_height * opts.rows_in_block;

      // 一个集群的行数 rows_in_cluster = 集群中的块数 * 块中的行数 = 4 * 50 = 200
      opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;

      // 一个集群的高度 cluster_height = 集群中的块数 * 单块的高度 = 4 * 1500 = 6000
      opts.cluster_height = opts.blocks_in_cluster * opts.block_height;

      // 原来的单行高 != 现在的单行高 -> undefined != 30 true
      return prev_item_height != opts.item_height;
    },
    /**
     * 获取当前集群编号
     * @param {array} rows 构造函数传入的数据列表
     * @return {string} 当前集群块编号
     * */ 
    getClusterNum: function (rows) {
      var opts = this.options;
      opts.scroll_top = this.scroll_elem.scrollTop; // 滚动的像素数
      // 集群总高度 - 单块的高度
      var cluster_divider = opts.cluster_height - opts.block_height;
      // 当前集群号 = 滚动距离 / (集群总高度 - 单块的高度)
      var current_cluster = Math.floor(opts.scroll_top / cluster_divider);
      // 最大的集群数 = (行数 * 单行高) / (集群总高度 - 单块的高度)
      var max_cluster = Math.floor((rows.length * opts.item_height) / cluster_divider);
      return Math.min(current_cluster, max_cluster);
    },
    // 如果没有提供数据,则生成空行
    generateEmptyRow: function() {
      var opts = this.options;
      if( ! opts.tag || ! opts.show_no_data_row) return [];
      var empty_row = document.createElement(opts.tag),
        no_data_content = document.createTextNode(opts.no_data_text), td;
      empty_row.className = opts.no_data_class;
      if(opts.tag == 'tr') {
        td = document.createElement('td');
        // fixes #53
        td.colSpan = 100;
        td.appendChild(no_data_content);
      }
      empty_row.appendChild(td || no_data_content);
      return [empty_row.outerHTML];
    },
    /**
     * 为当前滚动位置生成集群
     * @param {array} rows 要写入tbody的数据
     * @return {object} {隐藏的上半部分高度,隐藏的下半部分高度,开始的行数(不确定),需要展示的行}
     * */ 
    generate: function (rows) {
      var opts = this.options,
        rows_len = rows.length;
      // 数据行数 < 块中的行数时
      if (rows_len < opts.rows_in_block) {
        return {
          top_offset: 0,
          bottom_offset: 0,
          rows_above: 0,
          rows: rows_len ? rows : this.generateEmptyRow()
        }
      }
      // 开始的行数 = (一个集群的行数 - 一个块的行数) * 当前集群编号 = (200-50)*0 = 0 || (200-50)*1=150
      var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * this.getClusterNum(rows), 0),
        // 结束的行数 = 开始的行数 + 一个集群的行数 = 200
        items_end = items_start + opts.rows_in_cluster,
        // 隐藏的上半部分高度 = 开始的行数 * 一行的高度 = 0 * 30 = 0
        top_offset = Math.max(items_start * opts.item_height, 0),
        // 隐藏的下半部分高度 = (总行数-结束的行数) * 一行的高度
        bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),
        // 需要展示的行
        this_cluster_rows = [],
        rows_above = items_start;
      if(top_offset < 1) {
        rows_above++;
      }
      for (var i = items_start; i < items_end; i++) {
        rows[i] && this_cluster_rows.push(rows[i]);
      }
      return {
        top_offset: top_offset,
        bottom_offset: bottom_offset,
        rows_above: rows_above,
        rows: this_cluster_rows
      }
    },
    // 创建额外的行
    renderExtraTag: function(class_name, height) {
      var tag = document.createElement(this.options.tag),
        clusterize_prefix = 'clusterize-';
      tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');
      height && (tag.style.height = height + 'px');
      return tag.outerHTML;
    },
    /**
     * 检测数据变化并插入到DOM
     * @param {array} rows 要写入tbody的数据
     * @param {object} cache 当前展示的数据
     * @return {object} 
     * */ 
    insertToDOM: function(rows, cache) {
      // !集群高度时,去获取
      if( ! this.options.cluster_height) {
        this.exploreEnvironment(rows, cache);
      }
      var data = this.generate(rows),
        this_cluster_rows = data.rows.join(''),
        // 当前集群的数据是否变化
        this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),
        // 集群以上未渲染数据的高度是否变化
        top_offset_changed = this.checkChanges('top', data.top_offset, cache),
        // 集群以下未渲染数据的高度是否变化
        only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),
        callbacks = this.options.callbacks,
        layout = [];

      // 01 当前集群的数据变化 || 集群以上未渲染数据的高度变化
      if(this_cluster_content_changed || top_offset_changed) {
        // 011 存在 集群以上未渲染数据的高度变化时 -- 创建标签
        if(data.top_offset) {
          // 是否添加额外的标签 && 创建额外的行
          this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));
          // <tr class="clusterize-extra-row clusterize-top-space" style="集群以上未渲染数据的高度"></tr>
          layout.push(this.renderExtraTag('top-space', data.top_offset));
        }
        // 012 创建标签 <tr>当前集群的数据</tr>
        layout.push(this_cluster_rows);
        // 013 创建标签 <tr class="clusterize-extra-row clusterize-bottom-space" style="集群以下未渲染数据的高度"></tr>
        data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));
        // 014 触发 传入的回调的函数 -- 替换前触发
        callbacks.clusterWillChange && callbacks.clusterWillChange();
        // 015 替换 
        this.html(layout.join(''));
        // 016 css相关
          // 有序列表 则要添加序号渲染类名
        this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);
        // this.content_elem { counter-increment: clusterize-counter (data.rows_above-1)}
        // counter-increment: clusterize-counter 0; width: 1599px;
        // counter-increment: clusterize-counter 149; width: 1599px;
        // Increment "clusterize-counter" by data.rows_above-1
        this.content_elem.style['counter-increment'] = 'clusterize-counter ' + (data.rows_above-1);
        // 017 触发 传入的回调的函数 -- 替换后触发 
        callbacks.clusterChanged && callbacks.clusterChanged();
      } else if(only_bottom_offset_changed) {
        // <tr ... style="集群以下未渲染数据的高度"></tr>
        this.content_elem.lastChild.style.height = data.bottom_offset + 'px';
      }
    },
    /**
     * 将数据列表写入tbody -- 适配 ie <= 9不允许对表元素使用 innerHTML
     * @param {array} data 数据列表
     * @return void
     * */ 
    html: function(data) {
      // 当前的 tbody
      var content_elem = this.content_elem;
      if(ie && ie <= 9 && this.options.tag == 'tr') {
        var div = document.createElement('div'), last;
        // div.innerHTML = '<table><tbody><tr>…</tr>...<tr>…</tr></tbody></table>';
        div.innerHTML = '<table><tbody>' + data + '</tbody></table>';
        // 将 content_elem 的子元素全部移除
        while((last = content_elem.lastChild)) {
          content_elem.removeChild(last);
        }
        // [<tr>…</tr>, ..., <tr>…</tr>]
        var rows_nodes = this.getChildNodes(div.firstChild.firstChild);

        // 往 content_elem 添加子元素
        while (rows_nodes.length) {
          content_elem.appendChild(rows_nodes.shift());
        }
      } else {
        content_elem.innerHTML = data;
      }
    },
    getChildNodes: function(tag) {
        var child_nodes = tag.children, nodes = [];
        for (var i = 0, ii = child_nodes.length; i < ii; i++) {
            nodes.push(child_nodes[i]);
        }
        return nodes;
    },
    /**
     * 当前集群的数据是否变化
     * @param {array} value 要写入tbody的数据
     * @param {array} cache[type] 当前写入tbody的数据
     * @return {boolean} 
     * */
    checkChanges: function(type, value, cache) {
      var changed = value != cache[type];
      cache[type] = value;
      return changed;
    }
  }

  // 解决兼容问题的函数
  function on(evt, element, fnc) {
    return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc);
  }
  function off(evt, element, fnc) {
    return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc);
  }
  // 用于判断是否数组
  function isArray(arr) {
    return Object.prototype.toString.call(arr) === '[object Array]';
  }
  // 用于获取元素的样式
  function getStyle(prop, elem) {
    return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
  }

  return Clusterize;
}));
posted on 2022-09-22 10:58  pleaseAnswer  阅读(346)  评论(0编辑  收藏  举报