在 CSS3 出来以前,我们通过 setTimeout
和 setInterval
来实现动画,我们熟悉并且热衷于它们的使用,但它背后的原理却未深究。把握核心,深究问题的根本,永远是我们的核心,今天我们了解一下定时器背后的男人——单线程。
|
|
出乎意料,上面这段代码,会先输出 1,然后输出 0。(等我们掌握了单线程,也就迎刃而解了)
JS的单线程
一个浏览器进程,拥有很多线程,比如 GUI 渲染线程、事件触发线程、HTTP 请求线程,以及 js 引擎线程。 js 单线程就是指 js 的线程只有一条,也就是说,同一时间,只有某个任务能够执行,这段代码可能是初始化执行的代码,可能是某个元素的点击事件,也可能是 ajax 请求的回调函数。
javascript 操作几乎都是通过页面展示出来的,大多会用到 DOM 操作,设想一下,如果我有两个操作涉及到了同一个元素,而他们又是冲突的,浏览器需要如何处理?当然,我们也可以像其他语言一样,为 javascript 设计诸如 互斥锁
或者 信号量
的机制来实现多线程的协调处理,但是这会给定位轻量级脚本语言的 javascript 带来繁杂计算量的成本。
js 线程执行的时候,会阻塞其他线程的执行。比如下面这段代码,我们会发现,浏览器不会在每一个循环里去重新渲染页面,它会等待这段 js 执行完毕,再去执行 GUI 渲染线程,当然,浏览器会进行一定的优化,所以最终我们看到的是,浏览器直接显示出 1000000,而不是 0 - 1000000 极速闪过。
|
|
上面我提到了,一个浏览器有一个进程,但是我们可爱的 Chrome 自有其可爱之处,它为了防止——因为一个页面的 js 的不好的执行影响浏览器的所有页面,所以它为每个页面开了一个进程。当然,这里我只对 Chrome 和 Firefox 进行了测试,ie 我是一直都知道的,它是整个浏览器一个进程。整个浏览器一个进程是什么意思?所有页面共用一个 js 线程,也就是,A 页面如果有一段耗时很长的代码正在执行, B 页面触发一个元素的点击事件并不会立即执行,需要等待 A 页面的 js 执行完毕。
Event Loop
单线程意味着同一时间只能执行一个任务,是不是说 Javascript 是同步的?那么为什么还会有 XHR 异步请求?如果有好几个事件等着被执行,该如何安排?
js 线程上的代码是顺序执行的,是同步的。而非同步的,诸如响应事件, ajax 请求,定时器事件,它们都被放置到一个队列里面,我们称之为任务队列。
前面我们讲过,函数被调用的时候,会创建上下文,形成上下文栈,当函数返回后,其上下文会退出栈。如果栈空了,就会从任务队列中,读取排在最前面的事件并执行。
下面我们就要主要讲一讲任务队列的由来了。
身处执行栈中的代码会调用外部的各种 API ,我们拿 XMLHttpRequest 来说。JS 的执行线程发起异步请求,浏览器会开一条新的 HTTP 请求线程来执行请求,这时候 JS 的这一步任务已完成,会继续执行下面的其他任务。然后在之后的某一刻, js 的事件触发线程监听到刚才的 HTTP 请求已完成,它就会把回调函数放入任务队列中的尾部等待执行。所以,js 的单线程 和 异步 更多地应该是属于浏览器的行为。
任务队列就是由这些异步事件或者异步请求的回调函数组成的,JS 线程最后执行的异步任务,其实就是这些回调函数。
任务队列是一个先进先出(FIFO)的队列,排在前面的事件,会最先被读取执行。然后再创建执行上下文,推入栈中,依此循环往复。
借助下面的图片来看一下,仿照的 《Help, I’m stuck in an event-loop》 中的图片。
定时器
setTimeout
和 setInterval
是通过定时器线程进行处理的,它会在指定时间之后将回调函数放入任务队列中。我们来看一下下面这张图(来自于《JavaScript忍者秘籍》)。
由于在 0-10ms 期间触发了 click
事件,所以事件处理函数被放置在了任务队列,并且处于任务队列最前端,但是由于当前代码还未执行完毕,所以不能立即读取执行 click
事件回调函数。10ms 的时候 setTimeout
和 setInterval
的回调函数被放置到任务队列,紧随 click
事件。
到了 18ms 的时候,js 线程的代码执行完毕,先执行任务队列最前端的 click
事件回调函数,在其执行期间,20ms 处,setInterval
第二次想要插入队列尾部,但是由于拥有相同回调函数的 setInterval
实例已经存在了,所以这次的将被废弃,不能继续插入到队列中。
在 27ms 处, click
事件结束,相应地开始执行 setTimeout
的回调函数,在其执行期间,30ms 处,setInterval
第三次想要插入队列尾部,同理,不能继续插入到队列中。
在 34ms 处, setTimeout
事件结束,相应地开始执行 setInterval
的回调函数,在其执行期间,40ms 处,setInterval
第四次想要插入队列尾部,由于这时候队列中已经没有 setInterval
的实例了,所以成功插入到队列中。
在 42ms 处, 第一个 setInterval
事件结束,相应地开始执行第二个 setInterval
的回调函数…
零延迟 setTimeout
延迟时间为 0 的 setTimeout
不一定就是立即执行,这需要看当前栈是否为空,以及任务队列中在其前面是否有正在排队的任务。
我们上面所看的这段代码,就是指当前代码执行完后(等待当前栈为空),立即执行回调函数(假设任务队列中在其之前没有正在排队的任务)。
|
|
分解长耗时任务
利用上面我们刚刚学习的 零延迟setTimeout
可以给我们带来一个实际的开发福利,当我们进行一个需要耗时特别久的任务时,我们应当考虑一下,用户是否能够愿意等待这么久。所以,我么需要进行一些必要的分解。
|
|
这段代码创建了 200000 个DOM节点,对于浏览器来说是个挺大的负担,浏览器也需要一段时间来计算并渲染,也极有可能影响用户的交互操作。
所以我们需要做的就是把它分成几个步骤,好让后面的代码能够继续执行,也能减轻浏览器的压力。
|
|
- 浏览器是多线程的,js 是单线程的。
- 异步任务的回调函数都会被添加到任务队列中,当且仅当执行栈为空时,才会读取任务队列的第一个任务。
- 单线程带来的是某些时候事件响应不按照我们预定的时间节点执行。
- 对于耗时过久的任务,我们需要将它分解成多步进行,具体分为几步,根据实际情况即可。