03期:好用的setTimeout(func, 0)

一、引子


iScroll组件的刷新问题


使用过iScroll插件的同学都应该知道,iScroll在初始化时,需要知道Wrapper元素和Scroller元素的准确尺寸。在组件初始化成功后,假如我们后来又修改了其尺寸,那么会影响iScroll的正常使用,所以需要重新refresh iScroll组件。


但是另外一个问题又来了:页面UI更新不会即时完成,如果我们在页面UI还没有更新成功前,就对iScroll组件重新refresh,那么iScroll获取到的Wrapper元素和Scroller元素的尺寸,也不是最终稳定的尺寸,这将导致组件的刷新失败。


解决方案


我们不难想到的一个解决方案就是:延迟Iscroll组件的刷新,腾出足够的时间,以供页面UI更新完成。这个方案确实能够解决问题,但是新的问题又来了,要延迟出多少时间?1000ms? 500ms? 还是300ms?

iScroll官方示例


其实0ms就已足够。或许你会感到诧异:延迟0ms,那不就是没有延迟吗? 今天的分享就是为了解答这个诧异。


但在正式解答这个问题之前,我想先简单介绍一下所需要的前置知识:JS的出身及语言特性、浏览器的内部线程及线程间关系。


二、前置知识点:JS的出身及语言特性


每次讲讨论技术问题都文赳赳的,今天我们就来八卦一下javascript的前世今生。


爸爸和妈妈的故事


先介绍一下JS它妈

JS它妈:Brendan Eich


1995年四月,网景公司聘请了Brendan Eich。为了配合Navigator 2.0 Beta 版的发布计划,Brendan Eich临危受命,在10天内完成了第一版JS的设计和开发。没错,就是10天,听到这个,你会不会突然感觉到自己好(hen)惭(la)愧(ji)?



继续往下看。JS宝宝之所以能够诞生,除了有妈是不够的,还得有爸和爸爸的“冲动”:

web技术,在发展之初,主要用于显示一些静态文本,以方便科学家们进行学术交流。随着web技术的应用愈加广泛和深入,使用者也希望web技术能够提供更强的交互性和动态性。网景通信公司后来也意识到这一点,于是乎,网景通信公司的成立者Marc Andreessen,即JS它爸,计划开发一门易用的、能够内嵌于Netscape浏览器的、且能够直接在web页面编写和执行的脚本语言,以帮助web设计师和业余编程人员将图片和插件等组件整合到web页面中。


JS它爸:Marc Andreessen


于是在1995年,该公司雇佣了Brendan Eich,也就是JS它妈,来担此重任。Marc Andreessen的远见就像一只“蝌蚪”一样,游到了才华横溢的Brendan Eich那里,也就有了后来JS它妈的“十日怀胎”。而在JS语言开发阶段,爸爸Marc Andreessen为它取了个乳名,谓之Mocha;又在1995年9月,Netscape浏览器2.0版本发布时,为它取了个官方学名Livescript。


除了有爸,还有给力的干爹


为了和微软的桌面系统抗衡,以让更多的用户接受web浏览器系统,在JS脚本语言的编写计划开始之前,网景公司就已经和Sun公司(js它干爹)合作,将Sun公司的静态语言JAVA应用Netscape浏览器。也是为了和Sun公司形成共同的技术生态,网景公司对计划设计的浏览器脚本语言,希望其能够和Java形成一定的互补,并且语法和Java相近。


1995年,Sun公司的儿子Java已经声名远扬。作为合作的回应,Sun公司决定让干儿子抱一下大腿(以帮助其推广),同意了网景公司为该脚本语言申请的“Javascript”商标认证。所以Java和Javascript并不是简单的雷锋和雷峰塔的区别,而是有干兄弟关系。而在1995年12月,Netscape浏览器2.0 beta 3版本发布时,Livescript也正式改名为Javascript。


以上简单介绍了JS的出身。从JS语言的设计初衷,我们可以知道,它仅仅是作为浏览器的脚本语言。而使用者对交互性和动态特效的需求,也决定了这门语言的职责和特性。


JS解析引擎的单线程机制


众所周知,JS被设计为单线程机制。JS之所以设计为单线程,而不是多线程,与JS在浏览器的职责有关。


在浏览器端,脚本语言的主要使命是通过监听和响应用户行为、操纵DOM等来提高web页面的可交互性和动态性。这一类操作有如下两个特性:


1. 这一类操作在执行上是线性的、同步的,其操作结果的变化也应该是线性的、有承继性的。而要想保证操作结果的线性变化,对于这一类操作的脚本解析,也应该是线性的,同步的。学过操作系统的同学应该知道,在多线程机制下,会出现竞争条件(多个线程如果并发修改同一数据,数据的最终结果取决于线程间修改数据的精确顺序),简单来说就是:如果线程修改数据的顺序不一样,数据的最终结果也不一样。正因为有并发和竞争条件的存在,也就难免会发生后面操作被前面操作覆盖、后面操作依赖于前面操作但是后面操作先执行等情况。虽然多线程机制也有同步方法,但是同步代码会增加代码的复杂度,而浏览器脚本语言大部分处理的都是同步任务,所以使用多线程则显得有点画蛇添足,徒增复杂;


2. 这一类操作在触发时(主要由用户输入引起),往往类型单一,即任务类型单一,少有需要并发处理的情况。众所周知,多线程的强项在于可以在“同一时间”内处理不同类型的任务,而在这种大多时候只有单一类型任务需要执行的场景下,多线程机制少有用武之地。特别地,如果将脚本语言设计为多线程,那么还需要考虑多线程管理的实现,这种复杂实现无疑会增加开发成本,延长开发周期,但是实际上又很少会被用到,所以不如直接设计为单线程来的简单。

在少数情况下,确实需要在同一时间并发处理不同类型的任务(如除了解析脚本代码外,还需要额外执行setTimeout的时间计算、监听ajax异步请求的响应等)。并发也的确是单线程机制的短板,而且让单线程的JS解析线程按顺序一个不落地来执行这些任务,还会引发阻塞,但这两个问题并不是不能解决:在浏览器内核,其实还实现了额外的特定功能类线程如时间线程和异步请求线程等并发处理特定类型的任务,它既保证了JS解析线程职责的单一,又很好地避免了JS解析线程被阻塞再加上天才设计的回调处理机制,使得从JS解析线程分离出去的任务,其最终结果在某个时机点又会被JS解析线程重新处理,这两者可谓是单线程机制的强助攻。

所以,在这种大多时候只有单一类型任务需执行的场景,脚本语言采用单线程机制会更加简答、高效。在少数需要并发处理任务的场景,有浏览器内部实现的功能性线程和和回调处理机制,这两个小弟的补充辅助,单线程应付起来也游刃有余。(另外,HTML5的web worker也可以看作是一种特定功能类线程实现,它适合分担JS解析线程的长计算任务)


总之,不管是从语言设计的成本来讲,还是从实际的应用场景来讲,浏览器端的脚本语言设计为单线程,是一种绝佳的方案。


JS单线程下的事件循环机制


在介绍事件循环机制前,先了解两个概念。


  • 调用栈

解析和执行js代码的场所。


  • 任务队列(别名还有消息队列、回调队列、事件队列)

即将执行的event handler、ajax callback和setTimeout callback等,都会被当做成一个任务(通常对应一个函数),依次放入到任务队列中。等待JS解析线程从任务队列中取出,放入到调用栈中解析和执行。


事件循环机制,指的是:

JS解析线程会从任务队列的头部,取出一个任务,放到调用栈执行,每执行完一个任务后则再取下一个任务执行,周而复始,直到任务队列为空。如下图所示:


Event Loop


三、前置知识点:浏览器的内部线程及线程间关系


在官方网站,没有找到有对浏览器的内部线程进行了明确分类的文档,我们暂且约定有如下三类:JS解析线程、UI渲染线程和回调类线程(时间线程、事件触发线程和http请求处理线程)


  • JS解析线程

负责将任务队列中的任务,取到调用栈中解析和执行,直到任务队列为空。


  • UI渲染线程

负责页面UI的渲染更新。


  • 时间线程

负责setTimeout和setInterval等时间函数的计算处理,当满足触发条件时,会将事件的

handler(callback)作为一个任务,插入到任务队列的尾部。


  • 事件触发线程

监听用户行为或元素组件状态变化,当满足触发条件时,会将事件的handler(callback)作为一个任务,插入到任务队列的尾部。


  • http请求处理线程

负责http请求的监听和处理。当请求的状态变更时,如果配置了相应的calllback,这些callback会作为一个任务,插入到任务队列的尾部。


三者之间的关系为:

  • JS解析线程和回调类线程可以并发执行;

  • UI渲染线程和回调类线程可以并发执行;

  • JS解析线程和UI渲染线程互斥,同一时间仅能执行一个;

  • UI渲染线程的执行,发生在JS解析线程空闲时,通常是在处理完一个任务之后(不包括其回调函数的完成),即两个任务间。


四、setTimeout的作用


作用及解析过程


讲完了前置知识:JS的出身及语言特性、浏览器的内部线程及线程间关系。就可以进入今天的正题,setTimeout的作用:将要执行的代码封装到一个回调函数,等到预设的时间完成后,会将这个回调函数作为一个新的任务,放到任务队列的尾部,等待JS解析线程处理。具体过程如下:

1. 当setTimeout定时器被解析时,JS解析线程会将其交由浏览器的时间线程处理;

2. 当定时器的预设时间完成时,时间线程会把定时器对应的回调函数封装为一个任务,放到任务队列的尾部;

3. 如果前面有未处理的任务,那么就需要等待其他任务执行完毕方可执行;如果无,则会被立即执行


诧异解答


现在我们可以回答之前的诧异:为什么setTimout(func, 0)可以保证delay足够时间,让UI渲染线程完成页面UI更新,以让Iscroll组件能够正确刷新?

iScroll官方示例


原因是:在setTimeout的预设时间timeout后,其回调函数刷新iScroll组件)会被作为一个新的任务,放入到任务队列尾部,此时相当于把之前的一个任务(对应onCompletion函数的执行)一分为二;而UI渲染线程就是在第一个任务执行完毕之后和第二个任务刷新iScroll组件)开始执行之前,获得了执行机会,又因为UI渲染线程和JS解析线程是互斥的,所以JS解析线程的再次运行,必须等到UI渲染线程完成UI更新。所以等到UI更新成功后(页面UI尺寸已经稳定),第二个任务( 刷新iScroll组件)方被JS解析线程从任务队列中取出,然后放入到调用栈执行,这样就保证了iScroll组件的刷新是在页面UI稳定之后。


setTimeout delay参数的最小值


特别地,当delay参数设为0时,实际上取的值为4ms。在HTML5新规范中,4ms为最小值,如果自设值小于4ms,则取4ms。2010年以后发布的浏览器,均遵循此规范。详情请见:

Reasons for delays longer than specified

https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout


五、Show The Code


讲完了知识点,下面通过几个小程序,来回顾加深。


示例一:setTimeout的作用


  • 源码:setTimeout:create_a_new_task.html


  • 结果:

    


  • 结果分析

输出的结果不是123,而是132。是因为:

当setTimeout的预设时间timeout时,时间线程会将callback封装到一个新的任务(后             面称任务2),并将该任务插入到任务队列尾部。所以JS解析线程先解析任务1:输出           13,再解析任务2: 输出2。


  • 结论

setTimeout会创建一个新的任务。


示例二:任务队列中任务的处理顺序


  • 源码:task:one_end_next_begin.html


  • 结果:

task1的长计算完成前:

task1的长计算完成后:


  • 结果分析:

task 1内部包含一个setTimeout和长计算调用。当task1执行到setTimeout时,会将其交由时间线程处理,然后继续执行长计算调用(大约花费5s,你CPU牛逼可以把输入参数调大)。在task 1仍在进行长计算时,时间线程监测到了setTimeout 的delay完成,并将对应的callback封装到一个新的任务(task 2)中,然后将task 2插入到任务队列尾部。当task 1的长计算执行完毕之后,即task 1执行完毕, JS解析线程再从任务队列中取出task 2执行,最后打印出'task 2 begins'。


  • 结论

任务队列中的任务,只有前一个在调用栈执行完之后(不包括其回调函数的完成),后一个才被取出执行。


示例三:JS解析线程和UI渲染线程的关系


  • 源码:thread:js_free_ui_begin.html    


  • 结果:

        task 1的长计算完成前:


task2的长计算完成后:

  • 结果分析:

task 1中包含一系列UI更新操作和一个长计算调用。虽然JS解析线程先执行了一系列UI更新操作,但是由于task 1的长计算尚未完成,即JS解析线程仍在执行,UI渲染线程无法获得执行机会,所以一直无法在页面看到更新的UI。在长计算完成后(大约5s),JS解析线程空闲,UI渲染线程获得执行机会,之前JS解析线程执行过的UI更新操作,得到了UI渲染线程的真正处理,页面得到更新,故红色的Hello world跃然于page。


  • 结论

UI渲染线程,通常是在两个任务间,获得执行机会。


六、结尾语


本文通过解析setTimeout(func, 0)妙用背后的原理,向各位同学简单介绍了一下JS的事件循环机制和浏览器的内部线程。由于本人水平和经验有限,如有纰漏或建议,欢迎私信。如果觉得不错,也欢迎点赞和转发,你们的支持,将会是我写作的动力,谢谢你的阅读~


示例代码地址:

https://github.com/momopig/simplicity/tree/master/03:setTimeout





  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值