使用 jQuery Mobile 与 HTML5 开发 Web App —— jQuery Mobile 页面事件与 deferred
在系列的上一篇文章《使用 jQuery Mobile 与 HTML5 开发 Web App —— jQuery Mobile 事件详解》中,Kayo 介绍了除页面事件外的其他 jQuery Mobile 事件,而页面事件由于事件数较多,并且涉及 jQuery 中一个比较复杂的对象 deferred ,因此在本文中单独说明。jQuery Mobile 页面事件使用分为页面加载事件 (Page load events),页面跳转事件 (Page change events),页面显示/隐藏事件 (Page show/hide events),页面初始化事件 (Page initialization events),页面移除事件 (Page remove events) 五种。本文除了会对以上五种事件作出详细说明外,还会对 jQuery Mobile 的页面加载流程作出详细讲解。
一.页面加载事件 (Page load events)
当一个外部页面加载到 DOM 时,会触发两个事件 —— 第一个事件是 pagebeforeload,第二个是 pageload 或 pageloadfailed。jQuery Mobile 提供了这些 API ,可以使开发者可以方便地在页面加载前后对页面数据进行处理。
注意这里是外部页面加载到 DOM 的过程 ,即加载的页面不在当前页面的文档中,而一个文档中的多个 "page" 是本来就存在于 DOM 中,因此在同一文档中的不同 "page" 的跳转不会触发 pagebeforeload 事件。关于 jQuery Mobile 中“page”的理解,可以阅读《使用 jQuery Mobile 与 HTML5 开发 Web App —— jQuery Mobile 页面与对话框》
pagebeforeload
pagebeforeload 事件会在页面加载前被触发,这个事件的最常用实例是 —— 为绑定该事件的回调函数调用 preventDefault() ,阻止这个事件的默认行为,这样表明由事件的回调函数进行自定义处理页面,这时开发者必须使用 deferred 对象调用 resolve() 或者 reject() 在自定义处理结束后恢复页面请求。
在说明为什么需要用 resove() 或 reject() 恢复页面请求前,首先要详细介绍 deferred 对象。
deferred 对象从 jQuery 1.5.0 引入,jQuery Mobile 是基于 jQuery 库的,当然也可以使用 deferred 对象。在旧版本的 jQuery 中,回调函数功能很单一,就事件机制为例,通常只能是事件触发后指定调用一个函数(即为事件绑定的回调函数),然后执行该函数。而引入 deferred 后,jQuery 的回调函数功能则强大很多了,deferred 从字面上来说是“延时”的意思,但 deferred 对象的功能不止如此,它除了延时操作外(解决耗时操作问题),还进行了统一封装,为回调函数的相关处理提供统一编程接口。
deferred 对象是在 $.ajax() 内部实现的,所以可以在调用 ajax 时创建 deferred 对象并调用。jQuery Mobile 是基于 ajax 的,包括它的页面加载,并且页面加载的过程在内部实现时已经创建了相应的 deferred 对象,所以在调用页面事件的回调函数时可以调用 deferred 对象。
因此,我们只需指定 ajax 请求成功和失败时的回调函数列表即可方便的进行回调,这两种回调可以分别写在 done() 和 fail() 方法中,而上面介绍的 resolve() 或 rejected() 方法,则分别可以手动执行 done() 和 fail() 方法。这时候 deferred 的作用大致已经说明了。
这里再引入一个概念,deferred 状态。deferred 有三种状态:初始化(unresolved),成功(resolved),失败(rejected)。正如上面所说, deferred 执行哪些回调函数是依赖于状态。
简而言之,deferred 的用途是根据不同的请求状态,调用相应的回调队列,使到开发者可以方便地创建一系列的回调,甚至是链式调用,而不需要像传统方法那样只能为一个动作绑定一个回调函数,另外它还有 resolve() 和 rejected() 等方法,使到这个回调处理可以更加灵活。
接下来再回到 pagebeforeload 事件上,上面只是说明了 deferred 的主要作用,但并没有说明 deferred 在页面加载流程中的具体功能。在这之前,我们应该了解新页面载入的流程是怎样的?
当我们触发了一个跳转到新页面的链接后,jQuery Mobile 会调用 $.mobile.changePage() 方法,这是 jQuery Mobile 用于加载新页面的方法,日后会另作介绍,调用这个方法后,加载页面的流程正式开始,为了进一步说明这个流程,下面 Kayo 节选 $.mobile.changePage() 的源码并注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 若 toPage 参数为字符串,即第一个参数格式正确,则在 $.mobile.changePage() 内调用 $.mobile.loadPage 方法 if ( typeof toPage === "string" ) { $.mobile.loadPage( toPage, settings ) // 指定 done() 回调操作队列 .done( function ( url, options, newPage, dupCachedPage ) { isPageTransitioning = false ; options.duplicateCachedPage = dupCachedPage; $.mobile.changePage( newPage, options ); }) // 指定 fail() 回调操作队列,并链式调用 .fail( function ( url, options ) { isPageTransitioning = false ; // 清除用户点击的按钮的激活状态 removeActiveLinkClass( true ); // 释放 transition releasePageTransitionLock(); settings.pageContainer.trigger( "pagechangefailed" , triggerData ); }); return ; } |
从上面的源码中可以看出,实际上加载页面内容是由另一个方法 $.mobile.loadPage() 负责,$.mobile.changePage() 的责任是指定 $.mobile.loadPage() 请求成功和请求失败时的回调队列,即请求成功或失败后分别需要做些什么。因此不难想象,实际利用 deferred 对象的也是 $.mobile.loadPage() 。
这里 Kayo 需要指出两点:一是这里 done() 和 fail() 是链式写法,即调用 done() 后会继续调用 fail() ,这是因为无论页面请求成功与否,有一些操作(像清除用户点击的按钮的激活状态)都是必须的,因此 jQuery Mobile 采用链式写法,把这部分操作写在 fail() 中,若页面请求失败,则直接调用 fail() ,若请求成功则调用 done() 后链式调用 fail() 。二是以上两个方法的实际作用,由于篇幅有限,这里没有列出完整源码,无法看出以上两个方法更深入的作用,实际上 $.mobile.loadPage() 的作用是把外部页面的元素插入到当前 DOM 中,而 $.mobile.changePage() 只是把新页面显示(激活一个页面),因此外部页面才需要首先调用 $.mobile.loadPage() 把元素插入当前 DOM 中。
下面再对 $.mobile.loadPage() 的源码进行分析,这里会为整个 $.mobile.loadPage() 方法的源码进行注释,但为了方便阅读,不会列出全部的源码实体,以源码注释代替完整源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | $.mobile.loadPage = function ( url, options ) { // 创建一个 deferred 对象,用于告知调用者 $.mobile.changePage() 页面请求成功或是出现错误 // 也就是让 $.mobile.changePage() 知道需要调用 done() 或是 fail() 回调队列 var deferred = $.Deferred(), // … 根据参数处理页面数据 … // 触发一个 pagebeforeload 事件 var mpc = settings.pageContainer, pblEvent = new $.Event( "pagebeforeload" ), // 保存页面选项在 data 参数中 triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings }; // 让监听器知道正准备加载一个新页面 mpc.trigger( pblEvent, triggerData ); // 如果开发者阻止了默认行为,本函数马上结束,并返回 deferred 对象的 promise() 方法 if ( pblEvent.isDefaultPrevented() ) { return deferred.promise(); } // 提示正在加载页面 // 使用 ajax 把页面插入 DOM ,然后根据 ajax 请求成功还是失败作出相应的处理 // 请求成功 /* 根据实际情况和传递的参数调整页面内容(调整 jqm header, jqm title, 移除 loading 提示等) 触发 pageload 事件告知监听者页面请求成功 */ // 调用 resolve() 方法使 deferred 状态为成功 deferred.resolve( absUrl, options, page, dupCachedPage ); // 请求失败 /* 报错并对页面作出一个调整(移除 loading 提示等) 触发 pageloadfail 事件告知监听者页面请求失败 */ // 调用 rejected() 方法使 deferred 状态为失败 deferred.reject( absUrl, options ); // 返回一个 deferred 对象的 promise() 方法 return deferred.promise(); }; |
看了上面的代码和注释后,相信大家对页面加载的流程以及页面事件的触发时间已经有了完整的理解,deferred 对象起到了控制使用哪种回调的作用,这些回调的具体内容是在 $.mobile.changePage() 中分别以 done() 和 fail() 指定的。
若用户没有阻止事件的默认行为,则根据请求成功或失败,会调用 resolve() 或 rejected() 方法改变 deferred 的状态,然后根据状态判断是调用 done() ,或是直接调用 fail() 。
若用户阻止事件的默认行为,jQuery Mobile 会马上终止加载页面,并返回一个 deferred 的 promise() 方法,promise() 方法返回的是 deferred 的只读版本,即返回一个可读不可写的 deferred 对象,除此之外没有任何处理了。但是上面的源码注释中已经说明,$.mobile.loadPage 会把一系列的属性保存在 data 参数中,其中包括了 deferred 对象,我们知道,开发者可以为事件绑定一个回调函数,这个 data 对象会被分配到 pagebeforepage 事件的回调函数的第二个参数中(第一个参数为事件本身),因此我们可以调用这个 data.deferred 对象的 resolve() 或 rejected() 方法恢复请求,实际上这里的恢复请求是告知调用者 $.mobile.changePage() 这次 ajax 请求是成功还是失败,使到 $.mobile.changePage() 方法知道需要调用 done() 回调队列还是直接调用 fail() 回调队列。
resolve() 或 rejected() 中填写的参数实际上也就是 done() 或 fail() 中相应的参数。
当然,若开发者没有阻止默认行为,这个 $.mobile.loadPage() 在最后也会触发 resolve() 或 rejected() 方法,因此实际上有没有利用阻止默认行为进行自定义处理的区别是 —— 是否进行页面处理和触发 loadpage 或 loadpagefailed 事件,即自定义处理不会有上面的源码注释中使用 /* */ 注释的部分,或者说,自定义处理需要做的,就是自行设计一些处理,来代替 /* */ 注释中的默认处理。
这就意味着,若有需要的话,阻止默认行为并进行自定义处理页面(调整 jqm header, jqm title 移除 loading)后还必须手动触发 loadpage 事件或 loadpagefailed 事件。
在说明整个加载页面流程后,这里再介绍一下上面所说的传递给 pagebeforeload 事件的回调函数的第二个参数 —— data 的各项属性。
最后引用 jQuery Mobile 官方的例子来说明如何阻止默认行为并进行自定义处理,为了进一步说明,Kayo 会修改一下这些例子。
调用 resolve()
1 2 3 4 5 6 7 8 9 10 11 12 | $( document ).bind( "pagebeforeload" , function ( event, data ){ // 阻止默认行为,告知浏览器本次事件由事件的回调函数(即本函数)处理 event.preventDefault(); // 自定义处理,如简单的弹出一个提示 alert( '本次事件由开发者作出一些处理' ); // 在本函数或者其它异步方法中调用 resolve() // 告知 $.mobile.changePage() 方法继续页面请求并执行 done() 回调函数队列 data.deferred.resolve( data.absUrl, data.options, page ); }); |
调用 reject()
1 2 3 4 5 6 7 8 9 10 11 12 | $( document ).bind( "pagebeforeload" , function ( event, data ){ // 阻止默认行为,告知浏览器本次事件由事件的回调函数(即本函数)处理 event.preventDefault(); // 自定义处理,如简单的弹出一个提示 alert( '本次事件由开发者作出一些处理' ); // 在本函数或者其它异步方法中调用 rejected() // 告知 $.mobile.changePage() 方法继续页面请求并直接执行 fail() 回调函数队列 data.deferred.rejected( data.absUrl, data.options ); }); |
pageload
pageload 事件的触发流程相对 pagebeforeload 来说则较为简单,当页面成功加载并插入到 DOM 后会触发 pageload 事件,这个事件也会传递一个 data 参数作为事件回调函数的第二个参数,但这个 data 参数与 pagebeforeload 的属性有些不同,下面列出完整的属性。
可以看出,jQuery Mobile 没有为 pageload 提供 deferred 对象属性,这说明页面请求成功后,只能按默认情况执行 done() 队列,不能利用 deferred 对象直接执行 fail() 队列。
pageloadfailed
pageloadfailed 事件是页面请求失败时触发的,参数与 pageload 相似,但这里再次引入 deferred 对象,这表明 jQuery Mobile 允许开发者即使页面请求失败,仍可利用 deferred 对象选择执行 done() 队列或 fail() 队列 。具体的 data 属性如下。
上面介绍 pagebeforeload 事件时,已经介绍了使用 preventDefault() 阻止事件默认行为,然后进行自定义的处理,并且重新使用 deferred 对象的 resolve() 或 rejected() 方法恢复页面请求。而在 pageloadfailed 事件中也有类似的做法,这里的自定义处理通常是在页面请求失败后加载另一个页面,下面举例说明。
调用 resolve()
1 2 3 4 5 6 7 8 9 10 11 12 13 | $( document ).bind( "pageloadfailed" , function ( event, data ){ // 阻止默认行为,告知浏览器本次事件由事件的回调函数(即本函数)处理 event.preventDefault(); // 自定义处理,尝试加载另一个页面 // 在本函数或者其它异步方法中调用 resolve() // 告知 $.mobile.changePage() 方法继续页面请求并执行 done() 回调函数队列 data.deferred.resolve( data.absUrl, data.options, page ); }); |
调用 rejected()
1 2 3 4 5 6 7 8 9 10 11 12 13 | $( document ).bind( "pageloadfailed" , function ( event, data ){ // 阻止默认行为,告知浏览器本次事件由事件的回调函数(即本函数)处理 event.preventDefault(); // 自定义处理,尝试加载另一个页面 // 在即本函数或者其它异步方法中调用 rejected() // 告知 $.mobile.changePage() 方法继续页面请求并直接执行 fail() 回调函数队列 data.deferred.reject( data.absUrl, data.options ); }); |
下面给出一个完整例子来验证自定义处理的方法中需要代替的是页面处理和触发 pageload 或 pageloadfailed 事件,本例子中,Kayo 阻止了 pagebeforeload 的默认行为,注意,Kayo 在例子中绑定了 pageload 事件,并在其回调函数中添加弹出提示,但因为进行自定义处理,不会产生 pageload 事件,因此不会弹出加载 pageload 事件的提示,只有“自定义处理”的弹出提示。另外,读者可以尝试在 Demo 中的两个页面中来回点击几次,会发现只有第一次点击会弹出“自定义处理”提示,这是因为第一次加载另一个页面后,该页面已经存在于 DOM 中,再次加载不会触发 pagebeforeload 事件。读者可以在 Demo 中验证以上两点。
页面加载事件 Demo(建议使用 PC 上的 Firefox、Chrome 等现代浏览器和 IE9+ 或 Android , iPhone/iPad 的系统浏览器浏览,下同)
二.页面跳转事件 (Page change events)
页面跳转事件与页面加载事件类似,但因为没有直接使用 deferred 对象,因此这系列的事件会较为简单。
页面跳转事件由 $.mobile.changePage() 产生,第一个为 pagebeforechange ,第二个是 pagechange 或 pagechangefailed 。
$.mobile.changePage() 方法调用后会对页面参数进行一些处理,然后触发 pagebeforechange 事件,若页面请求成功,即上面的 $.mobile.loadPage() 调用了 done() 队列,并且页面外部处理(history 等)完成后,会触发 pagechange ,若请求失败,即上面的 $.mobile.loadPage() 直接调用了 fail() 队列,则触发 pagechangefailed 事件。
在上面 Kayo 已经说明过,实质处理页面加载的是 $.mobile.loadPage() 事件,因此需要进行页面加载前后的自定义处理还是用阻止 pagebeforeload , pageload, pageloadfail 等事件并进行自定义处理比较合适,尽管这样,阻止 pagebeforechange 仍具有意义 —— 它可以直接阻止一个页面加载,在某些情况下,可能你会需要这样做,就像有些情况下需要阻止 click 的默认跳转行为一样。
这三个事件的回调函数的第二个参数 data 有如下两个属性:
利用上面两个属性,可以在事件回调函数中作出一些处理。
值得注意的是,通过了解加载页面的流程,可以知道,一次正确的页面跳转中,事件产生的顺序应该是,pagebeforechange , pagebeforeload , pageload , pagechange ,在开发时需要注意这些事件的触发顺序。
三.页面显示/隐藏事件 (Page show/hide events)
在页面转场前后,会触发一些事件,举一个例子:
B 页面中有一个 id 为 page-B 的 page 组件 ,然后在 A 页面的 head 中作出如下的事件绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $( '#page-B' ).live( 'pagebeforeshow' , function (event, ui){ alert( 'pagebeforeshow' ); }); $( '#page-B' ).live( 'pagebeforehide' , function (event, ui){ alert( 'pagebeforehide' ); }); $( '#page-B' ).live( 'pageshow' , function (event, ui){ alert( 'pageshow' ); }); $( '#page-B' ).live( 'pagehide' , function (event, ui){ alert( 'pagehide' ); }); |
从 A 页面点击链接跳转到 B 页面,然后从 B 页面跳转回 A 页面,则触发事件的情况如下,
因此,触发哪种页面显示/隐藏事件,要视乎页面是被显示还是被隐藏。
这些事件会由转场实现函数控制触发,它们的回调函数的第二个参数 data 只有一个属性,分别为:
下面给出一个 Demo ,演示上面 A 与 B 页面的例子
页面显示/隐藏事件 Demo
四.页面初始化事件 (Page initialization events)
页面初始化中的初始化指的是页面增强,即 jQuery Mobile 对页面组件的样式、功能和交互进行丰富并增强的过程,在这个过程中也会触发一系列事件。
pagebeforecreate
在页面开始初始化之前触发,这时 DOM 已被 jQuery Mobile 获得,但仍未进行 jQuery Mobile 增强,因此绑定这个事件并在回调函数中作出处理可以在页面被增强前进行自定义处理。
另外若阻止事件的默认行为,会禁止页面组件初始化。
是否觉得这个流程与 pagebeforeload 的流程相似,的确,在 jQuery Mobile 中,很多事件的产生函数(如 pagebeforeload 的产生函数 pagebeforeload)内部都有阻止默认行为的处理,即用判断语句判断触发阻止默认行为后自动 return ,阻止后面的处理发生,了解这个流程对于了解 jQuery Mobile 的工作原理很有帮助。
pagecreate
在页面初始化时,即 DOM 刚开始被创建,并且需要进行 jQuery Mobile 的元素已经准备好(即加入待增强列表),但仍未实际开始进行 jQuery Mobile 增强前触发。
pageinit
当页面初始化后触发,jQuery Mobile 建议使用这个事件的绑定代替 jQuery 中常用的 DOM ready() ,因为触发这个事件意味着页面已经直接或者通过 Ajax 加载好并增强,更适合用在 jQuery Mobile 开发中。
五.页面移除事件 (Page remove events)
页面移除事件只有一个 —— pageremove ,当 jQuery Mobile 准备从 DOM 中移除一个外部页面时会触发这个事件,阻止这个事件的默认行为可以阻止一个页面被移除。
六.页面事件触发顺序
上面的说明中,基本说明了页面加载的完整流程,同时对不同事件的触发顺序作出了间接的说明,下面再给出一个例子,来直接说明在一次页面跳转中各个页面事件的触发顺序。
页面事件触发顺序 Demo
本文由 Kayo Lee 发表,本文链接:http://kayosite.com/web-app-by-jquery-mobile-and-html5-page-events-and-deferred.html
评论列表
不错,支持~~
我的第二个页面有用Ajax 生成的标签。当我返回第一个页面,再回到第二个页面,标签还是生成了,但jqm的Ajax并没有起作用,导致标签没有moblie-css样式。是原始的html样式。
我的理解是jpm只在第一次加载页内第二个Page时,使用Ajax修改样式。 等再次回到页内第二个Page页面时节省了这一步骤,才导致我的Ajax自动生成的标签没有moblie样式。
怎么才能使它第二次进入第二个Page页面时,也调用jpm的Ajax呢???
或者欺骗它,没有加载过第二个页面,始终保持是第一次加载。 但有什么方便设置这个状态的地方吗?
求大神解答
@bocai 你的说明有点乱了,我整理一下你的意思:是不是第二个页面中有 Ajax 生成的标签,但是从第一个页面再回到第二个页面时 jqm 没有起作用?
如果是这样的话,你可以在 pagebeforecreate 事件的回调函数中插入那些新标签,pagebeforecreate 事件触发时 DOM 仍未增强,插入的新标签会自动进行 jqm 增强。具体可以这样写:
$( document ).bind( “pagebeforecreate”, function( ){
// 插入新标签
});
@Kayo 多谢啊!解决了我想了一天的问题。 第二个页面是页内page, js是文件调用,建议使用live形式,如下:
$(‘#pageid’).live(‘pagebeforecreate’, function( ){
// 插入新标签
});
@bocai 不客气了!
是的,页内操作最好用 live()
看了半天,关于JQM的pagebeforeload()事件,在应用的时候还是出错了,data.deferred.resolve( data.absUrl, data.options, page );这句代码中的page 是什么,或该怎么写,我用的时候报 page is noe defined,错误,以下是我的代码!求解,谢谢
function AClick() { $.mobile.changePage(“../Pages/DataAbstact.aspx”, { type: “post”, transition: “slide” });
}
$(document).on(“pagebeforeload”, function (event, data) {
alert(“pagebeforeload”);
debugger
event.preventDefault();
data.deferred.resolve(data.absUrl, data.options, page);
data.deferred.rejected(data.absUrl, data.options);
});