浅析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实在没什么关系,来看代码:
- $(window).resize(function () {
- $("body>div.window-mask").css({width: $(window)._outerWidth(), height: $(window)._outerHeight()});
- setTimeout(function () {
- $("body>div.window-mask").css({width: _26d().width, height: _26d().height});
- }, 50);
- });
这个事件处理程序,像个一丝不挂的普通姑娘,没啥内涵,目的单纯而直白——只是为了实时调整window组件的蒙版大小,他的功能相对于fluid来讲实在微不足道,所以我们一眼嫖过去,不做过多讨论。
panel组件绑定的resize
panel组件绑定的resize事件处理程序是相当有内涵的,她绝对不是一丝不挂,而且穿了好几层情趣内衣,我们需要一层一层的扒,需要耐心。先来看事件处理程序的定义:
- // 定义的了一个定时器的引用,老鸟们应该都知道他的目的有两个
- // 一是防止短时间内多次触发window的resize事件处理程序;
- // 二是解决某些浏览器调整一次窗口却多次触发resize事件的问题
- var _23f = null;
- $(window).unbind(".panel").bind("resize.panel", function () {
- // 100ms内触发多次的话,则终止对前一次的事件处理程序的调用
- if (_23f) {
- clearTimeout(_23f);
- }
- // 重置定时器
- _23f = setTimeout(function () {
- // 这里主要是看body是否是一个layout实例(是layout的话,body元素上会有layout样式)
- var _240 = $("body.layout");
- // 如果body是一个layout,则调用layout的resize方法,
- // layout的resize方法最终调用的也是panel的resize方法
- // 所以layout的resize显然是个多层情趣内裤,我们不急着扒。
- if (_240.length) {
- _240.layout("resize");
- } else {
- // 如果body不是一个layout组件,则调用panel组件的doLayout方法
- // 这个层数应该少点,而且地处核心位置,我们先扒这个doLayout
- $("body").panel("doLayout");
- }
- // 清空定时器
- _23f = null;
- }, 100);
- });
当我们的页面上引用了jquery.easyui.min.js这个伪开源的文件之后,这段代码会被执行一次。这段代码虽然不长,但是里面调用了resize和doLayout方法。
layout的resize方法,其底层调用的是panel的reszie方法(这层内衣我迅速的扒了,不信的同学可以自己看layout的代码)。所以,最后的焦点全部落到panel组件的两个方法上: doLayout和resize。
doLayout方法
找到panel组件的doLayout代码(搜"doLayout"关键字即可):
- function doLayout (jq, all) {
- return jq.each(function () {
- // 缓存this
- var _24a = this;
- // _24b变量判断当前容器是不是body
- var _24b = _24a == $("body")[0];
- // find函数的选择器真的很长,最后还用了filter方法来进一步过滤find出来结果
- // 这个写法看起来似乎很简洁明了,但是,他是否高效呢?这个问题先放一放
- // 我们先弄清楚这个变量s到底是什么?这个必须要拿例子来说明,注释里我试了很多方式去表达,都觉得表达不清楚。
- var s = $(this).find("div.panel:visible,div.accordion:visible,div.tabs-container:visible,div.layout:visible,.easyui-fluid:visible").filter(function (_24c, el) {
- var p = $(el).parents("div.panel-body:first");
- if (_24b) {
- return p.length == 0;
- } else {
- return p[0] == _24a;
- }
- });
- // 找到的需要做fluid布局后,触发绑定在他们DOM上自定义的的"_reszie"事件
- s.trigger("_resize", [all || false]);
- });
- }
对于doLayout函数中的s变量,分两种情况举例子。
如果当前容器是body:
- <!-- 当前容器 -->
- <body>
- <div id="a1">
- <div id="a21" class="accordion">
- <div id="a3">
- <div id="a4" class="accordion">...</div>
- </div>
- </div>
- <div id="a22" class="accordion">...</div>
- </div>
- </body>
s只包含"a21"和 "a22",其实也就是离当前容器最近的包含特征样式的子孙级元素。
如果当前元素是div.panel-body:
- <div class="panel-body">
- <div id="a1">
- <div id="a21" class="accordion">
- <div id="a3">
- <div id="a4" class="accordion">...</div>
- </div>
- </div>
- <div id="a22" class="accordion">...</div>
- </div>
- </div>
s也只包含"a21"和 "a22",跟body情况是一样的,只是寻找方式不一样。
这个doLayout函数的目的就比较清楚了: 它负责寻找当前容器的下一级(不一定是子级别,也可能是孙子,重孙子等)需要做响应式布局的组件,然后触发绑定在这些组件上自定义的"_reszie"事件。
由此,我们也可以推断: 每一个含有响应式特性的组件,其DOM结构里面肯定存在一个绑定了自定义的"_resize"事件的事件处理程序。
到这里, “下一级需要做响应式布局的组件”完成了自适应,那么 “下下级需要做响应式布局的组件”(响应式组件多层嵌套)是怎么完成自适应的呢?。
不急,我们还是拿panel组件自定义的"_resize"来看:
- function _13(_14) {
- $(_14).addClass("panel-body")._size("clear");
- var _15 = $("<div class=\"panel\"></div>").insertBefore(_14);
- _15[0].appendChild(_14);
- _15.bind("_resize", function (e, _16) {
- if ($(this).hasClass("easyui-fluid") || _16) {
- // _3函数就是panel组件resize方法的实现
- _3(_14);
- }
- return false;
- });
- return _15;
- };
也就是说自定义的"_resize"事件处理程序里,调用组件自身的resize方法,看来想知道“下下级”的响应式布局是如何完成的,必须还是去问resize方法。
resize方法
我们直接看代码:
- // panel组件resize方法的实现
- function _3(_4, _5) {
- var _6 = $.data(_4, "panel");
- var _7 = _6.options;
- var _8 = _6.panel;
- var _9 = _8.children("div.panel-header");
- var _a = _8.children("div.panel-body");
- if (_5) {
- $.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});
- }
- _8._size(_7);
- _9.add(_a)._outerWidth(_8.width());
- if (!isNaN(parseInt(_7.height))) {
- _a._outerHeight(_8.height() - _9._outerHeight());
- } else {
- _a.css("height", "");
- var _b = $.parser.parseValue("minHeight", _7.minHeight, _8.parent());
- var _c = $.parser.parseValue("maxHeight", _7.maxHeight, _8.parent());
- var _d = _9._outerHeight() + _8._outerHeight() - _8.height();
- _a._size("minHeight", _b ? (_b - _d) : "");
- _a._size("maxHeight", _c ? (_c - _d) : "");
- }
- _8.css({height: "", minHeight: "", maxHeight: "", left: _7.left, top: _7.top});
- _7.onResize.apply(_4, [_7.width, _7.height]);
- // 关键代码只有这一行,瞧,它又调用了panel组件的doLayout方法!
- $(_4).panel("doLayout");
- };
结论
- 步骤一:window的resize事件触发doLayout方法,当前容器(上下文)是body;
- 步骤二:doLayout方法搜索“下一级响应式组件”,触发“下一级响应式组件”的"resize"方法;
- 步骤三:“下一级响应式组件”的"resize"方法调用doLayout方法,也就是回到“步骤一”只不过当前容器(上下文)换成“下一级响应式组件”,它再次执行的时候,找的就是“下下级响应式组件”。
如此循环调用,一层一层顺序作响应式处理,直到找不到为止。值得注意的是,响应式处理不一定是从body开始,比如tabs组件在切换标签页的时候。
性能问题
doLayout对“下一级响应式布局组件”的搜索代码是有很大优化空间的,当页面DOM结构很复杂的时候,特别是有大数据量表格的时候,在IE下的doLayout的搜索效率惨不忍睹,可以使用“children方法+递归调用+对普通表格不搜索”的方案优化。