Skip to content
Back to blog

Execution Stack and Event Loop

Notes on the execution stack and the event loop.

Updated

Execution Context

  • Explanation one: before a piece of JavaScript code runs, it needs to be compiled by the JavaScript engine; only after compilation finishes does it enter the execution phase. After compilation, the code produces an execution context (Execution Context) and executable code (Executable Code).
  • Explanation two: the JavaScript engine does not analyze and execute a program line by line, but rather segment by segment. When executing a segment of code, it does some "preparation work"; a more professional name for this "preparation work" is "execution context".

The Execution Context Stack

The JavaScript engine creates an execution context stack (ECS) to manage execution contexts

  • When JavaScript encounters the following piece of code
function fun3() {
  console.log('fun3')
}
function fun2() {
  fun3()
}
function fun1() {
  fun2()
}
fun1()
// Pseudocode
ECStack = []
ECStack = [globalContext]

// fun1()
ECStack.push(fun1_functionContext)

// func1 is found to call func2
ECStack.push(fun2_functionContext)

// func2 is found to call func3
ECStack.push(fun3_functionContext)

// fun3 finishes executing
ECStack.pop()

// fun2 finishes executing
ECStack.pop()

// fun1 finishes executing
ECStack.pop()

// The task is complete; the bottom of ECStack always holds globalContext

Every execution context has three important properties:

  1. Variable object (Variable object, VO)
  2. Scope chain (Scope chain)
  3. this

Global Context

  1. The global object is a predefined object that serves as a placeholder for JavaScript's global functions and global properties. Through the global object, all other predefined objects, functions, and properties can be accessed.
  2. In top-level JavaScript code, the keyword this can be used to reference the global object. Because the global object is the head of the scope chain, this means all unqualified variable and function names are looked up as properties of that object.
  3. For example, when JavaScript code references the parseInt() function, it references the parseInt property of the global object. The global object being the head of the scope chain also means that all variables declared in top-level JavaScript code become properties of the global object.
  • Simply put: in client-side JavaScript, the global object is the Window object

Function Context

  • In a function context, the variable object is represented by the activation object (AO)

The Function Execution Process

  • The code of an execution context is processed in two phases: analysis and execution, which can also be called:
  1. Entering the execution context;
  2. Code execution;

Variable Object (VO)

A variable object includes:

  1. All formal parameters of the function (if it is a function context)
    • A property of the variable object is created from the name and the corresponding value
    • Without an actual argument, the property value is set to undefined
  2. Function declarations
    • A property of the variable object is created from the name and the corresponding value (the function object (function-object))
    • If the variable object already has a property with the same name, that property is completely replaced
  3. Variable declarations
    • A property of the variable object is created from the name and the corresponding value (undefined)
    • If the variable name is the same as an already-declared formal parameter or function, the variable declaration does not interfere with the existing property
function foo(a) {
  var b = 2;
  function c() {}
  var d = function () {};
  b = 3;
}
foo(1);

// After entering the execution context, the AO at this point is:
AO = {
  arguments: {
    0: 1,
    length: 1,
  },
  a: 1,
  b: undefined,
  c: reference to function c(){},
  d: undefined,
};

// After the code finishes executing, the AO at this point is:
AO = {
  arguments: {
    0: 1,
    length: 1,
  },
  a: 1,
  b: 3,
  c: reference to function c(){},
  d: reference to FunctionExpression "d",
};
function foo() {
  console.log(a)
  a = 1
}
foo() // Uncaught ReferenceError: a is not defined

function bar() {
  a = 1
  console.log(a)
}
bar() // 1

// This is because "a" in the function is not declared with the var keyword, so it is not stored in the AO.
// When the second snippet executes console, the global object has already been given the property a, so the value 1 of a can be found on the global object
console.log(foo);
function foo() {
  console.log("foo");
}
var foo = 1;

// This prints the function, not undefined
// This is because when entering the execution context, function declarations are processed first, then variable declarations; if a variable name is the same as an already-declared formal parameter or function, the variable declaration does not interfere with the existing property.

Scope Chain

  • When looking up a variable, the engine first searches the variable object of the current context; if it is not found, it searches the variable object of the parent (lexical parent) execution context, all the way up to the variable object of the global context, that is, the global object. The linked list formed by the variable objects of multiple execution contexts is called the scope chain
  • A function's scope is determined when the function is defined
// For example:
function foo() {
  function bar() {
    // ...
  }
}

// When the functions are created, their respective [[scope]] values are:
foo['[[scope]]'] = [
  globalContext.VO
]

bar['[[scope]]'] = [
  fooContext.AO,
  globalContext.VO
]

this

Reference
  • The Reference here is a Specification Type, that is, "an abstract type that only exists in the specification". They exist to better describe the language's underlying behavioral logic, but do not exist in actual js code

A Reference consists of three components:

  1. base value: the object the property belongs to, or an EnvironmentRecord; its value can only be one of undefined, Object, Boolean, String, Number, environment record
  2. referenced name: the name of the property
  3. strict: a flag indicating whether it is strict mode
How to Determine the Value of this
  1. Evaluate the MemberExpression and assign the result to ref

    Simply put, MemberExpression is the part to the left of the ().

  2. Check whether ref is a Reference type.
    1. If ref is a Reference and IsPropertyReference(ref) is true, then the value of this is GetBase(ref)
    2. If ref is a Reference and the base value is an Environment Record, then the value of this is ImplicitThisValue(ref)
    3. If ref is not a Reference, then the value of this is undefined
const value = 1

const foo = {
  value: 2,
  bar() {
    return this.value
  },
}

// Example 1
console.log(foo.bar())
// Example 2
console.log(foo.bar())
// Example 3
console.log((foo.bar = foo.bar)())
// Example 4
console.log((false || foo.bar)())
// Example 5
console.log((foo.bar, foo.bar)())
Example 1 (foo.bar())
// This expression returns a Reference type; the value is
const Reference = {
  base: foo,
  name: 'bar',
  strict: false,
}

// The value is a Reference type, so what is the result of IsPropertyReference(ref)?

// We introduced the IsPropertyReference method earlier: if the base value is an object, the result is true.

// The base value is foo, which is an object, so IsPropertyReference(ref) is true.

// Now we can determine the value of this:

// this = GetBase(ref),
// GetBase was also introduced earlier: it gets the base value, which in this example is foo, so the value of this is foo, and the result of Example 1 is 2!
Example 2 ((foo.bar)())
  • In fact, () does not evaluate the MemberExpression, so the result is the same as Example 1
Example 3 ((foo.bar = foo.bar)())
  • There is an assignment operator; the value returned by (foo.bar = foo.bar) is not a Reference type
  • this is undefined; in non-strict mode, when the value of this is undefined, it is implicitly converted to the global object.
Example 4 ((false || foo.bar)())
  • (false || foo.bar) does not return a Reference type; this is undefined
Example 5 ((foo.bar, foo.bar)())
  • (foo.bar, foo.bar) does not return a Reference type; this is undefined
Results
const value = 1

const foo = {
  value: 2,
  bar() {
    return this.value
  },
}

// Example 1
console.log(foo.bar()) // 2
// Example 2
console.log(foo.bar()) // 2
// Example 3
console.log((foo.bar = foo.bar)()) // 1
// Example 4
console.log((false || foo.bar)()) // 1
// Example 5
console.log((foo.bar, foo.bar)()) // 1

Macro Tasks and Micro Tasks

When the execution stack is emptied or a macro task finishes executing, all micro tasks are cleared before continuing with the execution stack / macro tasks

Macro Tasks (macro task)

Usually triggered by the host environment (the browser or Node.js), used to handle tasks that need to interact with the external environment

  • setTimeout
  • setInterval
  • setImmediate
  • MessageChannel
  • Asynchronous I/O operations
  • UI rendering

Micro Tasks (micro task)

Produced by JavaScript itself, used to handle logic that needs an immediate response, such as the follow-up processing of asynchronous operations.

  • Promise.then
  • async / await
  • queueMicrotask
  • requestAnimationFrame
  • MutationObserver
  • process.nextTick

Special Cases

When listening for events, different event callbacks are invoked in different orders.

  • For DOM-blocking events such as click, the callback is also executed synchronously
  • For non-blocking events such as message, the callback is inserted into the macro task queue after the event is triggered
  • Events defined by addEventListener and onXXX can coexist; the one defined first is invoked first
console.log('before all') // 1st output

window.addEventListener('message', () => {
  console.log('listen message') // 8th output
})

window.onmessage = () => {
  console.log('onMessage') // 9th output
}

document.body.onclick = () => {
  console.log('onClick') // 3rd output
}

document.body.addEventListener('click', () => {
  console.log('listen click') // 4th output
})

Promise.resolve().then(() => {
  console.log('micro task run') // 6th output
})

setTimeout(() => {
  console.log('macro task run') // 7th output
}, 0)

console.log('before trigger event') // 2nd output

window.postMessage('')
document.body.click()

console.log('after trigger event') // 5th output