js执行上下文和作用域,js代码执行机制

  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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: