브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

不言
풀어 주다: 2019-01-15 09:35:12
앞으로
2427명이 탐색했습니다.

이 글은 브라우저와 노드의 이벤트 루프(이벤트 루프)의 차이점을 요약한 것입니다. 필요한 친구들이 참고할 수 있기를 바랍니다.

이 기사에서는 JS의 비동기 구현 원리를 소개하고 이벤트 루프가 실제로 브라우저와 Node.js에서 다르다는 것을 이해합니다.

1. 스레드 및 프로세스

1. 개념

우리는 종종 JS가 단일 스레드에서 실행된다고 말합니다. 이는 프로세스에 단 하나의 메인 스레드만 있다는 의미입니다. 프로세스란 무엇입니까?

공식 성명은 다음과 같습니다. 프로세스는 CPU 리소스 할당의 가장 작은 단위입니다. 스레드는 CPU 스케줄링의 가장 작은 단위입니다. 이 두 문장은 이해하기 쉽지 않습니다. 먼저 그림을 살펴보겠습니다.

브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

  • 과정은 그림 속 공장과 같으며, 별도의 공장 자원을 사용합니다.

  • 스레드는 사진 속 노동자와 같습니다. 공장에서는 여러 노동자가 협력하여 일합니다. 즉, 프로세스는 하나 이상의 스레드로 구성되며 스레드는 프로세스 내에서 서로 다른 코드 실행 경로입니다

  • 공장의 공간은 작업자가 공유하며 이는 메모리 공간을 상징합니다. 프로세스가 공유되면 이러한 공유 메모리는 각 스레드에서 사용할 수 있습니다.

  • 여러 개의 공장이 독립적으로 존재합니다.

2. 다중 프로세스 및 다중 스레드

  • 다중 프로세스: 동일한 컴퓨터 시스템에서 동시에 두 개 이상의 프로세스가 실행될 수 있는 경우입니다. 여러 프로세스의 이점은 분명합니다. 예를 들어, 음악을 들으면서 편집기를 열고 코드를 입력할 수 있으며, 편집기와 음악 듣기 소프트웨어의 프로세스는 전혀 서로 간섭하지 않습니다.

  • 멀티 스레딩: 프로그램에는 여러 실행 스트림이 포함되어 있습니다. 즉, 여러 다른 스레드가 하나의 프로그램에서 동시에 실행되어 여러 작업을 수행할 수 있습니다. 즉, 단일 프로그램이 여러 병렬 실행 스레드를 생성하여 작업을 완료할 수 있음을 의미합니다. 해당 작업.

Chrome 브라우저를 예로 들어보겠습니다. 탭 페이지를 열면 실제로 프로세스가 생성됩니다. 렌더링 스레드, JS 엔진 스레드 등 여러 스레드가 있을 수 있습니다. HTTP 요청 스레드 등 요청을 시작하면 실제로 스레드가 생성됩니다. 요청이 끝나면 스레드가 삭제될 수 있습니다.

2. 브라우저 커널

간단히 말하면, 브라우저 커널은 페이지 콘텐츠를 얻고, 정보를 구성하고(CSS 적용), 시각적 이미지 결과의 최종 출력을 계산 및 결합하며, 흔히 렌더링 엔진이라고 합니다.

브라우저 커널은 커널의 제어에 따라 각 스레드가 서로 협력하여 동기화를 유지합니다. 브라우저는 일반적으로 다음과 같은 상주 스레드로 구성됩니다.

  • GUI 렌더링 스레드

  • JavaScript 엔진 스레드

  • 타이밍 트리거 스레드

  • 이벤트 트리거 스레드

  • 비동기 http 요청 스레드

1. 페이지 렌더링, HTML, CSS 구문 분석, DOM 트리 구축을 주로 담당합니다. , 레이아웃 및 도면이 기다립니다.

  • 이 스레드는 인터페이스를 다시 그려야 하거나 일부 작업으로 인해 리플로우가 발생할 때 실행됩니다.

  • 이 스레드는 JS 엔진 스레드와 상호 배타적입니다. JS 엔진 스레드가 실행되면 GUI 렌더링이 일시 중지됩니다. 작업 대기열이 유휴 상태이면 JS 엔진이 GUI 렌더링을 실행합니다.

  • 2.JS 엔진 스레드

물론 이 스레드는 주로 JavaScript 스크립트 처리 및 코드 실행을 담당합니다.

  • 또한 실행할 준비가 된 이벤트 실행을 주로 담당합니다. 즉, 타이머 카운트가 끝나거나 비동기 요청이 성공하여 올바르게 반환되면 차례로 작업 대기열에 들어가서 실행을 기다립니다. JS 엔진 스레드.

  • 물론 이 스레드는 GUI 렌더링 스레드와 상호 배타적입니다. JS 엔진 스레드가 JavaScript 스크립트를 너무 오랫동안 실행하면 페이지 렌더링이 차단됩니다.

  • 3. 타이머 트리거 스레드

setTimeout, setInterval과 같은 비동기 타이머와 같은 기능 실행을 담당하는 스레드입니다.

  • 메인 스레드가 코드를 순차적으로 실행할 때 타이머를 만나면 처리를 위해 타이머를 스레드에 넘겨줍니다. 계산이 완료되면 이벤트 트리거 스레드가 계산된 이벤트를 끝에 추가합니다. 작업 대기열을 삭제하고 JS 엔진 스레드 실행을 기다립니다.

  • 4. 이벤트 트리거 스레드

는 주로 실행을 위해 준비된 이벤트를 JS 엔진 스레드에 전달하는 일을 담당합니다.

  • 예를 들어 setTimeout 타이머 카운트가 끝나면 ajax 및 기타 비동기 요청이 성공하여 콜백 함수를 트리거하거나 사용자가 클릭 이벤트를 트리거하면 스레드는 보낼 준비가 된 이벤트를 마지막에 추가합니다. JS 엔진 스레드의 실행을 기다리는 작업 대기열.

  • 5. 비동기 http 요청 스레드

Promise, axios, ajax 등과 같은 비동기 요청과 같은 기능을 실행하는 스레드입니다.

  • 메인 스레드가 코드를 순차적으로 실행하고 비동기 요청을 만나면 함수가 처리를 위해 스레드로 넘겨집니다. 상태 코드 변경이 모니터링될 때 콜백 함수가 있으면 이벤트를 트리거하는 스레드가 작업 대기열 끝에 콜백 함수를 추가합니다. JS 엔진 스레드가 실행될 때까지 기다립니다.

  • 3. 브라우저의 이벤트 루프

    1. 마이크로 태스크와 매크로 태스크

    이벤트 루프에는 매크로(매크로 태스크) 큐와 마이크로(마이크로 태스크) 큐의 두 가지 유형이 있습니다. 여러 매크로 작업 대기열이 있을 수 있지만 마이크로 작업 대기열은 하나만 있습니다.

    • setTimeout, setInterval, setImmediate, 스크립트(전체 코드), I/O 작업, UI 렌더링 등과 같은 일반적인 매크로 작업

    • process.nextTick, new Promise().then(콜백), MutationObserver(html5 새 기능) 등과 같은 일반적인 마이크로 작업

    2. 이벤트 루프 프로세스 분석

    전체 이벤트 루프 프로세스는 다음 단계로 요약할 수 있습니다.

    브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

    • 처음에는 실행 스택이 비어 있다고 생각할 수 있습니다. 저장소로서의 스택 함수 호출의 스택 구조는 선입후출 원칙을 따릅니다. 마이크로 큐는 비어 있고, 매크로 큐에는 하나의 스크립트(전체 코드)만 있습니다.

    • 전역 컨텍스트(스크립트 태그)가 실행 스택으로 푸시되어 코드 실행을 동기화합니다. 실행 과정에서 동기 작업인지 비동기 작업인지 판단되며, 일부 인터페이스를 호출하면 새로운 매크로 작업과 마이크로 작업이 생성될 수 있으며 해당 작업 대기열에 푸시됩니다. 동기화 코드가 실행된 후 스크립트는 매크로 대기열에서 제거됩니다. 이 프로세스는 본질적으로 대기열에 있는 매크로 작업의 실행 및 대기열 제거 프로세스입니다.

    • 이전 단계에서는 매크로 작업을 대기열에서 제거했고, 이 단계에서는 마이크로 작업을 다루고 있습니다. 그러나 매크로 작업이 대기열에서 제거되면 작업이

      하나씩 실행되는 반면, 마이크로 작업이 대기열에서 제거되면 작업은 한 번에 한 팀으로 실행됩니다. 따라서 마이크로 큐를 처리할 때 큐에 있는 작업을 하나씩 실행하고 큐가 지워질 때까지 큐에서 제거합니다.

    • 렌더링 작업 수행 및 인터페이스 업데이트

    • 웹 작업자 작업이 있는지 확인하고, 있으면 처리합니다.

    • 위 프로세스는 두 대기열이 모두 지워질 때까지 루프에서 반복됩니다

    요약하자면, 모든 주기는 다음과 같은 프로세스입니다.

    브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

    매크로 작업이 실행되면 마이크로 작업 대기열이 있는지 확인합니다. 있는 경우 마이크로태스크 대기열의 모든 작업이 먼저 실행됩니다. 그렇지 않은 경우 매크로태스크 실행 중에 마이크로태스크가 발견되면 마이크로태스크 대기열에 추가됩니다. 차례로. 스택이 비워지면 마이크로태스크 대기열의 작업을 다시 읽습니다.

    다음으로 위 과정을 소개하는 예를 살펴보겠습니다.

    Promise.resolve().then(()=>{
      console.log('Promise1')
      setTimeout(()=>{
        console.log('setTimeout2')
      },0)
    })
    setTimeout(()=>{
      console.log('setTimeout1')
      Promise.resolve().then(()=>{
        console.log('Promise2')
      })
    },0)
    로그인 후 복사
    최종 출력 결과는 Promise1, setTimeout1, Promise2, setTimeout2

    • 스택의 동기화 작업 실행 후(이것은 매크로입니다) 위의 질문에 마이크로태스크 대기열이 있는지 확인한 다음(단 한 개만 있음) 마이크로태스크 대기열의 모든 작업을 실행하여 동시에 Promise1을 출력합니다. 매크로 작업 setTimeout2가 생성됩니다

    • 그런 다음 매크로 작업 대기열과 매크로 작업 setTimeout1을 확인합니다. setTimeout2 전에 먼저 매크로 작업 setTimeout1을 실행하고 setTimeout1을 출력합니다

    • 매크로 작업 setTimeout1을 실행할 때 마이크로 작업 Promise2가 생성되어 다음 위치에 배치됩니다. 그런 다음 먼저 마이크로태스크 대기열의 모든 작업을 지우고 Promise2를 출력합니다.

    • 마이크로태스크 대기열의 모든 작업을 지운 후 매크로태스크 대기열로 이동하여 이번에는 setTimeout2

      를 실행합니다.
    4. Node의 이벤트 루프

    1. Node 소개

    Node의 이벤트 루프는 브라우저의 이벤트 루프와 완전히 다릅니다. Node.js는 V8을 js의 구문 분석 엔진으로 사용하고 I/O 처리를 위해 자체적으로 설계된 libuv를 사용합니다. libuv는 다양한 운영 체제의 일부 기본 기능을 캡슐화하고 통합 API를 제공하는 이벤트 기반 크로스 플랫폼 추상화 계층입니다. 외부 세계에서는 이벤트 루프 메커니즘도 구현됩니다(아래에서 자세히 소개합니다).

    브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

    Node.js의 작동 메커니즘은 다음과 같습니다.

    • V8 엔진은 JavaScript 스크립트를 구문 분석합니다.

    • 파싱된 코드는 Node API를 호출합니다.

    • libuv 라이브러리는 Node API 실행을 담당합니다. 서로 다른 스레드에 서로 다른 작업을 할당하여 이벤트 루프를 형성하고, 작업의 실행 결과를 비동기 방식으로 V8 엔진에 반환합니다.

    • V8 引擎再将结果返回给用户。

    2. 六个阶段

    其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

    브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

    从上图中,大致看出 node 中的事件循环的顺序:

    外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...

    • timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调

    • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调

    • idle, prepare 阶段:仅 node 内部使用

    • poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里

    • check 阶段:执行 setImmediate() 的回调

    • close callbacks 阶段:执行 socket 的 close 事件回调

    注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)

    接下去我们详细介绍timerspollcheck这 3 个阶段,因为日常开发中的绝大部分异步任务都是在这 3 个阶段处理的。

    (1) timer

    timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。
    同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行

    (2) poll

    poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情

    • 回到 timer 阶段执行回调

    • 执行 I/O 回调

    并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情

    • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

    • 如果 poll 队列为空时,会有两件事发生

      • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调

      • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

    当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

    (3) check 阶段

    setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后。

    我们先来看个例子:

    console.log('start')
    setTimeout(() => {
      console.log('timer1')
      Promise.resolve().then(function() {
        console.log('promise1')
      })
    }, 0)
    setTimeout(() => {
      console.log('timer2')
      Promise.resolve().then(function() {
        console.log('promise2')
      })
    }, 0)
    Promise.resolve().then(function() {
      console.log('promise3')
    })
    console.log('end')
    //start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
    로그인 후 복사
    • 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出 promise3

    • 然后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于 Node 与浏览器的 Event Loop 差异,下文还会详细介绍)。

    3. 注意点

    (1) setTimeout 和 setImmediate

    二者非常相似,区别主要在于调用时机不同。

    • setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;

    • setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行

    setTimeout(function timeout () {
      console.log('timeout');
    },0);
    setImmediate(function immediate () {
      console.log('immediate');
    });
    로그인 후 복사
    • 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。

    • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
      进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调

    • 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了

    但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout

    const fs = require('fs')
    fs.readFile(__filename, () => {
        setTimeout(() => {
            console.log('timeout');
        }, 0)
        setImmediate(() => {
            console.log('immediate')
        })
    })
    // immediate
    // timeout
    로그인 후 복사

    在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

    (2) process.nextTick

    这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

    setTimeout(() => {
     console.log('timer1')
     Promise.resolve().then(function() {
       console.log('promise1')
     })
    }, 0)
    process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
       process.nextTick(() => {
         console.log('nextTick')
         process.nextTick(() => {
           console.log('nextTick')
         })
       })
     })
    })
    // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
    로그인 후 복사

    五、Node 与浏览器的 Event Loop 差异

    浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务

    브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

    接下我们通过一个例子来说明两者区别:

    setTimeout(()=>{
        console.log('timer1')
        Promise.resolve().then(function() {
            console.log('promise1')
        })
    }, 0)
    setTimeout(()=>{
        console.log('timer2')
        Promise.resolve().then(function() {
            console.log('promise2')
        })
    }, 0)
    로그인 후 복사

    浏览器端运行结果:timer1=>promise1=>timer2=>promise2

    浏览器端的处理过程如下:

    브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

    Node 端运行结果:timer1=>timer2=>promise1=>promise2

    • 全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;

    • 首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;

    • 至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2

    Node 端的处理过程如下:

    브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약

    六、总结

    浏览器和 Node 环境下,microtask 任务队列的执行时机不同

    • Node 端,microtask 在事件循环的各个阶段之间执行

    • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

    위 내용은 브라우저와 노드 이벤트 루프(이벤트 루프)의 차이점 요약의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    관련 라벨:
    원천:segmentfault.com
    본 웹사이트의 성명
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
    인기 튜토리얼
    더>
    최신 다운로드
    더>
    웹 효과
    웹사이트 소스 코드
    웹사이트 자료
    프론트엔드 템플릿
    회사 소개 부인 성명 Sitemap
    PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!