loading加载问题

现象描述

需要用到ajax请求获取数据,并且需要获取数据后在执行接下来的代码,因此使用了同步ajax请求,但是当ajax请求耗时过长时,需要添加一个loading遮罩层,但是实现后发现loading遮罩层出不来,就是同步ajax导致的假死现象

问题分析

首先需要了解一些相关知识。

浏览器的工作机制(浏览器的加载与渲染机制):

1.资源加载
资源文件的获取过程,不同浏览器,或者浏览器的不同版本,会有不同的实现效果
浏览器的五个常驻线程

  1. 浏览器GUI渲染线程
    GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了.
  2. javascript引擎线程
    Javascript引擎,也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。Javascript引擎线程理所当然是负责解析Javascript脚本,运行代码。
  3. 浏览器定时器触发线程(setTimeout)
    浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
  4. 浏览器事件触发线程
    当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
  5. 浏览器http请求线程(.jpg <link />这类请求)
    在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。

这里涉及到 阻塞 的现象,当js引擎线程进行时,会挂起其他一切线程,这个时候3、4、5这三类线程也会产生不同的异步事件,由于 javascript引擎线程为单线程,所以代码都是先压到队列,采用先进先出的方式运行,事件处理函数,timer函数也会压在队列中,不断的从队头取出事件,这就叫:javascript-event-loop。简单点说应该是当在进行第二线程的时候,1,3,4,5都会挂起,比如这时候触发click事件,即使先前JS已经加载完成,click事件会压在队列里,这里也要先完成第二线程才会执行click事件。

加载顺序

浏览器解析http response 下载html文件会”自上而下“加载,并在加载过程中进行解析渲染。“自上而下”加载时遇到图片、视频之类资源时便会进入第5个线程,这是异步请求,并不会影响html文档进行加载。

加载过程中遇到外部css文件,浏览器另外发出一个请求,来获取css文件。这里也是第5个线程,这里css解析会生成一个rule tree(规则树),这个以后会更新。

当文档加载过程中遇到js文件,html文档会挂起渲染(加载解析渲染同步)的线程,不仅要等待文档中js文件加载完毕,还要等待解析执行完毕,才可以恢复html文档的渲染线程。

  原因:JS有可能会修改DOM,最为经典的document.write,这意味着,在JS执行完成前,后续所有资源的下载可能是没有必要的,这是js阻塞后续资源下载的根本原因。
  办法:可以将外部引用的js文件放在</body>前。

css可能影响js的执行造成阻塞

原因:如js里面var width = $(‘#id’).width();这里js执行前,浏览器必须保证之前的css文件已下载和解析完成(后面的不会影响),这也是css阻塞后续js的根本原因。当js文件不需要依赖css文件时,可以将js文件放在头部css的前面。

预加载网页,利用空余时间来提前加载该网页的后续网页。

<link rel="prefetch" href="http://">

为js脚本添加defer属性,其不会阻塞后续DOM的的渲染。但是因为这个defer只是IE专用,所以一般用得比较少。而标准的的HTML5也加入了一个异步载入javascript的属性:async,无论你对它赋什么样的值,只要它出现,它就开始异步加载js文件。但是, async的异步加载会有一个比较严重的问题,那就是它忠实地践行着“载入后马上执行”这条军规,所以,虽然它并不阻塞页面的渲染,但是你也无法控制他执行的次序和时机。

<script defer="true" src="JavaScript.js" type="text/javascript"/>

加载、解析、渲染这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一边解析,一边渲染的工作现象。

JavaScript引擎

JavaScript引擎是单线程运行的,浏览器无论在什么时候都有且只有一个线程在运行JavaScript程序.


GUI渲染线程与JavaScript引擎线程是互斥的。

因为JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥的关系,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被”冻结”了.

所以,在脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.


因此,执行loading后,继续执行javascript脚本,导致loading的渲染“被冻结”,所以无法看到loading被渲染的画面

如果JavaScript引擎队列非空,引擎就从队列头取出一个任务,直到该任务处理完,即返回后引擎接着运行下一个任务,在任务没返回前队列中的其它任务是没法被执行的

解决方案

Deferred

开发网站的过程中,我们经常遇到某些耗时很长的javascript操作。其中,既有异步的操作(比如ajax读取服务器数据),也有同步的操作(比如遍历一个大型数组),它们都不是立即能得到结果的。

通常的做法是,为它们指定回调函数(callback)。即事先规定,一旦它们运行结束,应该调用哪些函数。

但是,在回调函数方面,jQuery的功能非常弱。为了改变这一点,jQuery开发团队就设计了deferred对象。

简单说,deferred对象就是jQuery的回调函数解决方案。在英语中,defer的意思是”延迟”,所以deferred对象的含义就是”延迟”到未来某个点再执行。

它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。

解决了因为ajax阻塞线程导致loading层出不来的问题

function getAjaxDeffer(atype, param, resUrl) {
    var defer = $.Deferred();// 新建一个deferred对象
    $.ajax({
        type: atype,
        url: resUrl,
        data: param,
        async: true,
        dataType: "json",
        success: function(msg) {
            defer.resolve(msg); // 改变deferred对象的执行状态
        },
        error: function(e) {
            closeLoading(); //关闭loading
        }
    });
    return defer
}

showLoading('正在保存,请稍后…')
$.when(getQuickAjaxDeffer("post", param, SETTING_UPDATE)).done(function(rest) {
        closeLoading(); //关闭loading
        if(rest.resultCode == 0) {
                console.log('保存成功');
                /*执行代码块*/
        }
})

总之,想让ajax走完再加载页面,就要使用同步。但是只要同步,ajax就会阻塞ui线程,使得loading显示不出来。

只有使用了deffer对象和$.when(),既可以ajax设为异步,保证了loading的正常显示,又可以保证在ajax走完再加载页面。因为$.when().done()会在deffer.resolve()之前的代码全部走完后才走done中的代码。

$.ajax()操作完成后,如果使用的是低于1.5.0版本的jQuery,返回的是XHR对象,你没法进行链式操作;如果高于1.5.0版本,返回的是deferred对象,可以进行链式操作。

$.when($.ajax({
        type: atype,
        url: resUrl,
        data: param,
        async: true,
        dataType: "json",
        success: function(msg) {
            defer.resolve(msg);
        },
        error: function(e) {
            closeLoading(); //关闭loading
        }
    })).done(function(rest) {
        closeLoading(); //关闭loading
        if(rest.resultCode == 0) {
                console.log('保存成功');
                /*执行代码块*/
        }
}).fail(function(){ alert("出错啦!"); });

jQuery规定,deferred对象有三种执行状态—-未完成,已完成和已失败。如果执行状态是”已完成”(resolved),deferred对象立刻调用done()方法指定的回调函数;如果执行状态是”已失败”,调用fail()方法指定的回调函数;如果执行状态是”未完成”,则继续等待

Deferred知识

  (1) $.Deferred() 生成一个deferred对象。

  (2) deferred.done() 指定操作成功时的回调函数

  (3) deferred.fail() 指定操作失败时的回调函数

  (4) deferred.promise() 没有参数时,返回一个新的deferred对象,该对象的运行状态无法被改变;接受参数时,作用为在参数对象上部署deferred接口。

  (5) deferred.resolve() 手动改变deferred对象的运行状态为”已完成”,从而立即触发done()方法。

  (6)deferred.reject() 这个方法与deferred.resolve()正好相反,调用后将deferred对象的运行状态变为”已失败”,从而立即触发fail()方法。

  (7) $.when() 为多个操作指定回调函数。

除了这些方法以外,deferred对象还有二个重要方法,上面的教程中没有涉及到。

  (8)deferred.then()

有时为了省事,可以把done()和fail()合在一起写,这就是then()方法。

   . w h e n ( .ajax( “/main.php” ))

  .then(successFunc, failureFunc );

如果then()有两个参数,那么第一个参数是done()方法的回调函数,第二个参数是fail()方法的回调方法。如果then()只有一个参数,那么等同于done()。

  (9)deferred.always()

这个方法也是用来指定回调函数的,它的作用是,不管调用的是deferred.resolve()还是deferred.reject(),最后总是执行。

猜你喜欢

转载自blog.csdn.net/tjj3027/article/details/81704702