浅析jQuery EasyUI响应式布局的实现方案

首先解释一下本篇文章标题中提到的“jQuery EasyUI响应式布局”,这里是指EasyUI里布局类组件的fit属性,也就是实现自适应的属性。到了1.4版本,新增了一个宽度百分比的概念,既可 以用在布局类组件上,也可用在表单类组件上,但是其实现方案跟fit是类似的。

也就是说,jQuery EasyUI的自适应布局包含两块内容:

  • 布局类和表格类组件的fit属性设置为true,也就是宽度和高度都100%;
  • 布局类组件,表格类组件和表单类组件的宽度设置为百分比;

因为EasyUI比较复杂的DOM结构设计,导致响应式布局无法使用css里原生的百分比去实现,通常宿主DOM,都会被包装得面目全非。最后组件呈现的时候,全部是以具体的像素值显示的。

1.4版本的代码来做分析,我们看看EasyUI的fluid神秘面纱后的逻辑到底是个什么样的?特别是组件多层嵌套的时候,它是如何做到每个组件都能自适应的。

追本溯源

先来追本溯源,fluid的本质是浏览器窗口调整大小的时候,页面的布局能相应的做调整,而在这个过程中,唯一能利用的事件是window的resize,所以EasyUI响应式布局的实现,一定是在window的resize事件中处理的。

我们在源码中搜索”$(window)”关键字,共搜索到20个左右,但是跟resize事件相关的只有两个地方。一个是panel组件里绑定的resize;另一个是window组件里绑定的resize事件。

window组件绑定的resize

为什么要把window组件放到前面看?那是因为window组件里绑定的resize跟fluid实在没什么关系,来看代码:

  1. $(window).resize(function () {
  2.         $(“body>div.window-mask”).css({width: $(window)._outerWidth(), height: $(window)._outerHeight()});
  3.         setTimeout(function () {
  4.             $(“body>div.window-mask”).css({width: _26d().width, height: _26d().height});
  5.         }, 50);
  6.     });

这个事件处理程序,像个一丝不挂的普通姑娘,没啥内涵,目的单纯而直白——只是为了实时调整window组件的蒙版大小,他的功能相对于fluid来讲实在微不足道,所以我们一眼嫖过去,不做过多讨论。

panel组件绑定的resize

panel组件绑定的resize事件处理程序是相当有内涵的,她绝对不是一丝不挂,而且穿了好几层情趣内衣,我们需要一层一层的扒,需要耐心。先来看事件处理程序的定义:

  1. // 定义的了一个定时器的引用,老鸟们应该都知道他的目的有两个
  2. // 一是防止短时间内多次触发window的resize事件处理程序;
  3. // 二是解决某些浏览器调整一次窗口却多次触发resize事件的问题
  4. var _23f = null;
  5. $(window).unbind(“.panel”).bind(“resize.panel”, function () {
  6.     // 100ms内触发多次的话,则终止对前一次的事件处理程序的调用
  7.     if (_23f) {
  8.         clearTimeout(_23f);
  9.     }
  10.     // 重置定时器
  11.     _23f = setTimeout(function () {
  12.         // 这里主要是看body是否是一个layout实例(是layout的话,body元素上会有layout样式)
  13.         var _240 = $(“body.layout”);
  14.         // 如果body是一个layout,则调用layout的resize方法,
  15.         // layout的resize方法最终调用的也是panel的resize方法
  16.         // 所以layout的resize显然是个多层情趣内裤,我们不急着扒。
  17.         if (_240.length) {
  18.             _240.layout(“resize”);
  19.         } else {
  20.             // 如果body不是一个layout组件,则调用panel组件的doLayout方法
  21.             // 这个层数应该少点,而且地处核心位置,我们先扒这个doLayout
  22.             $(“body”).panel(“doLayout”);
  23.         }
  24.         // 清空定时器
  25.         _23f = null;
  26.     }, 100);
  27. });

当我们的页面上引用了jquery.easyui.min.js这个伪开源的文件之后,这段代码会被执行一次。这段代码虽然不长,但是里面调用了resize和doLayout方法。

layout的resize方法,其底层调用的是panel的reszie方法(这层内衣我迅速的扒了,不信的同学可以自己看layout的代码)。所以,最后的焦点全部落到panel组件的两个方法上: doLayout和resize

doLayout方法

找到panel组件的doLayout代码(搜”doLayout”关键字即可):

  1. function doLayout (jq, all) {
  2.     return jq.each(function () {
  3.         // 缓存this
  4.         var _24a = this;
  5.         // _24b变量判断当前容器是不是body
  6.         var _24b = _24a == $(“body”)[0];
  7.         // find函数的选择器真的很长,最后还用了filter方法来进一步过滤find出来结果
  8.         // 这个写法看起来似乎很简洁明了,但是,他是否高效呢?这个问题先放一放
  9.         // 我们先弄清楚这个变量s到底是什么?这个必须要拿例子来说明,注释里我试了很多方式去表达,都觉得表达不清楚。
  10.         var s = $(this).find(“div.panel:visible,div.accordion:visible,div.tabs-container:visible,div.layout:visible,.easyui-fluid:visible”).filter(function (_24c, el) {
  11.             var p = $(el).parents(“div.panel-body:first”);
  12.             if (_24b) {
  13.                 return p.length == 0;
  14.             } else {
  15.                 return p[0] == _24a;
  16.             }
  17.         });
  18.         // 找到的需要做fluid布局后,触发绑定在他们DOM上自定义的的”_reszie”事件
  19.         s.trigger(“_resize”, [all || false]);
  20.     });
  21. }

对于doLayout函数中的s变量,分两种情况举例子。

如果当前容器是body:

  1. <!– 当前容器 –>
  2. <body>
  3.     <div id=”a1″>
  4.         <div id=”a21″ class=”accordion”>
  5.             <div id=”a3″>
  6.                 <div id=”a4″ class=”accordion”>…</div>
  7.             </div>
  8.         </div>
  9.         <div id=”a22″ class=”accordion”>…</div>
  10.     </div>
  11. </body>

s只包含”a21″和 “a22”,其实也就是离当前容器最近的包含特征样式的子孙级元素。

如果当前元素是div.panel-body:

  1. <div class=”panel-body”>
  2.     <div id=”a1″>
  3.         <div id=”a21″ class=”accordion”>
  4.             <div id=”a3″>
  5.                 <div id=”a4″ class=”accordion”>…</div>
  6.             </div>
  7.         </div>
  8.         <div id=”a22″ class=”accordion”>…</div>
  9.     </div>
  10. </div>

s也只包含”a21″和 “a22″,跟body情况是一样的,只是寻找方式不一样。

这个doLayout函数的目的就比较清楚了: 它负责寻找当前容器的下一级(不一定是子级别,也可能是孙子,重孙子等)需要做响应式布局的组件,然后触发绑定在这些组件上自定义的”_reszie”事件。

由此,我们也可以推断: 每一个含有响应式特性的组件,其DOM结构里面肯定存在一个绑定了自定义的”_resize”事件的事件处理程序。

到这里, “下一级需要做响应式布局的组件”完成了自适应,那么 “下下级需要做响应式布局的组件”(响应式组件多层嵌套)是怎么完成自适应的呢?。

不急,我们还是拿panel组件自定义的”_resize”来看:

  1. function _13(_14) {
  2.     $(_14).addClass(“panel-body”)._size(“clear”);
  3.     var _15 = $(“<div class=\”panel\”></div>”).insertBefore(_14);
  4.     _15[0].appendChild(_14);
  5.     _15.bind(“_resize”, function (e, _16) {
  6.         if ($(this).hasClass(“easyui-fluid”) || _16) {
  7.             // _3函数就是panel组件resize方法的实现
  8.             _3(_14);
  9.         }
  10.         return false;
  11.     });
  12.     return _15;
  13. };

也就是说自定义的”_resize”事件处理程序里,调用组件自身的resize方法,看来想知道“下下级”的响应式布局是如何完成的,必须还是去问resize方法。

resize方法

我们直接看代码:

  1. // panel组件resize方法的实现
  2. function _3(_4, _5) {
  3.     var _6 = $.data(_4, “panel”);
  4.     var _7 = _6.options;
  5.     var _8 = _6.panel;
  6.     var _9 = _8.children(“div.panel-header”);
  7.     var _a = _8.children(“div.panel-body”);
  8.     if (_5) {
  9.         $.extend(_7, {width: _5.width, height: _5.height, minWidth: _5.minWidth, maxWidth: _5.maxWidth, minHeight: _5.minHeight, maxHeight: _5.maxHeight, left: _5.left, top: _5.top});
  10.     }
  11.     _8._size(_7);
  12.     _9.add(_a)._outerWidth(_8.width());
  13.     if (!isNaN(parseInt(_7.height))) {
  14.         _a._outerHeight(_8.height() – _9._outerHeight());
  15.     } else {
  16.         _a.css(“height”, “”);
  17.         var _b = $.parser.parseValue(“minHeight”, _7.minHeight, _8.parent());
  18.         var _c = $.parser.parseValue(“maxHeight”, _7.maxHeight, _8.parent());
  19.         var _d = _9._outerHeight() + _8._outerHeight() – _8.height();
  20.         _a._size(“minHeight”, _b ? (_b – _d) : “”);
  21.         _a._size(“maxHeight”, _c ? (_c – _d) : “”);
  22.     }
  23.     _8.css({height: “”, minHeight: “”, maxHeight: “”, left: _7.left, top: _7.top});
  24.     _7.onResize.apply(_4, [_7.width, _7.height]);
  25.     // 关键代码只有这一行,瞧,它又调用了panel组件的doLayout方法!
  26.     $(_4).panel(“doLayout”);
  27. };

结论

  • 步骤一:window的resize事件触发doLayout方法,当前容器(上下文)是body;
  • 步骤二:doLayout方法搜索“下一级响应式组件”,触发“下一级响应式组件”的”resize”方法;
  • 步骤三:“下一级响应式组件”的”resize”方法调用doLayout方法,也就是回到“步骤一”只不过当前容器(上下文)换成“下一级响应式组件”,它再次执行的时候,找的就是“下下级响应式组件”。

如此循环调用,一层一层顺序作响应式处理,直到找不到为止。值得注意的是,响应式处理不一定是从body开始,比如tabs组件在切换标签页的时候。

 性能问题

doLayout对“下一级响应式布局组件”的搜索代码是有很大优化空间的,当页面DOM结构很复杂的时候,特别是有大数据量表格的时候,在IE下的doLayout的搜索效率惨不忍睹,可以使用“children方法+递归调用+对普通表格不搜索”的方案优化。

Tagged: , ,

Comments are closed.