As a developer who switched from other programming languages (C#/Java) to Javascript, in the process of learning Javascript, the operating principle of the setTimeout() method is a part that I encountered that is not easy to understand. This article attempts to combine it with other programming languages. The implementation, from the setTimeout event loop model
The setTimeout() method is not defined by the ecmascript specification, but is a function provided by the BOM. See w3school's definition of the setTimeout() method. The setTimeout() method is used to call a function or calculate an expression after a specified number of milliseconds.
Syntax setTimeout(fn, millisec), where fn represents the code to be executed, which can be a string containing JavaScript code or a function. The second parameter millisec is the time expressed in milliseconds, indicating how long fn needs to be delayed.
After calling the setTimeout() method, the method returns a number. This number is a unique identifier of the planned execution code, which can be used to cancel the timeout call.
At first, my use of setTimeout() was relatively simple, and I did not have a deep understanding of its operating mechanism until I saw the following code
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
In my initial understanding of setTimeout(), the delay was set to 500ms, so the output should be Time elapsed: 500 ms. Because in an intuitive understanding, the Javascript execution engine should be a sequential execution process from top to bottom when executing the above code, and the setTimeout function is executed before the while statement. But in fact, after the above code is run multiple times, the output is delayed by at least 1000ms.
Reminiscent of my past experience in learning Java, the above-mentioned setTimeout() of Javascript confused me. Java has multiple API implementations for setTimeout. Here we take the java.util.Timer package as an example. Use Timer to implement the above logic in Java. After running multiple times, the output is Time elapsed: 501 ms.
import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest { public static void main(String[] args) { // TODO Auto-generated method stub long start = System.currentTimeMillis(); Timer timer = new Timer(); timer.schedule(new MyTask(start), 500); while (System.currentTimeMillis() - start < 1000) {}; } } class MyTask extends TimerTask { private long t; public MyTask(long start) { // TODO Auto-generated constructor stub t=start; } @Override public void run() { // TODO Auto-generated method stub long end = System.currentTimeMillis(); System.out.println("Time elapsed:"+(end - this.t)+ "ms"); } }
Before delving into why this difference occurs in setTimeout(), let’s first talk about the implementation principle of java.util.Timer.
The key elements of the above code are the Timer, TimerTask classes and the schedule method of the Timer class. You can understand its implementation by reading the relevant source code.
Timer: The scheduling class of a Task task. Like the TimerTask task, it is an API class for users to arrange the execution plan of the Task through the schedule method. This class completes Task scheduling through the TaskQueue task queue and TimerThread class.
TimerTask: Implements the Runnable interface, indicating that each task is an independent thread, and allows users to customize their own tasks through the run() method.
TimerThread: Inherited from Thread, it is the class that actually executes the Task.
TaskQueue: A data structure that stores Task tasks. It is internally implemented by a minimum heap. Each member of the heap is a TimeTask. Each task is sorted by the nextExecutionTime attribute value of TimerTask. The task with the smallest nextExecutionTime is at the front of the queue, so that the earliest implement.
After looking at Java.util.Timer’s implementation of setTimeout(), let’s go back to the setTimeout() method of Javascript and see why the previous output does not match expectations.
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
It is not difficult to see by reading the code that the setTimeout() method is executed before the while() loop. It declares that it "hopes" to execute the anonymous function once after 500ms. This statement, that is, the registration of the anonymous function, is in the setTimeout() method. It takes effect immediately after execution. The while loop in the last line of the code will continue to run for 1000ms. The delay time of the output of the anonymous function registered through the setTimeout() method is always greater than 1000ms, indicating that the actual call to this anonymous function is blocked by the while() loop. The actual call is in The while() loop is actually executed after blocking.
In Java.util.Timer, the solution to scheduled tasks is implemented through multi-threading. The task object is stored in the task queue, and a dedicated scheduling thread completes the execution of the task in a new sub-thread. When registering an asynchronous task through the schedule() method, the scheduling thread immediately starts working in the child thread, and the main thread does not block the running of the task.
This is a major difference between Javascript and languages such as Java/C#, that is, the single-thread mechanism of Javascript. In the existing browser environment, the Javascript execution engine is single-threaded. The statements and methods of the main thread will block the running of scheduled tasks. The execution engine will only execute the scheduled tasks after executing the statements of the main thread. This is The period of time may be greater than the delay time set when registering the task. At this point, the mechanisms of Javascript and Java/C# are very different.
How does setTimeout() work in a single-threaded Javascript engine? Here we will mention the event loop model in the browser kernel. To put it simply, outside of the Javascript execution engine, there is a task queue. When the setTimeout() method is called in the code, the registered delay method will be handed over to other modules in the browser kernel (taking webkit as an example, it is the webcore module) Processing, when the delay method reaches the trigger condition, that is, when the set delay time is reached, this delay method is added to the task queue. This process is handled by other modules of the browser kernel and is independent of the main thread of the execution engine. After the execution of the main thread method is completed and the execution engine reaches the idle state, it will sequentially obtain tasks from the task queue for execution. This process is a continuous loop. The process is called the event loop model.
Referring to the information in a speech, the above event loop model can be described by the following figure.
When the main thread of the Javascript execution engine runs, a heap and stack are generated. The code in the program enters the stack one by one and waits for execution. When the setTimeout() method is called, that is, when the WebAPIs method on the right side of the figure is called, the corresponding module of the browser kernel starts processing the delay method. When the delay method reaches the trigger condition, the method is Added to the task queue for callback, as long as the code in the execution engine stack is executed, the main thread will read the task queue and execute those callback functions that meet the trigger conditions in sequence.
Use examples from the speech to further illustrate
Taking the code in the picture as an example, when the execution engine starts executing the above code, it is equivalent to adding a main() method to the execution stack. When continuing to start console.log('Hi'), the log('Hi') method is pushed onto the stack. The console.log method is a common method supported by the webkit kernel, not the method involved in WebAPIs in the previous figure, so log here ('Hi') method is immediately popped off the stack and executed by the engine.
After the console.log('Hi') statement is executed, the log() method is popped out of the stack and Hi is output. The engine continues down and adds setTimeout(callback,5000) to the execution stack. The setTimeout() method belongs to the method in WebAPIs in the event loop model. When the engine pops the setTimeout() method from the stack for execution, it hands over the delayed execution function to the corresponding module, that is, the timer module on the right side of the figure for processing.
When the execution engine pops setTimeout from the stack for execution, it hands over the delay processing method to the webkit timer module, and then immediately continues to process the following code, so log('SJS') is added to the execution stack, and then log('SJS' ) is executed out of the stack and SJS is output. After the execution engine executes console.log('SJS'), the program processing is completed and the main() method is also popped off the stack.
At this time, 5 seconds after the setTimeout method is executed, the timer module detects that the delay processing method reaches the trigger condition, so it adds the delay processing method to the task queue. At this time, the execution stack of the execution engine is empty, so the engine starts polling to check whether there are tasks in the task queue that need to be executed. It detects that the delay method has reached the execution condition, so it adds the delay method to the execution stack. The engine found that the delay method called the log() method, so it pushed the log() method onto the stack. Then the execution stack is popped out one after another, output there, and clear the execution stack.
After clearing the execution stack, the execution engine will continue to poll the task queue to check whether there are still tasks that can be executed.
到这里已经可以彻底理解下面代码的执行流程,执行引擎先将setTimeout()方法入栈被执行,执行时将延时方法交给内核相应模块处理。引擎继续处理后面代码,while语句将引擎阻塞了1秒,而在这过程中,内核timer模块在0.5秒时已将延时方法添加到任务队列,在引擎执行栈清空后,引擎将延时方法入栈并处理,最终输出的时间超过预期设置的时间。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
前面事件循环模型图中提到的WebAPIs部分,提到了DOM事件,AJAX调用和setTimeout方法,图中简单的把它们总结为WebAPIs,而且他们同样都把回调函数添加到任务队列等待引擎执行。这是一个简化的描述,实际上浏览器内核对DOM事件、AJAX调用和setTimeout方法都有相应的模块来处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块,html的解析,css样式的计算等都由webcore实现。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现,这里还是继续以setTimeout为例,看下timer模块的实现。
Timer类是webkit 内核的一个必需的基础组件,通过阅读源码可以全面理解其原理,本文对其简化,分析其执行流程。
通过setTimeout()方法注册的延时方法,被传递给webcore组件timer模块处理。timer中关键类为TheadTimers类,其包含两个重要成员,TimerHeap任务队列和SharedTimer方法调度类。延时方法被封装为timer对象,存储在TimerHeap中。和Java.util.Timer任务队列一样,TimerHeap同样采用最小堆的数据结构,以nextFireTime作为关键字排序。SharedTimer作为TimerHeap调度类,在timer对象到达触发条件时,通过浏览器平台相关的接口,将延时方法添加到事件循环模型中提到的任务队列中。
TimerHeap采用最小堆的数据结构,预期延时时间最小的任务最先被执行,同时,预期延时时间相同的两个任务,其执行顺序是按照注册的先后顺序执行。
var start = new Date; setTimeout(function(){ console.log('fn1'); }, 20); setTimeout(function(){ console.log('fn2'); }, 30); setTimeout(function(){ console.log('another fn2'); }, 30); setTimeout(function(){ console.log('fn3'); }, 10); console.log('start while'); while (new Date - start < 1000) {}; console.log('end while');
上述代码输出依次为
start while end while fn3 fn1 fn2 another fn2
1.《Javascript异步编程》
2.JavaScript 运行机制详解:再谈Event Loophttp://www.ruanyifeng.com/blog/2014/10/event-loop.html
3.Philip Roberts: Help, I'm stuck in an event-loop.https://vimeo.com/96425312
4.How JavaScript Timers Work.http://ejohn.org/blog/how-javascript-timers-work/
5.How WebKit’s event model works.http://brrian.tumblr.com/post/13951629341/how-webkits-event-model-works
6.Timer实现.http://blog.csdn.net/shunzi__1984/article/details/6193023
The above is the detailed content of Let's talk about the event loop model from setTimeout. For more information, please follow other related articles on the PHP Chinese website!