JavaScript 학습은 분산되고 복잡하기 때문에 무언가를 배우지만 느끼지 못하는 경우가 많습니다. .진행하자마자 배운 내용을 잊어버리게 됩니다. 나는 이 문제를 해결하기 위해 학습 과정에서 핵심 단서를 찾으려고 노력해 왔다. 이 단서를 따르는 한 나는 조금씩 발전할 수 있다.
이 단서를 중심으로 프론트엔드 기본 발전이 서서히 펼쳐지고 있는데 EventLoop 메커니즘(Event Loop)이 가장 단서의 중요한 지식 포인트. 그래서 저는 이벤트 루프 메커니즘에 대해 계속 깊이 연구하고 여러분과 공유하기 위해 이 글을 요약했습니다.
이벤트 루프 메커니즘 전체는 우리가 작성하는 JavaScript 코드의 실행 순서를 알려줍니다. 그러나 공부하는 과정에서 피상적인 설명만 하는 국내 블로그 글을 많이 발견했고, 많은 글을 읽고 나면 그림에 원을 그려서 이해한 느낌이 들지 않았습니다. . 하지만 중, 고위급 면접을 할 때 이벤트 루프 메커니즘은 항상 피할 수 없는 주제일 정도로 중요합니다. 특히 PromiseObject가 ES6에 공식적으로 추가된 이후에는 새로운 표준의 이벤트 루프 메커니즘을 이해하는 것이 더욱 중요해졌습니다. 이것은 매우 당황스럽습니다.
최근 이 문제의 중요성을 표현한 두 가지 인기 기사가 있습니다.
이 프론트엔드 면접이 문제를 일으키고 있습니다
지원자의 80%가 JS 면접 질문에 실패했습니다하지만 안타깝게도 사부님들은 이 지식 포인트가 매우 중요하다고 모두에게 말씀하셨지만 그렇지 않았습니다. 왜 이런 일이 일어났는지 모두에게 말하지 마세요. 그래서 면접을 하다가 그런 질문을 받으면 결과를 알더라도 면접관이 또 질문을 하면 우리는 여전히 혼란스러워요.
이벤트 루프 메커니즘을 배우기 전에 다음 개념을 이미 이해하고 있다고 가정합니다. 여전히 궁금한 점이 있으면 이전 기사로 돌아가서 읽어보세요.
실행 컨텍스트
Queue데이터 구조(queue)
Promise(사용자 정의 캡슐화를 사용하여 Promise의 자세한 사용법은 다음 글에서 요약하겠습니다)
chrome 브라우저의 새로운 표준의 이벤트 루프 메커니즘은 nodejs와 거의 동일하므로 여기에 통합되어 있습니다. nodejs를 함께 이해해 봅시다. . nodejs에는 있지만 브라우저에는 없는 몇 가지 API를 소개하겠습니다. 사용법을 꼭 알 필요는 없습니다. 예를 들어, process.nextTick, setImmediate
그럼 먼저 결론을 내리고 사례를 통해 이벤트를 자세히 설명하겠습니다. 및 그림입니다.
우리는 JavaScript의 주요 기능이 단일 스레드라는 것을 알고 있으며 이 스레드에는 고유한 이벤트 루프가 있습니다.
물론 새 표준의 웹 워커에는 멀티스레딩이 포함되어 있으므로 이에 대해서는 잘 모르므로 여기서는 다루지 않겠습니다.
JavaScript 코드를 실행하는 동안 함수 호출 스택을 사용하여 함수의 실행 순서를 결정하는 것 외에도 작업 대기열을 사용하여 다른 코드를 실행합니다. .
스레드에서 이벤트 루프는 고유하지만 여러 작업 대기열을 가질 수 있습니다.
작업 대기열은 매크로 작업(macro task)과 마이크로 작업(micro-task)으로 구분됩니다. 최신 표준에서는 각각 작업(Task), 작업(Job)이라고 합니다.
매크로 작업에는 다음이 포함될 수 있습니다: 스크립트(전체 코드), setTimeout, setInterval, setImmediate, I/O, UI rend어링.
마이크로 작업에는 process.nextTick, Promise, Object.observe(구식), MutationObserver(html5새 기능)가 포함될 수 있습니다. 🎜>
setTimeout/Promise 등을 작업 소스라고 합니다. 작업 대기열에 들어가는 것은 그들이 지정한 특정 실행 작업입니다.
// setTimeout中的回调函数才是进入任务队列的任务 setTimeout(function() { console.log('xxxx'); })
다른 작업 소스의 작업은 다른 작업 대기열에 들어갑니다. 그 중 setTimeout과 setInterval은 출처가 동일합니다.
이벤트 루프의 순서에 따라 JavaScript 코드의 실행 순서가 결정됩니다. 스크립트(전체 코드)에서 첫 번째 루프를 시작합니다. 그런 다음 전역 컨텍스트가 함수 호출 스택에 들어갑니다. 호출 스택이 지워질 때까지(전역 스택만 남음) 모든 마이크로 작업이 실행됩니다. 실행 가능한 모든 마이크로 작업이 실행된 후. 루프는 다시 매크로 작업부터 시작하여 실행할 작업 대기열 중 하나를 찾은 다음 모든 마이크로 작업을 실행하고 루프가 계속됩니다.
매크로 작업이든 마이크로 작업이든 각 작업의 실행은 함수 호출 스택의 도움으로 완료됩니다.
순수한 텍스트 표현은 사실 약간 건조하기 때문에 여기서는 이벤트 루프의 구체적인 순서를 점차적으로 이해하기 위해 두 가지 예를 사용합니다.
// demo01 出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。 // 为了方便理解,我以打印出来的字符作为当前的任务名称 setTimeout(function() { console.log('timeout1'); }) new Promise(function(resolve) { console.log('promise1'); for(var i = 0; i < 1000; i++) { i == 99 && resolve(); } console.log('promise2'); }).then(function() { console.log('then1'); }) console.log('global1');
먼저 이벤트 루프는 매크로 작업 대기열에서 시작됩니다. 이때 매크로 작업 대기열에는 스크립트(전체 코드) 작업이 하나만 있습니다. 각 작업의 실행 순서는 함수 호출 스택에 의해 결정됩니다. 작업 소스가 발생하면 해당 작업이 먼저 해당 대기열에 배포됩니다. 따라서 위 예제의 첫 번째 단계는 아래 그림에 나와 있습니다.
2단계: 스크립트 작업이 실행되면, 먼저 setTimeout을 만나고 setTimeout은 하나의 매크로 작업 소스이며 그 역할은 해당 대기열에 작업을 배포하는 것입니다.
setTimeout(function() { console.log('timeout1'); })
3단계: 스크립트 실행 중에 Promise 인스턴스가 발생합니다. Promise 생성자의 첫 번째 매개변수는 new일 때 실행되므로 다른 대기열에 들어가지 않고 현재 작업에서 직접 실행되며 후속 .then은 이를 Promise 대기열에 배포합니다. 마이크로 태스크.
따라서 생성자가 실행되면 내부의 매개변수가 함수 호출 스택에 들어가 실행됩니다. for 루프는 대기열에 들어가지 않으므로 코드가 순차적으로 실행되므로 여기서 promise1과 promise2가 순차적으로 출력됩니다.
에 들어가고 스크립트 작업이 계속 실행됩니다. 결국에는 global1이라는 한 문장만 출력됩니다. 전역 작업이 완료되었습니다.
4단계: 첫 번째 매크로태스크 스크립트가 실행된 후 실행 가능한 모든 마이크로태스크가 시작됩니다. 이때 마이크로태스크에는 Promise 큐에 단 하나의 태스크만 존재하므로 직접 실행이 가능하며 실행 결과는 then1로 출력된다. 물론 해당 실행도 함수 호출 스택에서 실행된다.
5단계: 모든 마이크로 테이스트가 실행되면 루프의 첫 번째 라운드가 종료됩니다. 이때 주기의 두 번째 라운드가 시작되어야 합니다. 두 번째 주기는 여전히 매크로 작업에서 시작됩니다.
이때 매크로 태스크 중 대기 중인 타임아웃1 태스크가 단 한 개뿐인 것을 확인했습니다. setTimeout 대기열에서 실행됩니다. 그러니 직접 실행해 보세요.
이때 매크로 태스크 큐와 마이크로 태스크 큐에는 태스크가 없으며, 그러면 코드가 더 이상 출력되지 않습니다.
그러면 위 예시의 결과는 명백합니다. 직접 해보고 체험해 볼 수 있습니다.
这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复制一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。
// demo02 console.log('golb1'); setTimeout(function() { console.log('timeout1'); process.nextTick(function() { console.log('timeout1_nextTick'); }) new Promise(function(resolve) { console.log('timeout1_promise'); resolve(); }).then(function() { console.log('timeout1_then') }) }) setImmediate(function() { console.log('immediate1'); process.nextTick(function() { console.log('immediate1_nextTick'); }) new Promise(function(resolve) { console.log('immediate1_promise'); resolve(); }).then(function() { console.log('immediate1_then') }) }) process.nextTick(function() { console.log('glob1_nextTick'); }) new Promise(function(resolve) { console.log('glob1_promise'); resolve(); }).then(function() { console.log('glob1_then') }) setTimeout(function() { console.log('timeout2'); process.nextTick(function() { console.log('timeout2_nextTick'); }) new Promise(function(resolve) { console.log('timeout2_promise'); resolve(); }).then(function() { console.log('timeout2_then') }) }) process.nextTick(function() { console.log('glob2_nextTick'); }) new Promise(function(resolve) { console.log('glob2_promise'); resolve(); }).then(function() { console.log('glob2_then') }) setImmediate(function() { console.log('immediate2'); process.nextTick(function() { console.log('immediate2_nextTick'); }) new Promise(function(resolve) { console.log('immediate2_promise'); resolve(); }).then(function() { console.log('immediate2_then') }) })
这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。
第一步:宏任务script首先执行。全局入栈。glob1输出。
第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。
setTimeout(function() { console.log('timeout1'); process.nextTick(function() { console.log('timeout1_nextTick'); }) new Promise(function(resolve) { console.log('timeout1_promise'); resolve(); }).then(function() { console.log('timeout1_then') }) })
第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。
setImmediate(function() { console.log('immediate1'); process.nextTick(function() { console.log('immediate1_nextTick'); }) new Promise(function(resolve) { console.log('immediate1_promise'); resolve(); }).then(function() { console.log('immediate1_then') }) })
第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。
process.nextTick(function() { console.log('glob1_nextTick'); })
第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。
new Promise(function(resolve) { console.log('glob1_promise'); resolve(); }).then(function() { console.log('glob1_then') })
第六步:执行遇到第二个setTimeout。
setTimeout(function() { console.log('timeout2'); process.nextTick(function() { console.log('timeout2_nextTick'); }) new Promise(function(resolve) { console.log('timeout2_promise'); resolve(); }).then(function() { console.log('timeout2_then') }) })
第七步:先后遇到nextTick与Promise
process.nextTick(function() { console.log('glob2_nextTick'); }) new Promise(function(resolve) { console.log('glob2_promise'); resolve(); }).then(function() { console.log('glob2_then') })
第八步:再次遇到setImmediate。
setImmediate(function() { console.log('immediate2'); process.nextTick(function() { console.log('immediate2_nextTick'); }) new Promise(function(resolve) { console.log('immediate2_promise'); resolve(); }).then(function() { console.log('immediate2_then') }) })
这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。
其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。
当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。
这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。
setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。
只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。
setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。
当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。
大家需要注意这里的循环结束的时间节点。
当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。
OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。
当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。
// 用数组模拟一个队列 var tasks = []; // 模拟一个事件分发器 var addFn1 = function(task) { tasks.push(task); } // 执行所有的任务 var flush = function() { tasks.map(function(task) { task(); }) } // 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中 setTimeout(function() { flush(); }) // 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法 var dispatch = function(name) { tasks.map(function(item) { if(item.name == name) { item.handler(); } }) } // 当然,我们把任务丢进去的时候,多保存一个name即可。 // 这时候,task的格式就如下 demoTask = { name: 'demo', handler: function() {} } // 于是,一个订阅-通知的设计模式就这样轻松的被实现了
这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级。
因此,在老的浏览器没有支持Promise的时候,就可以利用setTimeout等方法,来模拟实现Promise,具体如何做到的,下一篇文章我们慢慢分析。
위 내용은 프론트엔드 발전(12): 이벤트 루프 메커니즘에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!