The mediate mediate mediate mediate mediate mediate macro task: Using setTimeout, setInterval, and setImmediate to mediate A callback function called by Process. nextTick, Promises, MutationObserver into the execution queue

Take a look at this code:

// example.js  
console.log('script start');
setTimeout(function() {  
  console.log('setTimeout');  
}, 0);
Promise.resolve().then(function() {  
  console.log('promise1');  
}).then(function() {  
  console.log('promise2');  
});
console.log('script end');
Copy the code

Running results:

script start

script end

promise1

promise2

setTimeout

Script start, script end, promise1, promise2 and setTimeout. SetTimeout is set to 0, but why print it at the end?

In an Event loop, the macro task runs before the micro task.

You might say that setTimeout should print first. Macro tasks run before microtasks, and there are no macro tasks queued before setTimeout. Yeah, you’re right. However, an event must occur first, otherwise JS will not run any code. This event will enter the macro task queue.

When you run any JS file, the JS engine wraps the file contents in a function and associates that function with an event (start or launch). The JS engine emits the start event, which is queued (macro task queue). At initialization, the JS engine pulls the first task from the macro task queue and executes the related callback function, at which point our code runs.

  1. Reading file contents
  2. Wrap content in functions
  3. Think of this function as an event handler, associated with the program’s “start” or “launch” events
  4. Perform other initializations
  5. Issue the start event for the program
  6. The event is put into the queue
  7. The JS engine pulls that event from the queue and executes the handler registered with it
  8. Our program is running!

So we see that running the script is the first macro task to queue up. The callback function then executes our code. Next, script start is printed by console.log. The setTimeout call then places the callback into the macro task queue. Next, the Promise call queues a microtask, and console.log prints the script end. Then the initial callback (macro task) ends.

The top one is the macro task, and the micro task starts to run. The Promise (then) callback prints promise1, returns it and adds a new microtask via the then function. Execute the new microtask to print promisE2 (remember, microtasks can add additional microtasks during a cycle, but they will be executed until the next macro task executes). There are no new microtasks, and the microtask queue is empty. The original macro task is cleared, leaving the setTimeout callback.

At this point, UI rendering starts running (if any). The next macro task, the setTimeout callback, is then executed. SetTimeout is printed and cleared from the macro queue. Then, with no more tasks, the call stack is empty and the JS engine relinquishes control.

Next, let’s simulate the event loop process with our JS code.

// js_engine.js  
1.let macrotask = []  
2.let microtask = []  
3.let js_stack = [] // microtask  
4.function setMicro(fn) {  
      microtask.push(fn)  
    } // macrotask  
5.function setMacro(fn) {  
      macrotask.push(fn)  
     } // macrotask  
6.function runScript(fn) {  
      macrotask.push(fn)  
     }
7.➥ global.setTimeout = function setTimeout(fn, milli) {  
      macrotask.push(fn)  
     } 

// your script here

8.function runScriptHandler() {  
      8I. ➥for (var index = 0; index < js_stack.length; index++) {  
          8II. ➥eval(js_stack[index])  
      }  
    } 

// start the script execution  
9.➥ the runScript (runScriptHandler)// run macrotask  

10.for (let ii = 0; ii < macrotask.length; ii++) {  
11.eval(macrotask[ii])()  
      if(microtask.length ! =0) {  
          // process microtasks  
12.for (let __i = 0; __i < microtask.length; __i++) {  
              eval(microtask[__i])()  
          }  
          // empty microtask  
          microtask = []  
      }  
   }
Copy the code

First, we define macroTask (1.) and MicroTask (2.) queues. Each time a macrotask (such as a setTimeout callback) is added to the macrotask queue (1.), the microtask queue (2.) is also added to the microtask function.

Js_stack (3.) Here is the code we need to execute. In fact, it is the code we write in the JS file. To execute them, we loop through the stack and call them with the eval function.

Next, we define macro/microtask setup functions: setMicro (4.), setMacro (5.), runScript (6.), and setTimeout (7.). These functions take a callback function as an argument and put it into the macro/micro task queue.

Those functions (4. 5. 6.) are called to set micro/macro tasks. In this case, we’re just putting these callbacks into the appropriate queue. SetMicro is a microtask (Settings) function, so its callbacks are added to MicroTask. We’ve redefined setTimeout to hook. So when we call setTimeout, we’re actually going to get this function that we defined.

Since it (setTimeout) is a macro task function, we put the callback into the macro task queue. We also have a runScript function that emulates the global “start” event of the JS engine during initialization. Since the global event (” start “) is a macro task, we put the (runScript) callback into the macro task queue. RunScriptHandler (8.) boots all scripts in js_stack (that is, emulates the code in our JS files).

(I didn’t quite understand the author’s meaning of this paragraph, so I wrote it in my own way. If there are any questions, PLEASE refer them to me and I will correct them.) We start by executing the runScript function, putting the initialized function into macrotask as a macrotask (not executed), and then start running to (10.). After each macro task (11.), all microtasks (currently) are completed (12.).

We loop the entire macroTask array, execute the functions in the current index, and execute the functions in the current index through a sub-for-loop, although new tasks may be added to some of the microtasks. The child for-loop runs until the MicroTask is empty. Wait until the microTask is complete before starting the next MacroTask execution.

As a practical matter, let’s run the following js code:

console.log('start')
console.log(`Hi, I'm running in a custom JS engine`)
console.log('end')
Copy the code

We need to place each row in the Js_stack array as a string:

.// your script here
js_stack.push(`console.log('start')`)
js_stack.push("console.log(`Hi, I'm running in a custom JS engine`)")
js_stack.push(`console.log('end')`)...Copy the code

This js_stack is just like the code in our JS file. The JS engine reads it and executes each one. This is actually what we did in step (8.). We for-loop(8I.)js_stack and execute each statement (8II.) with eval.

If we run Node js_engine.js, we can see:

start
Hi, I'm running in a custom JS engine
end
Copy the code

OK, let’s do something else. Let’s use the original example (example.js) with a slight modification:

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
setMicro((a)= > {
  console.log('micro1')
  setMicro((a)= > {
    console.log('micro2')})})console.log('script end');
Copy the code

We got rid of Promises and replaced them with setMicro. It’s the same, it’s recording a microtask. We can see that when the setMicromicro1 callback is executed from the microtask queue, it adds a new microtask, micro2, just as we did with example.js Promise.

So, we want to do the following:

script start
script end
micro1
micro2
setTimeout
Copy the code

To execute in our custom JS engine, let’s convert it again (instead of //your script here) :

// js_engine.js. js_stack.push(`console.log('script start'); `)
js_stack.push(`setTimeout(function() { console.log('setTimeout'); }, 0); `)
js_stack.push(`setMicro(()=> { console.log('micro1') setMicro(()=> { console.log('micro2') }) })`)
js_stack.push(`console.log('script end'); `)...Copy the code

Then, running Node js_engine.js, we get:

$ node js_engine
script start
script end
micro1
micro2
setTimeout
Copy the code

Just like a real JS engine. We modeled the real JS engine correctly.

RunScript registers our code as a macro task and exits, its macro task callback runs and our code prints script start, setTimeout sets a macro task, and setMicro sets a microtask, micro1. Script end is printed last. When each MacroTask runs, all microtasks in the MicroTask queue are executed. Micro1 This callback runs printing micro1 and also adds another microtask, micro2. After the micro1 microtask runs, the micro2 microtask runs prints micro2. After exiting again, there are no other microtasks, so the macro task continues. The setTimeout callback runs prints setTimeout. When there are no more macro tasks in the queue, the loop exits and our JS engine exits.

The main points are as follows:

  • Tasks come from the task queue.
  • Our handwritten code is packaged as a macro task, not a microtask. (THIS is my own paraphrase and I don’t really understand the original explanation.)
  • Microtasks start after the current (macro) task completes, and all microtasks are completed before the next macro task cycle.
  • Microtasks (while executing) can add new microtasks. All (microtasks) are completed before the next macro task.
  • UI rendering runs after all microtasks have been executed

conclusion

In this article, we simulate the task queue of JS engine and see how the tasks in the queue are executed. And we learned more than just task queues: microtasks and macro tasks. All microtasks are executed within a macro task execution cycle.

Feel free to use this custom JS engine to learn and understand the real JS engine.

The original text is here

I just translated the macro/micro tasks part of the article, welcome to correct any mistakes.