js执行上下文和作用域,js代码执行机制
本文向您介绍了线程和进程,并理解了JavaScript中的执行上下文和机制。希望对你有帮助!
js中的执行上下文、执行栈、执行机制(同步任务、异步任务、微任务、宏任务、事件循环)是面试中的高频考点。有些朋友被问到可能会不知所措,所以我今天总结一下,希望能对屏幕前的你有所帮助。【相关推荐:javascript学习教程】
线程和进程
先说线程和进程,再说js中的执行上下文和js执行机制
:官方说法
什么是线程
,线程是CPU调度的最小单位。
:官方说法
什么是进程
,进程是CPU资源分配的最小单位。
线程和进程的关系
线程是基于进程的程序运行单元。通俗点解释线程就是程序中的一个执行流。一个进程可以有一个或多个线程。
一个进程中只有一个执行流称为单线程,即执行一个程序时,所走的程序路径是按连续顺序排列的,必须处理好前者才会执行后者。
一个进程中有多个执行流,称为多线程,即一个程序中可以同时运行多个不同的线程来执行不同的任务,即允许单个程序创建多个并行执行线程来完成各自的任务。
这里有一个简单的例子。比如我们打开qq音乐听歌,qq音乐可以理解为一个过程。在qq音乐里,我们可以边听歌边下载。这里是多线程。听歌是一根线,下载是一根线。如果我们再打开vscode写代码,那就是另一个过程了。
进程是相互独立的,但是一些资源在同一个进程中的线程之间共享。
线程的生命周期
线程的生命周期将经历五个阶段。
新状态:使用New关键字和Thread类或其子类创建线程对象后,线程对象将处于新状态。它保持这种状态,直到程序开始()这个线程。
就绪状态:当线程对象调用start()方法时,线程进入就绪状态。就绪线程在就绪队列中,只要获得CPU的使用权就可以立即运行。
运行状态:如果就绪状态的线程获取了CPU资源,就可以执行run(),然后线程就处于运行状态了。正在运行的线程是最复杂的,它可以被阻塞、就绪和死亡。
阻塞状态:如果一个线程执行了sleep、suspend、wait等方法,那么它在失去所占用的资源后,就会从运行状态进入阻塞状态。您可以在睡眠时间结束或获取设备资源后重新进入就绪状态。它可以分为三种类型:
等待阻塞:处于运行状态的线程执行wait()方法,使线程进入等待阻塞状态。
同步阻塞:线程无法获取同步的同步锁(因为同步锁被其他线程占用)。
其他:当通过调用线程的sleep()或join()发出I/O请求时,线程将进入阻塞状态。当sleep()状态超时时,join()等待线程终止或超时,或者I/O处理完成,线程再次进入就绪状态。
死状态:当一个正在运行的线程完成它的任务或者其他终止条件发生时,线程将切换到终止状态。
js是单线程还是多线程呢
JS是单线程。JS作为一种浏览器脚本语言,主要用于与用户交互和操作DOM。这就决定了它只能单线程,否则会带来复杂的同步问题。例如,假设JavaScript同时有两个线程,一个线程向DOM节点添加内容,另一个线程删除该节点。浏览器应该以哪个线程为标准?
执行上下文和执行栈
什么是执行上下文
JS引擎解析可执行代码片段时(通常是函数调用阶段),会做一些执行前的准备工作。这个“准备工作”叫"执行上下文(execution context 简称 EC)"或者执行环境。
执行上下文分类
JavaScript中有三种类型的执行上下文。它们是:
全局执行上下文这是默认或最基本的执行上下文。一个程序中只有一个全局上下文,在javascript脚本的整个生命周期中它都会存在于执行堆栈的底层,不会被堆栈弹出破坏。全局上下文会生成一个全局对象(以浏览器环境为例,这个全局对象就是window),将这个值绑定到这个全局对象上。
函数执行上下文每当调用一个函数时,都会创建一个新的函数执行上下文(不管这个函数是否被重复调用)。
Eval 函数执行上下文eval函数内部执行的代码也会有自己的执行上下文,但由于eval不常用,这里就不分析了。
什么是执行栈?
前面我们说过js在运行的时候会创建一个执行上下文,但是执行上下文是需要存储的,那么可以用什么来存储呢?你需要使用堆栈数据结构。
堆栈是一种先进后出的数据结构。
所以总结一下,用来存储代码运行时创建的执行上下文就是执行栈。
js执行流程
在执行一段代码时,JS engine会先创建一个执行栈来存储执行上下文。
然后,JS引擎将创建一个全局执行上下文,并将其推送到执行堆栈。在这个过程中,JS引擎会为这段代码中的所有变量分配内存,并赋一个初始值(未定义)。创建完成后,JS引擎将进入执行阶段。在这个过程中,JS引擎会逐行执行代码,也就是给之前已经分配了内存的变量赋值(实值)。
如果这段代码中有函数调用,JS引擎将创建一个函数执行上下文,并将其推入执行堆栈。创建和执行的过程与全局执行上下文的过程相同。
当一个执行栈结束时,执行上下文会从栈中弹出,然后进入下一个执行上下文。
让我给你举个例子,如果我们的程序中有下面的代码
console.log(“全局执行上下文开始”);
函数优先(){
console.log(“第一个函数”);
second();
console.log(又是第一个函数);
}
函数second() {
console.log(“第二个函数”);
}
first();
console.log(“全局执行上下文结束”);我们来简单分析一下上面的例子。
将首先创建一个执行堆栈。
然后,创建全局上下文,并将执行上下文推送到执行堆栈。
执行并输出全局执行上下文开始。
当遇到第一个方法时,执行它,创建一个函数执行上下文并将其推送到执行堆栈。
执行第一执行上下文并输出第一函数。
当遇到第二种方法时,执行它,创建一个函数执行上下文并将其推送到执行堆栈。
执行第二执行上下文并输出第二函数。
当第二个执行上下文结束时,它从堆栈中弹出,进入下一个执行上下文,即第一个执行上下文。
第一执行上下文继续执行并再次输出第一函数。
第一个执行上下文结束后,弹出堆栈,进入下一个执行上下文的全局执行上下文。
全局执行上下文继续执行,输出全局执行上下文结束。
我们用一张图总结一下。
好吧。在执行上下文和执行栈之后,我们来说说js的执行机制。
执行机制
说到js的执行机制,我们需要了解一下js中的同步任务和异步任务,宏任务和微任务。
同步任务和异步任务
在js中,任务分为同步任务和异步任务。那么什么是同步任务和异步任务呢?
同步任务是指在主线程上排队等待执行的任务,只有前一个任务完成后才能执行后一个任务。
异步任务是指进入‘任务队列’而不是主线程的任务(任务队列中的任务与主线程并排执行)。只有当主线程空闲且‘任务队列’通知主线程可以执行异步任务时,任务才会进入主线程执行。由于是队列存储所以满足先进先出规则。常见的异步任务有我们的setInterval、setTimeout、promise.then等。
事件循环
前面介绍了同步任务和异步任务。先说事件循环。
同步和异步任务分别进入不同的执行‘地方’,同步任务进入主线程。只有前一个任务完成后,才能执行后一个任务。异步任务不进入主线程,而是进入事件表和寄存器函数。
当指定的事情完成后,事件表会将该函数移入事件队列。事件队列是一种队列数据结构,所以它满足先入先出规则。
如果主线程中的任务执行后为空,它会去事件队列读取相应的函数,进入主线程执行。
以上过程会不断重复,也就是常说的Event Loop(事件循环)。
我们用一张图总结一下。
下面笔者简单介绍一个例子。
函数test1() {
console . log(“log 1”);
setTimeout(()={
console . log( setTimeout 1000 );
}, 1000);
setTimeout(()={
console . log( settime out 100 );
}, 100);
console . log( log2 );
}
test1();//log1,log2,setTimeout 100,setTimeout 1000我们知道在js中同步任务会在异步任务之前执行,所以上面的例子会先输出log1和log2。
同步任务执行后,异步任务将被执行,因此延迟100毫秒的回调函数将优先输出setTimeout 100。
延迟1000毫秒的回调函数将在会议结束后执行输出setTimeout 1000。
上面的例子比较简单。我相信只要你理解了上面作者说的同步和异步任务,就不会有问题。那我再给你举个例子。朋友们会输出什么?
函数test2() {
console . log(“log 1”);
setTimeout(()={
console . log( setTimeout 1000 );
}, 1000);
setTimeout(()={
console . log( settime out 100 );
}, 100);
新承诺((解决,拒绝)={
console.log(新承诺);
resolve();
}).然后(()={
console . log( promise . then );
});
console . log( log2 );
}
test2();要解决以上问题,光知道同步和异步任务是不够的,还要知道宏观任务和微观任务。
宏任务和微任务
在js中,任务分为两种,一种叫宏任务,一种叫微任务。
宏任务的常见宏任务有
主代码块
setTimeout()
setInterval()
setImmediate() -节点
RequestAnimationFrame()-浏览器
常见的微任务有
Promise.then()
process.nextTick() -节点
所以在上面的例子中,涉及到了宏观任务和微观任务。宏观任务和微观任务的执行顺序是怎样的?
首先,当整个脚本(作为第一个宏任务)开始执行时,所有的代码会被分成两部分:同步任务和异步任务。同步任务会直接进入主线程执行,异步任务会进入异步队列然后分为宏任务和微任务。
宏进入事件表,并在其中注册一个回调函数。每当指定的事件完成时,事件表就会将该函数移动到事件队列中。
微任务还将进入另一个事件表,其中将注册一个回调函数。每当指定的事件完成时,事件表就会将该函数移动到事件队列中。
当主线程中的任务完成且主线程为空时,将检查微任务的事件队列。如果有什么任务,都会执行。否则,将执行下一个宏任务。
我们用一张图总结一下。
看了异步的宏观任务和微观任务的例子,我们很容易得到答案。
我们知道在js中同步任务会在异步任务之前执行,所以上面的例子会先输出log1,new promise和log2。这里关注new promise里面是同步的。
主代码块作为宏任务执行后,这个宏任务生成的所有微任务都会被执行,所以会输出promise.then。
执行完所有的微任务后,会执行另一个宏任务,延迟100毫秒的回调函数会优先输出setTimeout 100。
此宏任务不生成微任务,因此没有要执行的微任务。
继续执行下一个宏任务,延迟1000毫秒的回调函数会更好地执行输出setTimeout 1000。
因此,在执行test2方法后,将依次输出log1,new promise,log2,promise.then,setTimeout 100和setTimeout 1000。
俗话说,眼见为实。这里有两个例子。如果你能把每件事都做对,你就掌握了js执行机制的知识。
示例1
函数test3() {
console . log(1);
setTimeout(function () {
console . log(2);
新承诺(功能(解决){
console . log(3);
resolve();
}).then(function () {
console . log(4);
});
console . log(5);
}, 1000);
新承诺(功能(解决){
console . log(6);
resolve();
}).then(function () {
console . log(7);
setTimeout(function () {
console . log(8);
});
});
setTimeout(function () {
console . log(9);
新承诺(功能(解决){
console . log(10);
resolve();
}).then(function () {
console . log(11);
});
}, 100);
console . log(12);
}
test3();下面具体分析一下。
首先将整个js代码块作为一个宏任务执行,依次输出1,6,12。
整个代码块的宏任务执行后,生成一个微任务和两个宏任务,所以宏任务队列有两个宏任务,微任务队列有一个微任务。
宏任务完成后,将执行该宏任务生成的所有微任务。因为只有一个微任务,所以会输出7。这个微任务生成了另一个宏任务,所以当前在宏任务队列中有三个宏任务。
在三个宏任务中,没有要先执行的延迟,所以输出是8。该宏任务没有微任务,因此没有要执行的微任务。继续执行下一个宏任务。
宏任务延迟100毫秒执行输出9和10,生成一个微任务,所以微任务队列当前有一个微任务。
宏任务执行后,宏任务生成的所有微任务都会被执行,所以微任务队列中的所有微任务都会被执行,输出11。
延迟1000毫秒执行的宏任务输出2,3,5,产生了一个微任务,所以微任务队列当前有一个微任务。
宏任务执行后,宏任务生成的所有微任务都会被执行,所以微任务队列中的所有微任务都会被执行,输出4。
所以上面的代码示例会依次输出1,6,12,7,8,9,10,11,2,3,5,4。朋友们做的对吗?
示例2
我们稍微修改了上面的例子1,引入了async和await。
异步函数test4() {
console . log(1);
setTimeout(function () {
console . log(2);
新承诺(功能(解决){
console . log(3);
resolve();
}).then(function () {
console . log(4);
});
console . log(5);
}, 1000);
新承诺(功能(解决){
console . log(6);
resolve();
}).then(function () {
console . log(7);
setTimeout(function () {
console . log(8);
});
});
const result=await async 1();
console.log(结果);
setTimeout(function () {
console . log(9);
新承诺(功能(解决){
console . log(10);
resolve();
}).then(function () {
console . log(11);
});
}, 100);
console . log(12);
}
异步函数async1() {
console.log(13)
return promise . resolve( promise . resolve );
}
test4();上面的例子会输出什么?我们可以解决异步问题,并在这里等待。
我们知道async和await实际上是承诺的语法糖。在这里,我们只需要知道await等价于Promise.then .所以上面的例子可以理解为下面的代码
函数test4() {
console . log(1);
setTimeout(function () {
console . log(2);
新承诺(功能(解决){
console . log(3);
resolve();
}).then(function () {
console . log(4);
});
console . log(5);
}, 1000);
新承诺(功能(解决){
console . log(6);
resolve();
}).then(function () {
console . log(7);
setTimeout(function () {
console . log(8);
});
});
新承诺(功能(解决){
console . log(13);
return resolve( promise . resolve );
}).然后((结果)={
console.log(结果);
setTimeout(function () {
console . log(9);
新承诺(功能(解决){
console . log(10);
resolve();
}).then(function () {
console . log(11);
});
}, 100);
console . log(12);
});
}
test4();看到上面的代码能轻易得出结果吗?
首先将整个js代码块作为一个宏任务执行,依次输出1,6,13。
整个代码块的宏任务执行后,生成两个微任务和一个宏任务,所以宏任务队列有一个宏任务,微任务队列有两个微任务。
宏任务完成后,将执行该宏任务生成的所有微任务。所以它会输出7,Promise.resolve,12。这个微任务又产生了两个宏任务,所以当前在宏任务队列中有三个宏任务。
在三个宏任务中,没有要先执行的延迟,所以输出是8。该宏任务没有微任务,因此没有要执行的微任务。继续执行下一个宏任务。
宏任务延迟100毫秒执行输出9和10,生成一个微任务,所以微任务队列当前有一个微任务。
宏任务执行后,宏任务生成的所有微任务都会被执行,所以微任务队列中的所有微任务都会被执行,输出11。
延迟1000毫秒执行的宏任务输出2,3,5,产生了一个微任务,所以微任务队列当前有一个微任务。
宏任务执行后,宏任务生成的所有微任务都会被执行,所以微任务队列中的所有微任务都会被执行,输出4。
所以上面的代码示例会依次输出1,6,13,7,Promise.resolve,12,8,9,10,11,2,3,5,4。朋友们做的对吗?
扩展
setTimeout(fn, 0)
关于setTimeout(fn),可能很多朋友还是不太理解。不耽误时间不是很明显吗?不是应该立即执行吗?
setTimeout(fn)可以理解为SetTimeout(fn,0),其实意思是一样的。
我们知道js分为同步任务和异步任务,setTimeout(fn)是异步任务,所以即使这里不设置延迟时间,它也会进入异步队列,只有在主线程空闲的时候才会执行。
在这里,我想再提一件事。你觉得在setTimeout设置的延迟时间之后,js一定会按照我们的延迟时间执行吗?我不这么认为。我们设置的时间是回调函数可以执行,但是主线程是否可用就是另外一回事了。我们可以举一个简单的例子。
函数test5() {
setTimeout(function () {
console . log( settime out );
}, 100);
设I=0;
while (true) {
我;
}
}
test 5();上面的例子会一直在100毫秒后输出setTimeout吗?不会,因为我们的主线程进入了无限循环,没有时间执行异步队列的任务。
GUI渲染
GUI渲染这里说的可能有些朋友看不懂。后面作者会给出一篇关于浏览器的文章,详细介绍。在这里,只是简单的理解。
因为JS引擎线程和GUI渲染线程是互斥的,为了让宏任务和DOM任务有序进行,浏览器会在一个宏任务的执行结果之后,下一个宏任务执行之前,启动GUI渲染线程渲染页面。
因此,宏任务、微任务和GUI渲染之间的关系如下
宏-微-任务-GUI渲染-宏-任务-.【相关视频教程推荐:web前端】以上是对JavaScript中执行上下文和执行机制的深入分析。更多请关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。