ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript の実行コンテキストと実行メカニズムの詳細な分析

JavaScript の実行コンテキストと実行メカニズムの詳細な分析

青灯夜游
青灯夜游転載
2022-03-30 11:43:562421ブラウズ

この記事では、スレッドとプロセスについて紹介し、JavaScript の実行コンテキストと実行メカニズムを理解します。お役に立てば幸いです。

JavaScript の実行コンテキストと実行メカニズムの詳細な分析

js の実行コンテキスト、実行スタック、実行メカニズム (同期タスク、非同期タスク、マイクロタスク、マクロタスク、イベント ループ) について面接は頻繁に行われるテスト ポイントであり、質問されると混乱する友人もいるかもしれません。そのため、画面の前の皆さんのお役に立てればと思い、今日は著者がそれを要約します。 [関連する推奨事項: JavaScript 学習チュートリアル]

スレッドとプロセス

実行コンテキストと js での js 実行について説明します# メカニズムの前に、スレッドとプロセスについて話しましょう

スレッドとは

正式用語ではスレッドとは CPUスケジューリングの最小単位。

プロセスとは

公式用語ではプロセスCPU最小のリソース割り当てです。ユニット。

スレッドとプロセスの関係

スレッドは、プロセスユニットに基づいて実行されるプログラムです。一般的な説明 スレッドはプログラム内の実行フローであり、プロセスには1つ以上の スレッドを含めることができます。

プロセスには、シングルスレッドと呼ばれる実行フローが1つだけあります。つまり、プログラムが実行されるとき、取られるプログラムパスは連続した順序で配置されます。前のものも処理する必要があります。残りは実行されます。

プロセス

内の複数の実行ストリームはマルチスレッドと呼ばれ、複数の異なるスレッドを1つのプログラム内で同時に実行できます。つまり、1 つのプログラムで、それぞれのタスクを完了するために並行して実行される複数の スレッド を作成できます。 著者は以下に簡単な例を示します。たとえば、音楽を聴くために

qqMusic

を開いた場合、qqMusic はプロセスとして理解できます。 #qqMusic では、音楽を聴きながらダウンロードできます。これはマルチスレッドです。音楽を聴くことは 1 つのスレッドであり、ダウンロードは 1 つのスレッドです。 vscode を再度開いてコードを記述すると、別のプロセスになります。 プロセスは互いに独立していますが、一部のリソースは同じプロセス内のスレッド間で共有されます。

スレッドのライフ サイクル

スレッドのライフ サイクルは 5 つの段階を経ます。

新しい状態:
    new
  • キーワードと

    Thread クラスまたはそのサブクラスを使用してスレッド オブジェクトを作成すると、スレッド オブジェクトは次の状態になります。新しい状態。状態。プログラム start() がこのスレッドに到達するまで、この状態が維持されます。

    準備完了状態: スレッド オブジェクトが
  • start()
  • メソッドを呼び出すと、スレッドは準備完了状態になります。準備完了状態のスレッドは準備完了キュー内にあり、

    CPU の使用権を取得している限り、すぐに実行できます。

    実行状態: 準備完了状態のスレッドが
  • CPU
  • リソースを取得すると、

    run() を実行でき、スレッドは走行状態です。実行状態のスレッドは最も複雑で、ブロックされたり、準備完了になったり、停止したりする可能性があります。

    ブロッキング状態: スレッドが
  • sleep (スリープ)
  • suspend (サスペンド) wait (待機) ## を実行した場合# および他のメソッドでは、占有されていたリソースを失った後、スレッドは実行状態からブロッキング状態に入ります。スリープ時間が経過するか、デバイスのリソースが取得された後、準備完了状態に再度入ることができます。これは 3 つのタイプに分類できます。 待機ブロック: 実行状態のスレッドは

    wait()
      メソッドを実行して待機ブロック状態に入ります。
    • 同期ブロック: スレッドは

      synchronized
    • 同期ロックを取得できませんでした (同期ロックが他のスレッドによって占有されているため)。
    • その他のブロック:

      I/O
    • リクエストは、スレッドの
    • sleep()

      または join()## を呼び出すことによって行われました。 # 、スレッドはブロッキング状態になります。 sleep() ステータスがタイムアウトになるか、join() がスレッドの終了を待つかタイムアウトになるか、I/O が処理されると、スレッドは再実行されます。 -準備完了状態になります。

      停止状態: 実行中のスレッドがタスクを完了するか、その他の終了条件が発生すると、スレッドは終了状態に切り替わります。
  • js はシングルスレッドですか?それともマルチスレッドですか?

    JS はシングルスレッドです。 JS ブラウザ スクリプト言語としてのその主な目的は、ユーザーと対話し、DOM を操作することです。これにより、シングルスレッドのみが可能であることが決まります。そうでない場合は、非常に複雑な同期の問題が発生します。たとえば、JavaScript に同時に 2 つのスレッドがあるとします。1 つのスレッドは特定の DOM ノードにコンテンツを追加し、もう 1 つのスレッドはそのノードを削除します。ブラウザは許可として使用しますか?

    実行コンテキストと実行スタック

    実行コンテキストとは

    JS エンジンが実行可能コード フラグメント (通常は関数呼び出し段階) では、実行前の準備作業が最初に行われます。この "準備作業"" 実行コンテキスト ( EC と呼ばれます) と呼ばれます。 " または 実行環境 とも呼ばれます。

    実行コンテキストの分類

    javascript 実行コンテキストには 3 つのタイプがあります。

    グローバル実行コンテキスト
      これはデフォルトまたは最も基本的な実行コンテキストです。プログラム内に存在するグローバル コンテキストは 1 つだけであり、
    • javascript

      スクリプトのライフ サイクル全体の実行中に存在します。スタックの一番下はスタックのポップによって破壊されません。グローバル コンテキストはグローバル オブジェクト (ブラウザ環境を例にとると、このグローバル オブジェクトは window です) を生成し、this 値をこのグローバル オブジェクトにバインドします。

      関数実行コンテキスト
    • 関数が呼び出されるたびに、(関数が繰り返し呼び出されるかどうかに関係なく) 新しい関数実行コンテキストが作成されます。
    • Eval 関数の実行コンテキスト
    • eval

      関数内で実行されるコードにも独自の実行コンテキストがありますが、頻繁には使用されないため、 eval なので、ここでは分析は行われません。 #実行スタックとは何ですか?

    js は実行時に実行コンテキストを作成すると前述しましたが、実行コンテキストは保存する必要があります。では、実行コンテキストを保存するには何を使用するのでしょうか?スタック データ構造を使用する必要があります。

    スタックは先入れ後出しのデータ構造です。

    要約すると、

    コードの実行時に作成された実行コンテキストを保存するために使用される実行コンテキストは、実行スタック

    です。 JavaScript の実行コンテキストと実行メカニズムの詳細な分析

    js 実行プロセス

    コードを実行するとき、

    JS エンジンは最初に実行コンテキストを保存する実行スタックを作成します。

    その後、

    JS エンジンはグローバル実行コンテキストを作成し、実行スタックに push

    を実行します。このプロセス

    JS では、エンジンはこのコードのグローバル実行コンテキスト すべての変数がメモリを割り当て、初期値 (未定義) を割り当てます。作成が完了すると、JS エンジンは実行フェーズに入ります。このプロセスでは、JS エンジンはコードを 1 行ずつ実行します。つまり、メモリに割り当てられた変数に値 (実数値) を 1 つずつ割り当てます。 このコードに function への呼び出しがある場合、JS

    エンジンは関数実行コンテキストを作成し、実行に

    push を実行します。 stack の作成および実行プロセスは、グローバル実行コンテキストと同じです。 実行スタックが完了すると、実行コンテキストがスタックからポップされ、次の実行コンテキストに入ります。 著者は以下に例を示します。プログラムに次のコードがある場合

    console.log("Global Execution Context start");
    
    function first() {
      console.log("first function");
      second();
      console.log("Again first function");
    }
    
    function second() {
      console.log("second function");
    }
    
    first();
    console.log("Global Execution Context end");

    上の例を簡単に分析してみましょう

    最初のすべて 実行スタックが作成されます

    • 次に、グローバル コンテキストが作成され、実行コンテキスト

      push

      が実行スタックに追加されます
    • 実行の開始、出力 グローバル実行コンテキストの開始

    • first

      メソッドの検出、メソッドの実行、関数の作成実行コンテキストと
    • push
    • 実行スタックへ

      #first 実行コンテキストを実行し、

      first function
    • # を出力します。

      #秒メソッドに遭遇し、メソッドを実行し、関数実行コンテキストを作成して、実行スタックにプッシュ

      します
    • #execute実行コンテキスト、出力秒関数

    • 実行コンテキストが完了し、スタックからポップされました、次の実行コンテキストに入りますfirst実行コンテキスト

    • first実行コンテキストは実行を継続し、出力再び最初の関数

    • first実行コンテキストの実行が完了すると、スタックからポップされ、次の実行コンテキストに入ります。グローバル実行コンテキスト

    • グローバル実行コンテキストは実行を継続します。出力
    • グローバル実行コンテキストの終了

    • 画像を使用して要約します
    • JavaScript の実行コンテキストと実行メカニズムの詳細な分析

      好了。说完执行上下文和执行栈我们再来说说js的执行机制

      执行机制

      说到js的执行机制,我们就需要了解js中同步任务和异步任务、宏任务和微任务了。

      同步任务和异步任务

      js中,任务分为同步任务和异步任务,那什么是同步任务什么是异步任务呢?

      同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

      异步任务指的是,不进入主线程、而进入"任务队列"的任务(任务队列中的任务与主线程并列执行),只有当主线程空闲了并且"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。由于是队列存储所以满足先进先出规则。常见的异步任务有我们的setIntervalsetTimeoutpromise.then等。

      事件循环

      前面介绍了同步任务和异步任务,下面我们来说说事件循环。

      • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,只有前一个任务执行完毕,才能执行后一个任务。异步任务不进入主线程而是进入 Event Table 并注册函数。

      • 当指定的事情完成时,Event Table 会将这个函数移入 Event QueueEvent Queue是队列数据结构,所以满足先进先出规则。

      • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。

      上述过程会不断重复,也就是常说的 Event Loop(事件循环)

      我们用一张图来总结下

      JavaScript の実行コンテキストと実行メカニズムの詳細な分析

      下面笔者简单来介绍个例子

      function test1() {
        console.log("log1");
      
        setTimeout(() => {
          console.log("setTimeout 1000");
        }, 1000);
      
        setTimeout(() => {
          console.log("setTimeout 100");
        }, 100);
      
        console.log("log2");
      }
      
      test1(); // log1、log2、setTimeout 100、setTimeout 1000
      • 我们知道在js中会优先执行同步任务再执行异步任务,所以上面的例子会先输出log1、log2

      • 同步任务执行完后会执行异步任务,所以延迟100毫秒的回调函数会优先执行输出setTimeout 100

      • 延迟1000毫秒的回调函数会后执行输出setTimeout 1000

      上面的例子比较简单,相信只要你看懂了上面笔者说的同步异步任务做出来是没什么问题的。那下面笔者再举一个例子小伙伴们看看会输出啥呢?

      function test2() {
        console.log("log1");
      
        setTimeout(() => {
          console.log("setTimeout 1000");
        }, 1000);
      
        setTimeout(() => {
          console.log("setTimeout 100");
        }, 100);
      
        new Promise((resolve, reject) => {
          console.log("new promise");
          resolve();
        }).then(() => {
          console.log("promise.then");
        });
      
        console.log("log2");
      }
      
      test2();

      要解决上面的问题光知道同步和异步任务是不够的,我们还得知道宏任务和微任务。

      宏任务和微任务

      js中,任务被分为两种,一种叫宏任务MacroTask,一种叫微任务MicroTask

      常见的宏任务MacroTask

      • 主代码块

      • setTimeout()

      • setInterval()

      • setImmediate() - Node

      • requestAnimationFrame() - 浏览器

      常见的微任务MicroTask

      • Promise.then()

      • process.nextTick() - Node

      所以在上面的例子中就涉及到宏任务和微任务了,那宏任务微任务的执行顺序是怎么样的呢?

      • 首先,整体的 script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分,同步任务会直接进入主线程依次执行,异步任务会进入异步队列然后再分为宏任务和微任务。

      • 宏任务进入到 Event Table 中,并在里面注册回调函数,每当指定的事件完成时,Event Table 会将这个函数移到 Event Queue

      • 微任务也会进入到另一个 Event Table 中,并在里面注册回调函数,每当指定的事件完成时,Event Table 会将这个函数移到 Event Queue

      • 当主线程内的任务执行完毕,主线程为空时,会检查微任务的 Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务

      我们用一张图来总结下

      JavaScript の実行コンテキストと実行メカニズムの詳細な分析

      读懂了异步里面的宏任务和微任务上面的例子我们就可以轻易的得到答案了。

      • 我们知道在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执行到底是先宏任务再微任务还是先微任务再宏任务网上的文章各有说辞。笔者的理解是如果把整个js代码块当做宏任务的时候我们的js执行顺序是先宏任务后微任务的。

      正所谓百看不如一练,下面笔者举两个例子如果你都能做对那你算是掌握了js执行机制这一块的知识了。

      例子1

      function test3() {
        console.log(1);
      
        setTimeout(function () {
          console.log(2);
          new Promise(function (resolve) {
            console.log(3);
            resolve();
          }).then(function () {
            console.log(4);
          });
          console.log(5);
        }, 1000);
      
        new Promise(function (resolve) {
          console.log(6);
          resolve();
        }).then(function () {
          console.log(7);
          setTimeout(function () {
            console.log(8);
          });
        });
      
        setTimeout(function () {
          console.log(9);
          new Promise(function (resolve) {
            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稍作修改,引入asyncawait

      async function test4() {
        console.log(1);
      
        setTimeout(function () {
          console.log(2);
          new Promise(function (resolve) {
            console.log(3);
            resolve();
          }).then(function () {
            console.log(4);
          });
          console.log(5);
        }, 1000);
      
        new Promise(function (resolve) {
          console.log(6);
          resolve();
        }).then(function () {
          console.log(7);
          setTimeout(function () {
            console.log(8);
          });
        });
      
        const result = await async1();
        console.log(result);
      
        setTimeout(function () {
          console.log(9);
          new Promise(function (resolve) {
            console.log(10);
            resolve();
          }).then(function () {
            console.log(11);
          });
        }, 100);
      
        console.log(12);
      }
      
      async function async1() {
        console.log(13)
        return Promise.resolve("Promise.resolve");
      }
      
      test4();

      上面这里例子会输出什么呢?这里我们弄懂asyncawait题目就迎刃而解了。

      我们知道asyncawait其实是Promise的语法糖,这里我们只需要知道await后面就相当于Promise.then。所以上面的例子我们可以理解成如下代码

      function test4() {
        console.log(1);
      
        setTimeout(function () {
          console.log(2);
          new Promise(function (resolve) {
            console.log(3);
            resolve();
          }).then(function () {
            console.log(4);
          });
          console.log(5);
        }, 1000);
      
        new Promise(function (resolve) {
          console.log(6);
          resolve();
        }).then(function () {
          console.log(7);
          setTimeout(function () {
            console.log(8);
          });
        });
      
        new Promise(function (resolve) {
          console.log(13);
          return resolve("Promise.resolve");
        }).then((result) => {
          console.log(result);
      
          setTimeout(function () {
            console.log(9);
            new Promise(function (resolve) {
              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就一定会按我们的延迟时间执行吗,我觉得并不见得。我们设置的时间只是该回调函数可以被执行了,但是主线程有没有空还是另外一回事,我们可以举个简单的例子。

      function test5() {
        setTimeout(function () {
          console.log("setTimeout");
        }, 100);
      
        let i = 0;
        while (true) {
          i++;
        }
      }
      
      test5();

      上面的例子一定会在100毫秒后输出setTimeout吗,并不会,因为我们的主线程进入了死循环,并没有空去执行异步队列的任务。

      GUI渲染

      GUI渲染在这里说有些小伙伴可能不太理解,后面笔者会出关于浏览器的文章会再详细介绍,这里只是简单了解下即可。

      由于JS引擎线程GUI渲染线程是互斥的关系,浏览器为了能够使宏任务DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。

      所以宏任务、微任务、GUI渲染之间的关系如下

      宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...

      【相关视频教程推荐:web前端

以上がJavaScript の実行コンテキストと実行メカニズムの詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。