最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • setTimeout 小尴尬的启示

    正文概述 掘金(为之漫笔)   2020-12-10   590

    本文通过分析一个小案例帮大家从一个侧面理解 JavaScript 的异步执行机制,从而可以在实践中避免类似的尴尬。

    小背景

    我们都知道,alert这种内置弹框会阻塞后续代码执行:

    setTimeout 小尴尬的启示

    之所以如此,就是因为 JavaScript 代码在浏览器中是单线程执行的。换句话说,浏览器中只有一个主线程负责运行所有 JavaScript 代码(不考虑 Web Worker)。

    提到浏览器中的 JavaScript,基本上只有三个来源:

    • BOM API 的代码,让我们可以操作并利用浏览器提供的能力
    • DOM API 的代码,让我们可以操作网页内容
    • 我们自己写的 ECMAScript 代码

    这没什么。我们也知道,setTimeout用于 “定时” 执行代码,比如这样可以定时在 3 秒钟之后执行一段代码(函数):

    setTimeout(delayCode, 3000)
    

    当然,我们也都知道,setTimeout的 “定时” 并不精确,它只能保证delayCode函数在 3 秒以后执行,至于在 3 秒以后还要等多长时间才能执行,就跟它没关系了。

    那跟什么有关系?我们知道,任务队列 / 事件循环是 JavaScript 执行异步任务的机制。setTimeout作为 BOM API,它只负责设定一个计时器,到点把要执行的函数添加到任务队列。这样它就完成任务了。而把函数添加到任务队列并不能保证立即执行。什么时候能执行取决于事件循环什么时候把这个函数调度到主线程。事件循环调度异步函数的前提是主线程空闲。如果主线程被阻塞了,即使把函数添加到事件对列,事件循环也不会立即调度它到主线程。这就是setTimeout不能精确定时执行某个函数的原因。

    显然,如果你的代码中存在依赖setTimeout精确定时的逻辑,就有可能遭遇尴尬。为此我们自己写代码时,除非绝对有把握,一定尽量不要依赖setTimout的精确定时。可是,问题在于我们能保证自己写的代码不依赖它,却很难保证我们代码依赖的第三方代码不依赖它。

    小案例

    下面我们就来介绍一个遭遇这种尴尬的真实案例。这个案例涉及的功能很简单,就是 jQuery 的$.ajax()函数在加载数据失败时重发请求。由于其超时逻辑依赖setTimeout的精确定时,结果导致超时设置失效。

    相关代码也很简单,主要涉及 3 个函数:

    function asyncRequest() {
      $.ajax({
        url: 'https://api.example.rs',
        timeout: 15
      }).then(success, fail)
    }
    
    function success(data) {
      // 正常处理数据
    }
    
    function fail(xhr, errtext, errthrown) {
      // 重发请求
      asyncRequest()
      // 弹框提示;阻塞主进程
      alert('请求超时')
    }
    // 首次调用
    asyncRequest()
    
    • asyncRequest:包含 Ajax 请求的函数,会在fail中再次调用
    • success:Ajax 请求成功的回调
    • fail:Ajax 请求失败的回调

    正常逻辑是这样的:调用asyncRequest发送请求,成功则浏览器将success添加到任务队列,失败则浏览器将fail添加到任务队列。之后由事件循环将它们调度到主线程执行。success就是正常处理数据,而fail会先调用asyncRequest重发请求,再调用alert弹框提示。

    测试环境下 Ajax 请求 100 毫秒左右可以返回。而为了测试超时失败后的逻辑,我们故意将超时时间设置为 15 毫秒,确保一定会超时。实际测试时,首次请求超时,走fail分支,重发请求、弹框,都没问题。但是,在鼠标点击关闭弹框后,却发现重发的请求正常返回了,并没有因超时被取消掉。反复测试都是如此。

    这就尴尬了,到底为什么呢?研究发现,jQuery 干掉超时请求的代码是这样的(j11y.io/jquery/#v=g…):

    // Timeout
    if (s.async && s.timeout > 0) {
      timeoutTimer = window.setTimeout(function () {
          jqXHR.abort("timeout");
      },
      s.timeout);
    }
    

    也就是说,在我们设置了timeout选项的情况下,jQuery 会通过setTimeout设置一个 15 毫秒后定时执行的函数,用来中断(abort)请求,我们称其为中断函数

    正常情况下,执行完上面的代码,浏览器会在 15 毫秒后把中断函数添加到任务队列上。此时如果主线程是空闲的,则事件循环会立即把这个函数调度到主线程去执行,请求被取消,浏览器把fail添加到任务队列,事件循环把它调度到主线程执行。这正是首次调用asyncRequet的情况。

    第二次调用asyncRequest时有什么不同呢?不同之处在于这次调用完asyncRequest之后,还弹框阻塞了主线程。调用asyncRequest的结果跟之前一样,浏览器仍然会在 15 毫秒后把中断函数添加到任务队列。但是,这里要注意,由于此时主线程因弹框阻塞一直处于被占用状态,事件循环只能等待。直到我们手拿鼠标花一两秒时间把弹框关闭,主线程空闲出来,中断函数才会被调度到主线程上执行。而在此之前,Ajax 请求早已成功返回,同时浏览器把success添加到任务队列。

    理论上,Ajax 请求返回后jqXHRXMLHttpRequest)对象的状态不应再有任何改变(改变也没意义)。因此,中断函数的执行并不会改变 “请求已经成功返回” 这个事实。更为尴尬的是——中断函数执行后,紧接着,事件循环又把success函数调度到主线程。而fail函数根本就没有进入任务队列,更谈不上执行了。

    小收获

    通过上面的案例分析,我们看到本该 “超时” 失败的请求,因为中断函数被耽误在任务队列上迟迟得不到执行,最终反而成功返回了数据。当然,问题的根源在于alert弹框阻塞了主线程,以及 JavaScript 的异步机制(事件循环)。

    至于 jQuery 依赖setTimeout取消超时请求的逻辑,只要不是遇到像本文案例这样长时间阻塞主进程的情况就不会有问题。在本案例中,如果不是为了测试而把超时时间设置得那么短,而是设置为比如 5000 毫秒,这个尴尬的局面也不会出现。假如实际的服务器响应时间真超过了 5 秒,只要我们在 Ajax 请求返回前关掉弹框,中断函数还是会先一步执行,从而取消未完成的请求。当然,实践中使用系统弹框阻塞主进程本来也不是推荐的做法。

    不管怎么样,机缘巧合,我们还是借这个小尴尬(重温或者)深入理解了setTimeout乃至 JavaScript(应该说浏览器提供的 JavaScript 运行时)的异步代码执行机制。那么在今后的编程实践中,我们就可以有意识地在逻辑中避免依赖setTimeout精确定时,因为它的定时真的不可靠啊!

    感谢 hax 帮忙审校本文


    起源地下载网 » setTimeout 小尴尬的启示

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元