>웹 프론트엔드 >프런트엔드 Q&A >자바스크립트는 순차적으로 실행되나요?

자바스크립트는 순차적으로 실행되나요?

青灯夜游
青灯夜游원래의
2022-02-09 14:35:252577검색

javascript는 순차적으로 실행됩니다. JavaScript는 단일 스레드 언어이며 실행 순서는 하향식입니다. 즉, 코드 실행 중에 다른 코드가 실행되려면 현재 코드가 실행될 때까지 기다려야 합니다. 완벽한.

자바스크립트는 순차적으로 실행되나요?

이 튜토리얼의 운영 환경: Windows 7 시스템, JavaScript 버전 1.8.5, Dell G3 컴퓨터.

JavaScript의 순차 실행 실행 메커니즘

1. 단일 스레드 JavaScript

우리 모두 알고 있듯이 JavaScript는 단일 스레드 언어이며 실행 순서는 하향식입니다. JavaScript에는 멀티스레딩이라는 개념이 없으며 모든 프로그램은 단일 스레드에 의해 순차적으로 실행됩니다. 즉, 코드가 실행되는 동안 다른 코드가 실행되려면 현재 코드의 실행이 완료될 때까지 기다려야 합니다.

JavaScript는 하나의 스레드에서만 실행되지만 이는 JavaScript 엔진에 하나의 스레드만 있다는 의미는 아닙니다. 실제로 JavaScript 엔진에는 여러 스레드가 있습니다. 단일 스크립트는 하나의 스레드(메인 스레드라고 함)에서만 실행될 수 있으며 다른 스레드는 백그라운드에서 협력합니다. 그러면 js가 다중 스레드가 아닌 이유는 무엇입니까? 멀티스레딩이 더 효율적이지 않나요?

JavaScript가 다중 스레드가 아닌 단일 스레드인 이유는 웹 스크립팅 언어로서 JavaScript의 주요 목적은 사용자와 상호 작용하고 DOM을 작동하는 것입니다. JavaScript에 두 개의 스레드가 동시에 있으면 한 스레드는 웹 페이지의 DOM 노드에 콘텐츠를 추가하고 다른 스레드는 노드를 삭제하는 경우 브라우저는 어떤 스레드를 사용해야 합니까? 잠금 장치도 있나요?

JavaScript는 탄생부터 단일 스레드였습니다. 다중 스레드는 리소스를 공유해야 하고 서로의 실행 결과를 수정할 수 있기 때문에 브라우저를 너무 복잡하게 만들고 싶지 않기 때문입니다. 너무 복잡해요.

그래서 복잡성을 피하기 위해 JavaScript는 처음부터 단일 스레드였습니다.

이것은 언어의 핵심 기능이 되었으며 앞으로도 변하지 않을 것입니다.

이 모드의 장점은 구현이 상대적으로 간단하고 실행 환경이 상대적으로 단순하다는 것입니다. 단점은 하나의 작업에 시간이 오래 걸리면 후속 작업을 대기열에 추가해야 하므로 실행이 지연된다는 것입니다. 전체 프로그램의. 일반적인 브라우저 무응답(일시 중지)은 장시간 동안 실행되는 특정 JavaScript 코드 조각(예: 무한 루프)으로 인해 발생하는 경우가 많습니다. 이로 인해 전체 페이지가 이 위치에 멈춰 다른 작업을 수행할 수 없게 됩니다.

JavaScript 언어 자체는 느리지 않습니다. 느린 것은 Ajax 요청이 결과를 반환할 때까지 기다리는 등 외부 데이터를 읽고 쓰는 것입니다. 이때 상대방 서버가 오랫동안 응답하지 않거나 네트워크가 원활하지 않으면 스크립트가 오랫동안 정체되는 원인이 됩니다.

계산량이 많아 대기열이 발생하고 CPU가 너무 바쁘다면 잊어버리세요. 하지만 IO 작업(입력 및 출력)이 매우 느리기 때문에(예: 데이터를 읽는 Ajax 작업과 같이) CPU가 유휴 상태인 경우가 많습니다. 네트워크) 진행하기 전에 결과가 나올 때까지 기다리지 마십시오.

JavaScript 언어 설계자는 이때 CPU가 IO 작업을 완전히 무시하고 대기 작업을 일시 중지하며 나중 작업을 먼저 실행할 수 있다는 것을 깨달았습니다. IO 작업이 결과를 반환할 때까지 기다린 다음 돌아가서 일시 중지된 작업을 계속 실행합니다. 이 메커니즘은 JavaScript에서 내부적으로 사용되는 "이벤트 루프" 메커니즘(이벤트 루프)입니다.

단일 스레드 모델은 JavaScript에 큰 제한을 두지만, 다른 언어에는 없는 장점도 제공합니다. 잘 사용하면 자바스크립트 프로그램이 막히지 않기 때문에 Node는 아주 적은 리소스로 대규모 트래픽 액세스를 처리할 수 있습니다.

멀티 코어 CPU의 컴퓨팅 성능을 활용하기 위해 HTML5는 JavaScript 스크립트가 여러 스레드를 생성할 수 있도록 허용하지만 하위 스레드는 기본 스레드에 의해 완전히 제어되며 DOM을 작동해서는 안 되는 Web Worker 표준을 제안합니다. . 따라서 이 새로운 표준은 JavaScript의 단일 스레드 특성을 변경하지 않습니다.

이제 코드를 살펴보겠습니다

function fn(){
    console.log('start');
    setTimeout(()=>{
        console.log('setTimeout');
    },0);
    console.log('end');
}

fn() // 输出 start end setTimeout

자바스크립트의 실행 순서는 하향식인데 왜 위 코드의 실행 순서가 어긋나는 걸까요?

JavaScript

에는 동기식 및 비동기식 2개의 실행 모드가 있으므로 동기식 및 비동기식 JavaScript

프로그램의 모든 작업은 두 가지 범주로 나눌 수 있습니다. 동기식 작업( 동기)

비동기 작업(비동기)

. 동기식과 비동기식이란 무엇인가요? 동기화와 비동기성은 어떻게 구현되나요? 동기 작업
: 엔진에 의해 일시 중단되지 않고 메인 스레드에서 실행 대기 중인 작업입니다. 다음 작업은 이전 작업이 실행된 후에만 실행할 수 있습니다.

异步任务: 是那些被引擎放在一边,不进入主线程、而进入 任务队列 的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。(通俗讲就是 只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。)

排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。

举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。

js中包含诸多创建异步的函数如:
seTimeout,setInterval,dom事件,ajax,Promise,process.nextTick等函数

3.任务队列和事件循环

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。
如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

자바스크립트는 순차적으로 실행되나요?

  • 因为单线程,所以代码自上而下执行,所有代码被放到执行栈中执行;

  • 遇到异步函数将回调函数添加到一个任务队列里面;

  • 执行栈中的代码执行完以后,会去循环任务队列里的函数;

  • 任务队列里的函数放到执行栈中执行;

  • 如此往复,称为事件循环;

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环。
这样分析,上面那一段代码就得到了合理的解释。
再来看一下这段代码:

function fn() {
    setTimeout(()=>{
        console.log('a');
    },0);
    new Promise((resolve)=>{
        console.log('b');
        resolve();
    }).then(()=>{
        console.log('c')
    });
}
fn() // b c a

4.Promise和async 立即执行

Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。
而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?

await等到之后做了什么?

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到微任务(microtask)中,然后就会跳出整个async函数来执行后面的代码。

不管await后面的代码是同步还是异步,await总是需要时间,从右向左执行,先执行右侧的代码,执行完后,发现有await关键字,于是让出线程,阻塞代码。

由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。
例如:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

等同于

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

5.宏任务和微任务

两个任务分别处于任务队列中的宏队列 和 微队列中;
宏任务队列微任务队列组成了任务队列;
任务队列将任务放入执行栈中执行

宏任务

宏队列,macrotask,也叫tasks。
异步任务的回调会依次进入macro task queue,等待后续被调用。

宏任务一般包括:

  1. 整体代码script
  2. setTimeout
  3. setInterval
  4. setImmediate (Node独有)
  5. requestAnimationFrame (浏览器独有)
  6. I/O
  7. UI rendering (浏览器独有)

微任务

微队列,microtask,也叫jobs。
异步任务的回调会依次进入micro task queue,等待后续被调用

微任务一般包括:

  • process.nextTick (Node独有)

  • Promise

  • Object.observe

  • MutationObserver



자바스크립트는 순차적으로 실행되나요?

1.执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等)。
2.全局Script代码执行完毕后,执行栈Stack会清空。
3.先从微任务队列中取出位于队首的回调任务,放入执行栈Stack中执行,执行完后微队列长度减1。
4.继续循环取出位于微队列的任务,放入执行栈Stack中执行,以此类推,直到直到把微任务执行完毕。注意,如果在执行微任务的过程中,又产生了微任务,那么会加入到微队列的末尾,也会在这个周期被调用执行。
5.微队列中的所有微任务都执行完毕,此时微队列为空队列,执行栈Stack也为空。
6.取出宏队列中的任务,放入执行栈Stack中执行。
7.执行完毕后,执行栈Stack为空。
8.重复第3-7个步骤。

以上是完成的****事件循环

6.面试题测试

现在我们再来分析一下最开始的那个面试题

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');


/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

我们分析一下整个过程:

1.首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。

2.然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中。

3.script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。
遇到了await时,会将await后面的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log(‘async1 end’)加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。

4.script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。

5.script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。
根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 end和promise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。

6.第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。

再来一个稍微复杂点的代码

function fn(){
    console.log(1);
    
    setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3);
        });
    },0);
    
    new Promise((resolve, reject) => {
        console.log(4);
        resolve(5);
    }).then(data => {
        console.log(data);
    });
    
    setTimeout(() => {
        console.log(6);
    },0);
    
    console.log(7);
}
fn(); //

流程重现

1.执行函数同步语句

  • step1
console.log(1);

执行栈: [ console ]
宏任务: []
微任务: []

打印结果:
1

  • step2
setTimeout(() => {
    // 这个回调函数叫做callback1,setTimeout属于宏任务,所以放到宏队列中
    console.log(2);
    Promise.resolve().then(() => {
        console.log(3)
    });
});

执行栈: [ setTimeout ]
宏任务: [ callback1 ]
微任务: []

打印结果:
1

  • step3
new Promise((resolve, reject) => {
    // 注意,这里是同步执行的
    console.log(4);
    resolve(5)
}).then((data) => {
    // 这个回调函数叫做callback2,promise属于微任务,所以放到微队列中
    console.log(data);
});

执行栈: [ promise ]
宏任务: [ callback1 ]
微任务: [ callback2 ]

打印结果:
1
4

  • step4
setTimeout(() => {
    // 这个回调函数叫做callback3,setTimeout属于宏任务,所以放到宏队列中
    console.log(6);
})

执行栈: [ setTimeout ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]

打印结果:
1
4

  • step5
console.log(7)

执行栈: [ console ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]

打印结果:
1
4
7

2.同步语句执行完毕,从微队列中依次取出任务执行,直到微队列为空

  • step6
console.log(data)       // 这里data是Promise的成功参数为5

执行栈: [ callback2 ]
宏任务: [ callback1 , callback3 ]
微任务: []

打印结果:
1
4
7
5

3.这里微队列中只有一个任务,执行完后开始从宏队列中取任务执行

  • step7
console.log(2);

执行栈: [ callback1 ]
宏任务: [ callback3 ]
微任务: []

打印结果:
1
4
7
5
2

但是执行callback1的时候遇到另一个Promise,Promise异步执行完毕以后在微队列中又注册了一个callback4函数

  • step8
Promise.resolve().then(() => {
    // 这个回调函数叫做callback4,promise属于微任务,所以放到微队列中
    console.log(3);
});

执行栈: [ Promise ]
宏任务: [ callback3 ]
微任务: [ callback4 ]

打印结果:
1
4
7
5
2

4.取出一个宏任务macrotask执行完毕,然后再去微任务队列microtask queue中依次取出执行

  • step9
console.log(3)

执行栈: [ callback4 ]
宏任务: [ callback3 ]
微任务: []

打印结果:
1
4
7
5
2
3

5.微队列全部执行完,再去宏队列**中取第一个任务执行

  • step10
console.log(6)

执行栈: [ callback3 ]
宏任务: []
微任务: []

打印结果:
1
4
7
5
2
3
6

6.以上全部执行完毕,执行栈宏队列,****微队列**均为空
执行栈: []
宏任务: []
微任务: []

打印结果:
1
4
7
5
2
3
6

总结

1、代码的检查装载阶段(预编译阶段),此阶段进行变量和函数的声明,但是不对变量进行赋值, 变量的默认值为undefined。
2、代码的执行阶段,此阶段对变量进行赋值和函数的声明。 所以:Js的变量提升和函数提升会影响JS的执行结果,ES6中的let定义的变量不会提升。
3、js的执行顺序,先同步后异步。
4、异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列。
5、调用Promise 中的resolve,reject属于微任务队列,setTimeout等属于宏任务队列 所以:
【同步>异步;微任务>宏任务】

【相关推荐:javascript学习教程

위 내용은 자바스크립트는 순차적으로 실행되나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.