要理解这个过程,首先我们得了解以下几点
js的执行上下文
js执行代码的过程,实际上就是在运行“执行上下文”。通过以下方式可以创建“执行上下文”。
- 全局上下文是为运行代码主体而创建的执行上下文,也就是说它是为那些存在于 JavaScript 函数之外的任何代码而创建的。
- 每个函数会在执行的时候创建自己的执行上下文,这个上下文就是通常说的“本地上下文”。
- 使用
eval()
函数也会创建一个新的执行上下文。
其中每个上下文层级其实都是一个作用域层级,如以下代码
let outputElem = document.getElementById("output");
let userLanguages = {
"Mike": "en",
"Teresa": "es"
};
function greetUser(user) {
function localGreeting(user) {
let greeting;
let language = userLanguages[user];
switch(language) {
case "es":
greeting = `¡Hola, ${user}!`;
break;
case "en":
default:
greeting = `Hello, ${user}!`;
break;
}
return greeting;
}
outputElem.innerHTML += localGreeting(user) + "<br>\r";
}
greetUser("Mike");
greetUser("Teresa");
greetUser("Veronica");
其执行过程如下
- 当运行代码时,全局上下文就已经被创建好,并被推入到“执行上下文栈”中
- 当运行到greetUser("Mike")时,即greetUser函数被调用,会创建greetUser("Mike")函数的上下文,并推入到“执行上下文栈”中。
- 当运行到localGreeting(user)时,会创建localGreeting(user)函数的上下文,并推入到“执行上下文栈”中,
- localGreeting(user)执行完毕,其上下文会被销毁并退出“执行上下文栈”,程序会从“执行上下文栈”中找到上一个执行上下文并恢复执行,此处是getUser("Mike")。getUser("Mike")执行完毕,其上下文被销毁并退出执行栈。
- 开始执行getUser("Teresa"),重复2到4的过程。
- 开始执行greetUser("Veronica"),重复2到4的过程。
- 主程序退出,全局执行上下文从执行栈中弹出。此时栈中所有的上下文都已经弹出,程序执行完毕。
以这种方式执行代码的好处
- 每个程序和函数都可以拥有自己的变量和其他对象
- 每个上下文能额外的跟踪程序中下一行需要执行的代码以及一些对上下文非常重要的信息。
JS运行时
JS运行代码时,实际上维护了一组用于执行JS代码的代理。每个代理由主线程,执行上下文合集,执行上下文栈,一组可能创建用于执行 worker 的额外的线程集合,一个任务队列,一个微任务队列 组成。
其中除了主线程外,其他对于一个代理都是唯一的。(也就是说某些浏览器会在多个代理之间共享主线程)
事件循环(主线程与微/宏任务)
事件循环其实是一种编程模型,其主要功能是收集事件,对任务进行排队,以及在合适的时间执行回调。在js中表现为“主线程”,其收集宏任务(由浏览器发起的任务,通常包括网络请求,定时任务,用户事件等) 和微任务(由js引擎发起的任务,比如Promise回调,异步函数等)。 并将其加入宏任务队列和微任务队列。当执行栈中的同步代码执行完后,事件循环会优先查看微任务队列中是否有等待执行的任务,有的话将其依次执行,再看宏队列中是否有需要执行的任务,有的话将其依次执行,这个过程表现为一次事件循环。看以下例子:
console.log('sync start 1');
setTimeout(()=>{
console.log('This is a sync event in macro event 5')
setTimeout(()=>{
console.log('This is macro event in another macro event 8')
})
new Promise((resolve,reject)=>{
console.log('This is a sync event in another event loop 6')
resolve();
}).then(()=>{
console.log('This is a micro event in another event loop 7')
})
})
new Promise((resolve,rejuect)=>{
console.log('This is also a sync event 2')
resolve();
}).then(()=>{
console.log('This is a micro event 4')
})
console.log('end 3')
其执行步骤如下:
- 主线程先执行同步代码,“sync start 1”被输出
- 遇到setTimeout,主线程将其加入到“宏队列”中,此处称为
s1
,继续执行代码 - 遇到new Promise,执行同步代码,输出This is also a sync event 2,
then
回调函数属于微事件,将其加入到“微队列”中,此处称为then1
,继续执行代码 - 输入“end 3”
- 同步代码执行完毕,主线程优先搜索“微队列”,发现
then1
并执行,输入This is a micro event 4 - 接下来搜索“宏队列”,执行
s1
,此为一次事件循环 - 执行
s1
中的同步代码,输出This is a sync event in macro event 5 s1
中执行到setTimeout,此处称为s2
,将其加入到“宏队列”,继续执行代码- 遇到new Promise,执行同步代码,输出This is a sync event in another event loop 6,
then
回调属于微事件,加入到“微队列” 中。 s1
同步代码执行完毕,主线程搜索“微队列”,执行s2
,输出This is a micro event in another event loop 7- 主线程再搜索“宏队列”,输出This is macro event in another macro event 8
- 此致事件循环完毕