Home > Article > Web Front-end > In-depth understanding of JavaScript timing mechanism
This article introduces the JavaScript timing mechanism. To understand the timing mechanism of JavaScript, you must know the running mechanism of JavaScript.
First of all, it is stated that JavaScript is single-threaded (JavaScript engine thread) event-driven.
1. There are multiple threads in the browser
The most basic threads included in a browser:
1. JavaScript engine thread.
2. Timer thread, setInterval and setTimeout will trigger this thread.
3. Browser event trigger thread, this thread will trigger onclick, onmousemove and other browser events.
4. The interface rendering thread is responsible for rendering the HTML elements of the browser interface. Note: While the JavaScript engine is running the script, the interface rendering thread is in a suspended state. That is to say, when JavaScript is used to operate nodes in the interface, it will not be reflected immediately. It will not be reflected until the JavaScript engine thread is idle. (This is the last thing to say)
5. HTTP request thread (Ajax requests are also among them).
The above threads cooperate with each other to complete the work under the control of the browser kernel (I don’t know the details).
2. Task Queue
We know that JavaScript is single-threaded, and all JavaScript code runs in the JavaScript engine thread. Teacher Ruan Yifeng's article calls this thread the main thread, which is an execution stack. (The following content is mainly based on the understanding and summary of Teacher Ruan Yifeng's article.)
We can regard these JavaScript codes as tasks one by one. These tasks are divided into synchronous tasks and asynchronous tasks. Synchronous tasks (such as variable assignment statements, alert statements, function declaration statements, etc.) are executed directly on the main thread in sequence, and asynchronous tasks (such as various events triggered by browser event trigger threads, use server responses returned by Ajax etc.) are queued in the task queue (also called event queue, message queue) in chronological order, waiting to be executed. As long as the tasks on the main thread are executed, the task queue will be checked to see if there are any tasks waiting in the queue. If so, the queued tasks will be entered into the main thread for execution.
For example, the following example:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>定时机制</title> <style type="text/css"> body{ margin: 0; padding: 0; position: relative; height: 600px; } #test{ height: 30px; width: 30px; position: absolute; left: 0; top: 100px; background-color: pink; } </style> </head> <body> <div id="test"> </div> <script> var pro = document.getElementById('test'); pro.onclick = function() { alert('我没有立即被执行。'); }; function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } test(); </script> </body> </html>
In this example, it takes about 8~9 seconds for the test() function to be executed, so when we open this page and click the pink square before 8 seconds, it will not pop up immediately The prompt box will not pop up until 8 seconds later. If you click the pink box several times before 8 seconds, it will pop up several times after 8 seconds.
When we open this page, the main thread first declares the function test, then declares the variable pro, then assigns the p node to pro, then adds a click event to the p node, specifies a callback function (suspension), and then calls the test function, Execute the code within it. During the execution of the code in the test function, we clicked on the p node. The browser event triggering thread detected this event and placed the event in the task queue so that the task on the main thread (here is the test function) is completed. Finally, this event is found when checking the task queue and the corresponding callback function is executed. If we click multiple times, these multiple triggered events will be queued in the task queue according to the triggering time (you can add a click event to another element and alternately click on different elements to verify).
The following is a summary of the operating mechanism of the task:
The operating mechanism of asynchronous execution is as follows. (The same is true for synchronous execution, because it can be regarded as asynchronous execution without asynchronous tasks.)
1. All synchronous tasks are executed on the main thread, forming an execution context stack.
2. In addition to the main thread, there is also a "task queue". As long as the asynchronous task has running results, an event is placed in the "task queue".
3. Once all synchronization tasks in the "execution stack" are completed, the system will read the "task queue" to see what events are in it. Those corresponding asynchronous tasks end the waiting state, enter the execution stack, and start execution.
4. The main thread continues to repeat the third step above.
3. Events and callback functions
When we specify an event for a DOM element, we will specify a callback function so that the corresponding code can be executed when the event actually occurs.
The callback function of the event in the main thread will be suspended. If there is a corresponding event being queued in the task queue, the corresponding callback function will be executed when the main thread detects it. We can also say that the main thread performs asynchronous tasks, which is executing the corresponding callback function.
4. Event loop
The process of the main thread checking events in the task queue is cyclic, so we can draw a diagram of the event loop:
In the above picture, the main thread generates the heap and execution stack , after the tasks in the stack are executed, the main thread checks the events that have occurred in the task queue and are passed in by other threads. If the event at the top is detected, it will find out the event corresponding to the event from the suspended callback function. The callback function is then executed on the execution stack, and this process is repeated.
5. Timer
结合以上知识,下面探讨JavaScript中的定时器:setTimeout()和setInterval()。
setTimeout(func, t)是超时调用,间隔一段时间后调用函数。这个过程在事件循环中的过程如下(我的理解):
主线程执行完setTimeout(func, t);语句后,把回调函数func挂起,同时定时器线程开始计时,当计时等于t时,相当于发生了一个事件,这个事件传入任务队列(结束计时,只计时一次),当主线程中的任务执行完后,主线程检查任务队列发现了这个事件,就执行挂起的回调函数func。我们指定的时间间隔t只是参考值,真正的时间间隔取决于执行完setTimeout(func, t);语句后的代码所花费的时间,而且是只大不小。(即使我们把t设为0,也要经历这个过程)。
setInterval(func, t)是间歇调用,每隔一段时间后调用函数。这个过程在事件循环中的过程与上面的类似,但又有所不同。
setTimeout()是经过时间t后定时器线程在任务队列中添加一个事件(注意是一个),而setInterval()是每经过时间t(一直在计时,除非清除间歇调用)后定时器线程在任务队列中添加一个事件,而不管之前添加的事件有没有被主线程检测到并执行。(实际上浏览器是比较智能的,浏览器在处理setInterval的时候,如果发现已任务队列中已经有排队的同一ID的setInterval的间歇调用事件,就直接把新来的事件 Kill 掉。也就是说任务队列中一次只能存在一个来自同一ID的间歇调用的事件。)
举个例子,假如执行完setInterval(func, t);后的代码要花费2t的时间,当2t时间过后,主线程从任务队列中检测到定时器线程传入的第一个间歇调用事件,func开始执行。当第一次的func执行完毕后,第二次的间歇调用事件早已传入任务队列,主线程马上检测到第二次的间歇调用事件,func函数又立即执行。这种情况下func函数的两次执行是连续发生的,中间没有时间间隔。
下面是个例子:
function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } function test2() { var d = new Date().valueOf(); //var e = d-a; console.log('我被调用的时刻是:'+d+'ms'); //alert(1); } setInterval(test2,3000); test();
结果:
为什么8.6秒过后没有输出两个一样的时刻,原因在上面的内容中可以找到。
执行例子中的for循环花费了8601ms,在执行for循环的过程中队列中只有一个间歇调用事件在排队(原因如上所述),当8601ms过后,第一个间歇调用事件进入主线程,对于这个例子来说此时任务队列空了,可以再次传入间歇调用事件了,所以1477462632228ms这个时刻第二次间歇调用事件(实际上应该是第三次)传入任务队列,由于主线程的执行栈已经空了,所以主线程立即把对应的回调函数拿来执行,第二次调用与第一次调用之间仅仅间隔了320ms(其实8601+320=8920,差不多就等于9秒了)。我们看到第三次调用已经恢复正常了,因为此时主线程中已经没有其他代码了,只有一个任务,就是隔一段时间执行一次间歇调用的回调函数。
用setTimeout()实现间歇调用的例子:
function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } function test2(){ var d = new Date().valueOf(); console.log('我被调用的时刻是:'+d+'ms'); setTimeout(test2,3000); } setTimeout(test2,3000); test();
结果:
每两次调用的时间间隔基本上是相同。想想为什么?
再看一个例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Flex布局练习</title> <style type="text/css"> body{ margin: 0; padding: 0; position: relative; height: 600px; } #test{ height: 30px; width: 30px; position: absolute; left: 0; top: 100px; background-color: pink; } </style> </head> <body> <div id="test"> </div> <script> var p = document.createElement('p'); p.style.width = '50px'; p.style.height = '50px'; p.style.border = '1px solid black'; document.body.appendChild(p); alert('ok'); </script> </body> </html>
这个例子的结果是提示框先弹出,然后黑色边框的p元素才出现在页面中。原因很简单,就一句话:
在JavaScript引擎运行脚本期间,界面渲染线程都是处于挂起状态的。也就是说当使用JavaScript对界面中的节点进行操作时,并不会立即体现出来,要等到JavaScript引擎线程空闲时,才会体现出来。