이 기사의 내용은 JS의 비동기 단일 스레드 분석(그림 및 텍스트)에 대한 것입니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.
일반 개발자(특히 병렬 컴퓨팅/멀티스레딩 배경 지식이 있는 개발자)에게 js의 비동기 처리는 정말 이상합니다. 결과적으로 이 이상한 현상은 js의 "단일 스레드" 기능으로 인해 발생합니다.
이 부분의 내용을 '먼저 정의하고 나중에 확장'하는 교과서적인 방법으로 설명하려고 한 적이 있는데 너무 고통스러웠습니다. 이 일의 뒤에 있는 세부 사항을 명확히 하고, 일반화하고, 더 높은 관점에서 문제를 보기 위해서는 실제로 많은 기본 지식이 필요하기 때문입니다. 내가 이 지식을 명확하게 설명하고 마치면 독자들에게 운영 체제나 컴퓨터 네트워크 등 최면 책의 여러 장을 읽도록 강요하는 것과 같아서 정말 지루하고 지루할 것입니다.
그리고 더 중요한 것은, 그 단계에 도달하면 독자의 에너지가 소진되어 원래 문제인 js의 비동기 처리가 이상한 이유에 관심을 가질 에너지가 없다는 것입니다.
그래서 반대로 하기로 했어요, 초보자처럼 아무것도 없이 시작하자,
먼저 "잘못된 개념"을 사용하세요# 🎜🎜# 시작하자 토론을 한 다음 코드를 사용하여 개념을 위반하는 장소를 찾습니다.
그런 다음 몇 가지 수정을 가하고, 몇 가지 예를 살펴보고, 아직 만족스럽지 않고 명확하지 않은 부분이 있는지 생각해보고 조정합니다. 이런 식으로 우리는 탐정처럼 완전히 정확하지 않은 가설에서 시작하여 끊임없이 증거를 찾고, 가설을 끊임없이 수정하며, 최종적인 완전한 진실에 도달할 때까지 한 단계씩 추적해 나가게 될 것입니다. 저는 이런 글쓰기 방식이 개인의 진정한 지식 추구 및 연구 과정에 더 부합하고, '탐구 문제'에 대해 더 많은 영감을 줄 수 있다고 생각합니다. 저는 이런 사고방식과 연구 개념이 일반적인 지식보다 더 중요하다고 생각합니다. 그것은 당신이 먹이를 기다리는 아기가 되도록 강요당하는 대신 독립적으로 먹이를 찾을 수 있는 지식의 사냥꾼이 될 수 있게 해줍니다. 좋아, js 코드로 탐험의 여정을 시작해 보자.console.log('No. 1'); setTimeout(function(){ console.log('setTimeout callback'); }, 5000); console.log('No. 2');출력 결과는 다음과 같습니다.
No. 1 No. 2 setTimeout callback이 코드에는 복잡한 것이 거의 없으며 모두 인쇄 문입니다. 유일한 특수 함수는
setTimeout
입니다. 대략적인 온라인 정보에 따르면 두 가지 매개변수를 허용합니다:
setTimeout
,根据粗略的网上资料显示,它接受两个参数:第一个参数是callback函数,就是让它执行完之后,回过头来调用的函数。
另一个是时间参数,用于指定多少微妙之后,执行callback函数。这里我们使用了5000微妙,也即是5秒钟。
另一个重点是,setTimeout
是一个异步函数,意思是我的主程序不必去等待setTimeout
执行完毕,将它的运行过程扔到别的地方执行,然后主程序继续往下走。也即是,主程序是一个步调、setTimeout
是另一个步调,即是“异步”的方式跑代码。
如果你有一些并行计算或者多线程编程的背景知识,那么上面的语句就再熟悉不过了。如果在多线程环境,无非是另起一根线程去运行打印语句console.log('setTimeout callback')
。然后主线程继续往下走,新线程去负责打印语句,清晰明了。
所以综合起来,这段代码的意思是,主线程执行到语句setTimeout
时,就把它交给“其它地方”,让这个“其它地方”等待5秒钟之后运行。而主线程继续往下走,去执行“No. 2”的打印。所以,由于其它部分要等待5秒钟之后才运行,而主线程立刻往下运行了“No. 2”的打印,最终的输出结果才会是先打印“No. 2”,再打印“setTimeout callback”。
嗯,so far so good。一切看来都比较美好。
如果我们对上述程序做一点变动呢?例如,我可不可以让“setTimeout callback”这个信息先被打印出来呢?因为在并行计算中,我们经常遇到的问题便是,由于你不知道多个线程之间谁执行得快、谁执行得慢,所以我们无法判定最终的语句执行顺序。这里我们让“setTimeout callback”停留了5秒钟,时间太长了,要不短一点?
console.log('No. 1'); setTimeout(function(){ console.log('setTimeout callback'); }, 1); console.log('No. 2');
我们将传递给setTimeout
매개변수는 콜백 함수로, 실행 후 콜백되는 함수입니다.
다른 하나는 콜백 함수가 몇 초 후에 실행될지 지정하는 데 사용되는 시간 매개변수입니다. 여기서는 5000마이크로초, 즉 5초를 사용합니다.
setTimeout
이 비동기 함수라는 점입니다. 즉, 기본 프로그램이 setTimeout을 기다릴 필요가 없습니다.
실행 후 실행 중인 프로세스를 다른 위치로 던져서 실행하면 주 프로그램이 계속됩니다. 즉, 기본 프로그램은 프로그램이고 setTimeout
은 코드를 실행하는 "비동기" 방식인 또 다른 프로그램입니다. #🎜🎜##🎜🎜#병렬 컴퓨팅이나 다중 스레드 프로그래밍에 대한 배경 지식이 있다면 위의 설명이 매우 익숙할 것입니다. 다중 스레드 환경에 있는 경우 인쇄 문 console.log('setTimeout callback')
을 실행하기 위해 새 스레드를 시작하는 것 이상입니다. 그런 다음 메인 스레드는 계속해서 다운되고 새 스레드는 명령문을 명확하고 명확하게 인쇄하는 역할을 담당합니다. #🎜🎜##🎜🎜#요약하자면, 이 코드의 의미는 메인 스레드가 setTimeout
문을 실행할 때 이를 "다른 장소"로 넘겨주고 이 "다른 장소"를 허용한다는 것입니다. 장소" 실행하기 전에 5초 동안 기다리십시오. "No. 2"의 인쇄를 수행하기 위해 메인 스레드가 계속해서 다운됩니다. 따라서 다른 부분은 5초 정도 기다려야 실행되고, 메인 스레드는 즉시 "No. 2"의 인쇄를 실행하기 때문에 최종 출력 결과는 "No. 2"를 먼저 인쇄한 후 "setTimeout"을 인쇄하게 됩니다. 콜백" ". #🎜🎜##🎜🎜#글쎄, 지금까지는 너무 좋아요. 모든 것이 더 좋아 보입니다. #🎜🎜##🎜🎜#위 절차를 일부 변경하면 어떻게 되나요? 예를 들어 "setTimeout 콜백" 메시지가 먼저 인쇄되도록 할 수 있나요? 왜냐하면 병렬 컴퓨팅에서 우리가 흔히 겪는 문제는 여러 스레드 중에서 누가 빠르게 실행하고 누가 느리게 실행하는지 알 수 없기 때문에 최종 명령문 실행 순서를 결정할 수 없다는 것입니다. 여기서는 "setTimeout 콜백"을 5초 동안 유지합니다. 시간이 너무 길어야 할까요? #🎜🎜#console.log('No. 1'); setTimeout(function(){ console.log('setTimeout callback'); }, 0); console.log('No. 2');#🎜🎜#
setTimeout
에 전달되는 매개변수를 1밀리초로 변경했습니다. 여러 번 실행해봐도 결과가 변하지 않은 것을 알 수 있나요? ! 좀 비정상인 것 같은데요, 좀 더 작게 만들어 보시는 건 어떨까요? 0으로 변경하시겠습니까? #🎜🎜#console.log('No. 1'); setTimeout(function(){ console.log('setTimeout callback'); }, 0); for (let i = 0; i #🎜🎜#여러번 실행해본 결과 여전히 변경할 수 없는 것을 발견했습니다. 사실 이건 좀 이상해요. 왜냐하면 일반적인 병렬 컴퓨팅과 멀티스레드 프로그래밍에서는 여러 번 실행을 하면 실제로 예측할 수 없는 다양한 결과를 볼 수 있기 때문입니다. 여기서는 동일한 실행 시퀀스 결과가 마술처럼 얻어집니다. 이것은 비정상입니다. #🎜🎜#<p>但我们还无法完全下一个肯定的结论,可不可能因为是<code>setTimeout</code>的启动时间太长,而导致“No. 2”这条语句先被执行呢?为了做进一步的验证,我们可以在“No. 2”这条打印语句之前,加上一个<code>for</code>循环,给<code>setTimeout</code>充分的时间去启动。</p><pre class="brush:php;toolbar:false">console.log('No. 1'); setTimeout(function(){ console.log('setTimeout callback'); }, 0); for (let i = 0; i <p>运行这段代码,我们发现,"No. 1"这条打印语句很快地显示到了浏览器命令行,等了一秒钟左右,接着输出了</p><pre class="brush:php;toolbar:false">No. 2 setTimeout callback
诶?!这不就更加奇怪了吗?!setTimeout
不是等待0秒钟后立刻运行吗,就算启动再慢,也不至于等待一秒钟之后,还是无法正常显示吧?况且,在加入这个for
循环之前,“setTimeout callback”这条输出不是立刻就显示了吗?
综合这些现象,我们有理由怀疑,似乎“setTimeout callback”一定是在“No. 2”后显示的,也即是:setTimeout
的callback函数,一定是在console.log('No. 2')
之后执行的。为了验证它,我们可以做一个危险一点的测试,将这个for
循环,更改为无限while
循环。
console.log('No. 1'); setTimeout(function(){ console.log('setTimeout callback'); }, 0); while {} // dangerouse testing console.log('No. 2');
如果setTimeout
的callback函数是按照自己的步调做的运行,那么它就有可能在某个时刻打印出“setTimeout callback”。而如果真的是按照我们猜测的那样,“setTimeout callback”必须排在“No. 2”之后,那么浏览器命令行就永远不会出现“setTimeout callback”。
运行后发现,在浏览器近乎要临近崩溃、达到内存溢出的情形下,“setTimeout callback”依旧没有打印出来。这也就证明了我们的猜测!
这里,我们第一次出现了理念和现实的矛盾。按照通常并行计算的理念,被扔到“其它地方”的setTimeout
callback函数,应该被同时运行。可事实却是,这个“其它地方”并没有和后一条打印“No. 2”的语句共同执行。这时候,我们就必须要回到基础,回到js这门语言底层的实现方式上去追查,以此来挖掘清楚这后面的猫腻。
js的特性之一是“单线程”,也即是从头到尾,js都在同一根线程下运行。或许这是一个值得调查深入的点。想来,如果是多线程,那么setTimeout
也就该按照我们原有的理念做执行了,但事实却不是。而这两者的不同,便在于单线程和多线程上。
找到了这个不同点,我们就可以更深入去思考一些细节。细想起来,所谓“异步”,就是要开辟某个“别的地方”,让“别的地方”和你的主运行路线一起运行。可是,如果现在是单线程,也就意味着计算资源有且只有一份,请问,你如何做到“同时运行”呢?
这就好比是,如果你去某个办事大厅,去缴纳水费、电费、天然气。那么,我们可以粗略地将它们分为水费柜台、电费柜台、天然气柜台。那么,如果我们依次地“先在水费柜台办理业务,等到水费的明细打印完毕、缴纳完费用后;再跑去电费柜台打印明细、加纳费用;再跑去天然气柜台打印明细、加纳费用”,这就是一个同步过程,必须等待上一个步骤做完后,才能做下一步。
而异步呢,就是说我们不必在某个环节浪费时间瞎等待。比如,我们可以在“打印水费明细”的空闲时间,跑到电费和天然气柜台去办理业务,将“电费明细、天然气明细的打印”这两个任务提前启动起来。再回过头去缴纳水费、缴纳电费、缴纳天然气费用。其实,这就是华罗庚推广优选法的时候举的例子,烧水、倒茶叶、泡茶,如何安排他们的顺序为高效。
显然,异步地去做任务更高效。但这要有一个前提,就是你做任务的资源,也即是干活的人或者机器,得有多份才行。同样按照上面的例子来展开讨论,虽然有水费、电费、天然气这三个柜台,可如果这三个柜台背后的办事人员其实只有一个呢?比如你启动了办理水费的业务,然后想要在办理水费业务的等待期,去电费柜台办理电费业务。表面上,你去电费柜台下了申请单,请求办理电费业务,可却发现根本没有办事员去接收你的这个业务!为何?因为这有且只有一个的办事员,还正在办理你的水费业务啊!这时候,你的这个所谓的“异步”,有何意义?!
所以从这个角度来看,当计算资源只有一份的时候,你做“异步”其实是没什么意义的。因为干活的资源只有一份,就算在表面做了名义上的“异步”,可最终就像上面的多柜台单一办事员那样,到了执行任务层面,还是会一个接一个地完成任务,这就没有意义了。
그렇다면 js의 특징은 "싱글 스레드" + "비동기"인데, 우리가 논의하고 있는 바로 "의미 없는" 상황이 아닌가요? ! 그렇다면 왜 의미도 없는 일을 단번에 해야 합니까?
글쎄... 상황이 점점 흥미로워지고 있습니다.
일반적으로 이벤트에 마술적이거나 이상한 것이 나타나면 기본적으로 특정 세부 사항을 간과했거나 특정 세부 사항에 대한 오해 또는 오해가 있기 때문입니다. 문제를 해결하기 위해서는 기존 자료를 끊임없이 검토해야 하며, 반복적인 점검을 통해 우리가 간과했던 몇 가지 비결을 발견할 수 있습니다.
JS 비동기에 대한 홍보영상을 살펴보겠습니다. 일반적으로 js 비동기의 필요성을 설명하기 위해 브라우저의 리소스 로딩과 페이지 렌더링 간의 모순이 언급됩니다.
렌더링은 대략 "그림"을 그리는 과정으로 이해될 수 있습니다. 예를 들어, 브라우저가 페이지에 버튼과 그림을 표시하려면 웹 페이지에 "그림"을 그리는 작업이 있어야 합니다. 또는 운영 체제가 그래픽 인터페이스 "데스크탑"을 모니터에 표시하려는 경우 해당 "그림"을 모니터에 그려야 합니다. 이렇게 "끌어내는" 과정을 정리하자면 "렌더링"이라고 합니다.
예를 들어 페이지에서 버튼을 클릭하면 브라우저는 백엔드 데이터베이스로 이동하여 데이터 보고서를 검색하고 웹 페이지에 숫자를 표시합니다. 그리고 js가 비동기식을 지원하지 않으면 전체 웹 페이지가 그대로 유지됩니다. 즉, 마우스가 버튼을 클릭하면 페이지가 후속 렌더링 작업을 완료할 수 없습니다. 백엔드가 데이터를 프런트엔드로 반환할 때까지 프로그램 흐름을 계속할 수 없습니다.
여기서 js의 "비동기"는 실제로 브라우저가 "로딩" 작업을 "다른 장소"에 할당하여 "로딩 프로세스"와 "렌더링 프로세스"가 동기적으로 진행될 수 있도록 허용하는 것입니다.
잠깐, 또 여기가 "다른 곳"인가요? ! !
제 생각엔 js가 싱글 쓰레드라고 하지 않았나요? 컴퓨팅 리소스가 하나밖에 없는 것 아닌가요? 어떻게 "동시에 로드하고 렌더링"할 수 있나요? ! 뭐야, 농담하는 거야? !
젠장, 이 문장 중 어느 문장이 사실인가요? ! js가 단일 스레드라는 것이 사실입니까? 아니면 브라우저가 동시에 "로딩과 렌더링"을 동시에 수행할 수 있다는 것이 사실입니까? !
이 의심을 해결하는 방법은 무엇입니까? ! 분명히, 우리는 브라우저가 어떻게 디자인되었는지 보려면 브라우저 내부로 깊숙이 들어가야 합니다.
검색 엔진에서 브라우저와 js에 대한 검색을 수행하면 몇 가지 기본 정보를 얻는 것이 어렵지 않습니다. JS가 브라우저의 전부는 아닙니다. 브라우저는 처리해야 할 일이 너무 많습니다. JS를 담당하는 것은 JS 엔진이라는 브라우저의 구성 요소일 뿐입니다. Chrome에서 사용되는 가장 유명한 엔진은 js의 구문 분석 및 실행을 담당하는 유명한 V8 엔진입니다.
한편, js를 사용하는 가장 큰 이유는 DOM 요소를 자유롭게 조작하고, Ajax 비동기 요청을 수행할 수 있으며, 처음에 제시한 /code> 예제와 같이 setTimeoutsetTimeout
做异步任务分配。这些都是js优秀特性。
可令人惊讶的事情来了,当我们去探索这个掌管js一切的V8引擎的时候,我们却发现,它并不提供DOM的操控、Ajax的执行以及setTimeout
setTimeout
기능을 제공하지 않는다는 사실을 발견했습니다. 위 그림은 Alexander Zlatkov의 것입니다. 구조는 다음과 같습니다.
DOM(문서)
분명히 js에서는 이러한 기능을 구현하지 않습니다. 통제하다 그것? 음 흥미롭네요~~어! "단일 스레드"가 아닌데 로딩 프로세스가 다른 곳으로 던져지는 것 아닌가요? ! js는 싱글 쓰레드, 즉 js 엔진에서는 싱글 쓰레드이고 컴퓨팅 리소스를 한 몫만 할당받을 수 있습니다. 그런데 데이터를 로드하는 Ajax 기능은 js 엔진에 없는 것 아닌가요? ! 잔디! 정말 늙은 여우야! "단일 스레드"와 "동시에 로드 및 렌더링"이라는 두 가지 진술 중 하나만 맞다고 생각했는데 둘 다 맞는 것으로 밝혀졌습니다! 왜? 왜냐면 js가 싱글 쓰레드라고 할 뿐, 브라우저 자체가 싱글 쓰레드라고는 말하지 않거든요! 따라서 렌더링 관련 js 부분과 데이터 로딩 Ajax 부분은 기본적으로 두 개의 모듈, 즉 두 개의 스레드로 구성되어 있기 때문에 동시에 수행할 수 있습니다! 물론 병렬로 수행될 수도 있습니다! 뭐야!
诶~等等,让我们再仔细看看上面这张图呢?!Ajax不在js引擎里,可是setTimeout
也不在js引擎里面啊!!如果Web APIs这部分是在不同于js引擎的另外一根线程里,它们不就可以实现真正意义上的并行吗?!那为何我们开头的打印信息“setTimeout callback”,无法按照并行的方式,优先于“No. 2”打印出来呢?
嗯......真是interesting......事情果然没有那么简单。
显然,我们需要考察更多的细节,特别是,每一条语句在上图中,是按照什么顺序被移动、被执行的。
谈到语句的执行顺序,我们需要再一次将关注点放回到js引擎上。再次回看上面这幅结构图,JS引擎包含了两部分:一个是 memory heap,另一个是call stack。前者关于内存分配,我们可以暂时放下。后面即是函数栈,嗯,它就是要进一步理解执行顺序的东西。
函数栈(call stack)为什么要叫做“栈(stack)”呢?为什么不是叫做函数队列或者别的神马?这其实可以从函数的执行顺序上做一个推断。
函数最开始被引进,其实就是为了代码复用和模块化。我们期望一段本该出现的代码,被单独提出来,然后只需要用一个函数调用,就可以将这段代码的执行内容给插入进来。
所以,如果当我们执行一段代码时,如果遇到了函数调用,我们会期望先去将函数里面的内容执行了,再跳出来回到主程序流,继续往下执行。
所以,如果把一个函数看作函数节点的话,整个执行流程其实是关于函数节点的“深度优先”遍历,也即是从主函数开始运行的函数调用,整个呈深度优先遍历的方式做调用。而结合算法和数据结构的知识,我们知道,要实现“深度遍历”,要么使用递归、要么使用stack这种数据结构。而后者,无疑更为经济使用。
所以咯,既然期望函数调用呈深度优先遍历,而深度优先遍历又需要stack这种数据结构做支持,所以维护这个函数调用的结构就当然呈现为stack的形式。所以叫做函数栈(stack)。
当然,如果再发散思考一下,操作系统的底层涌来维护函数调用的部分也叫做函数栈。那为何不用递归的方式来实现维护呢?其实很简单,计算机这么个啥都不懂的东西,如何知道递归和返回?它只不过会一往无前的一直执行命令而已。所以,在没有任何辅助结构的情况下,能够一往无前地执行的方式,只能是stack,而不是更为复杂的递归概念的实现。
另一方面,回到我们最开头的问题,矛盾其实是出现在setTimeout
的callback函数上。而上面的结构图里,还有一部分叫做“callback queue”。显然,这一部分也是我们需要了解的东西。
结合js的call stack和callback queue这两个关键词,我们不难搜索到一些资料,来展开讨论这两部分是如何同具体的语句执行相结合的。
先在整体上论述一下这个过程:
正常的语句执行,会一条接一条地压入call stack,执行,再根据执行的内容继续压入stack。
而如果遇到有Web APIs相关的语句,则会将相应的执行内容扔到Web APIs那边。
Web APIs这边,可以独立于js引擎,并行地分配给它的语句,如Ajax数据加载、setTimeout
的内容。
Web APIs这边的callback function,会在在执行完相关语句后,被扔进“callback queue”。
Event loop会不断地监测“call stack”和“callback queue”。当“call stack”为空的时候,event loop会将“callback queue”里的语句压入到stack中,继续做执行。
如此循环往复。
以上内容比较抽象,让我们用一个具体的例子来说明。这个例子同样来自于Alexander Zlatkov。使用它的原因很简单,因为Zlatkov在blog中使用的说明图,实在是相当清晰明了。而目前我没有多余的时间去使用PS绘制相应的结构图,就直接拿来当作例子说明了。
让我们考察下面的代码片段:
console.log('Hi'); setTimeout(function cb1() { console.log('cb1'); }, 5000); console.log('Bye');
哈哈,其实和我们使用的代码差不多,只是打印的内容不同。此时,在运行之前,整个底层的结构是这样的:
然后,让我们执行第一条语句console.log('Hi')
,也即是将它压入到call stack中:
그런 다음 js 엔진은 스택의 최상위 명령문을 실행합니다. 이에 따라 브라우저 콘솔은 "Hi" 메시지를 출력합니다:
이 문이 실행되기 때문에 스택에서도 사라집니다:
그런 다음 두 번째 문 setTimeout
을 푸시합니다. #🎜 🎜#setTimeout
:
执行setTimeout(function cb1() { console.log('cb1'); }, 5000);
:
注意,由于setTimout
部分并没有被包含在js引擎中,所以它就直接被扔给了Web APIs的Timeout
部分。这里,stack中的蓝色部分起到的作用,就是将相应的内容“timer、等候时间5秒、回调函数cb1”扔给Web APIs。然后这条语句就可以从stack中消失了:
继续压入下一条语句console.log('Bye')
:
注意,此时在Web APIs的部分,正在并行于js引擎执行相应的语句,即:等候5秒钟。Okay,timer继续它的等待,而stack这边已经有语句了,所以需要把它执行掉:
相应的浏览器控制台,就会显示出“Bye”的信息。而stack中运行后的语句,就该消失:
此时,stack已经为空。Event loop检测到stack为空,自然就想要将callback queue中的语句压入到stack中。可此时,callback queue中也为空,于是Event loop只好继续循环检测。
另一方面,Web APIs这边的timer,并行地在5秒钟后开始了它的执行——什么也不做。然后,将它相应的回调函数cb1()
,放到callback queue中:
Event loop由于一直在循环检测,此时,看到callback queue有了东西,就迅速将它从callback queue中取出,然后将其压入到stack里:
现在Stack里有了东西,就需要执行回调函数cb1()
。而cb1()
里面调用了 console.log('cb1')
这条语句,所以需要将它压入stack中:
stack继续执行,现在它的最上层是 console.log('cb1')
,所以需要先执行它。于是浏览器的控制它打印出相应的信息“cb1”:
将执行了的 console.log('cb1')
语句弹出栈:
继续执行cb1()
剩下的语句。此时,cb1()
setTimeout(function cb1() { console.log('cb1'); }, 5000 );
: #🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜#setTimout
부분은 js 엔진에 포함되지 않으므로 Timeout 웹 API의 일부입니다. 여기서 스택의 파란색 부분의 역할은 해당 콘텐츠 "타이머, 대기 시간 5초, 콜백 함수 cb1"을 Web API에 전달하는 것입니다. 그러면 다음 문이 스택에서 사라질 수 있습니다: #🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜#계속해서 다음 문을 푸시하세요console.log('Bye' ) 코드>:#🎜🎜##🎜🎜##🎜🎜#<img src="https://img.php.cn//upload/image/704/533/587/1536569531802611.png" title="1536569531802611. png" alt="자바스크립트의 비동기 단일 스레드 분석(그림 및 텍스트)">#🎜🎜##🎜🎜##🎜🎜#Web API 섹션에서 해당 문은 js 엔진과 병렬로 실행됩니다. 즉, 잠깐 5초. 좋아요, 타이머는 계속 기다리고 있으며 스택에 이미 명령문이 있으므로 실행해야 합니다: #🎜🎜##🎜🎜##🎜🎜#<img src="https://img.php.%20cn%20//upload/image/393/736/584/1536569546216881.png%20" title=" 1536569546216881.png " alt="자바스크립트의 비동기 단일 스레드 분석(그림 및 텍스트) ">#🎜🎜##🎜🎜##🎜🎜#현재 스택은 비어 있습니다. 이벤트 루프는 스택이 비어 있음을 감지하고 자연스럽게 콜백 대기열의 명령문을 스택에 푸시하려고 합니다. 하지만 이때 콜백 큐도 비어 있으므로 이벤트 루프는 루프 감지를 계속해야 합니다. #🎜🎜##🎜🎜#반면에 Web API 측의 타이머는 5초 후에 병렬로 실행을 시작하며 아무것도 하지 않습니다. 그런 다음 해당 콜백 함수 <code>cb1()
를 콜백 대기열에 넣습니다: #🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜# 이로 인한 이벤트 루프 있다 이때 콜백 큐에서 뭔가를 발견하면 재빨리 콜백 큐에서 꺼내 스택에 밀어 넣었습니다. #🎜🎜##🎜🎜##🎜🎜##🎜🎜## 🎜🎜##🎜🎜#이제 스택에 뭔가가 있으므로 콜백 함수 cb1()
을 실행해야 합니다. console.log('cb1')
문은 cb1()
에서 호출되므로 스택에 푸시해야 합니다. #🎜🎜##🎜🎜# # 🎜🎜##🎜🎜##🎜🎜##🎜🎜#stack은 계속 실행되며 이제 최상위 레벨은 console.log('cb1')
이므로 먼저 실행해야 합니다. 따라서 브라우저는 해당 정보 "cb1"을 인쇄하도록 제어합니다. #🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜#실행된 console.log( 'cb1 ')
문은 스택을 팝합니다: #🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜##🎜🎜#cb1()
의 나머지 문을 계속 실행합니다. 이때 cb1()
에는 실행해야 할 다른 명령문이 없습니다. 즉, 실행이 완료되었으므로 스택에서 팝합니다. #🎜🎜#
整个过程结束!如果从头到尾看一遍的话,就是下面这个gif图了:
相当清晰直观,对吧!
如果你想进一步地把玩js的语句和call stack、callback queue的关系,推荐Philip Roberts的一个GitHub的开源项目:Loupe,里面有他online版本供你做多种尝试。
有了这些知识,现在我们回过头去看开头的那段让人产生疑惑的代码:
console.log('No. 1'); setTimeout(function(){ console.log('setTimeout callback'); }, 0); console.log('No. 2');
按照上面的js处理语句的顺序,第一条语句console.log('No. 1')
会被压入stack中,然后被执行的是setTimout
。
根据我们上面的知识,它会被立刻扔进Web APIs中。可是,由于这个时候我们给它的等待时间是0,所以,它的callback函数console.log('setTimeout callback')
会立刻被扔进“Callback Queue”里面。所以,那个传说中的“其它地方”指的就是callback queue。
那么,我们能够期望这一条console.log('setTimeout callback')
先于“No. 2”被打印出来吗?
其实是不可能的!为什么?因为要让它被执行,首先它需要被压入到call stack中。可是,此时call stack还没有将程序的主分支上的语句执行完毕,即还有console.log('No. 2')
这条语句。所以,event loop在stack还未为空的情况下,是不可能把callback queue的语句压入stack的。所以,最后一条“setTimeout callback”的信息,一定是会排在“No. 2”这条信息后面被打印出来的!
这完全符合我们之前加入无限while
循环的结果。因为主分支一直被while
循环占有,所以stack就一直不为空,进而,callback queue里的打印“setTimeout callback”的语句就更不可能被压入stack中被执行。
探索到这里,似乎该解决的问题也都解决了,好像就可以万事大吉,直接封笔走人了。可事实却是,这才是我们真正的泛化讨论的开始!
做研究和探索,如果停留于此,就无异于小时候自己交作业给老师,目的仅仅是完成老师布置的任务。在这里,这个老师布置的任务就是文章开头所提出的让人疑惑的代码。可是,解决这段代码并不是我们的终极目的。我们需要泛化我们的所学和所知,从更深层次的角度去探索,为什么我们会疑惑,为什么一开始无法发现这些潜藏在表面之下不同。我们要继续去挖掘,我们到底在哪些最根本的问题上出现了误解和错误认识,从而导致我们一路如此辛苦,无法在开头看到事情的真相。
回顾我们的历程,一开始让我们载跟斗的,其实就是对“异步”和“多线程”的固定假设。多线程了,就是异步,而异步了,一定是多线程吗?我们潜意识里是很想做肯定回答的。这是因为如果异步了,但却是单线程,整个异步就没有意义了(回忆那个多柜台、单一办事员的例子)。可js却巧妙地运用了:使用异步单线程去分配任务,而让真正做数据加载的Ajax、或者时间等待的setTimeout的工作,扔给浏览器的其它线程去做。所以,本质上js虽然是单线程的,可在做实际工作的时候,却利用了浏览器自身的多线程。这就好比是,虽然是多柜台、单一办事员,可办事员将缴纳电费、水费的任务,外包给其它公司去做,这样,虽然自己仍然是一个办事员,但却由于有了外包服务的支持,依旧可以一起并行来做。
另一方面,js的异步、单线程的特性,逼迫我们去把并行计算中的“同步/异步、阻塞/非阻塞”等概念理得更清楚。
“同步”的英文是synchronize,但在中文的语境下,却很容易和“同时”挂钩。于是,在潜意识里有可能会有这样一种联想,“同步”就是“同时”,所以,一个同步(synchronize)的任务就被理解为“可以一边做A,一边做B”。而这个潜意识的印象,其实完全是错误的(一般做A一边做B,其实是“异步”+“并行”的情况)。
但在各类百科词典上,确实有用“同时”来作为对“同步”的解释。这是为什么呢?其实这是对”同步“用作”同时“的一个混淆理解。如果仔细考虑”同时“的意思,细分起来,其实是有两种理解:
같은 시간에 예를 들어 오전 9시에 A와 B를 동시에 하고 있습니다.
다른 하나는 동일한 시간 기준 시스템, 즉 소위 벽에 걸린 시계가 동일하다는 것입니다.
전자는 이해하기 쉬우나 여기서는 후자에 중점을 두고 설명하겠습니다. 예를 들어, 중국 본토에서 미국에 있는 동급생과 WeChat 음성 채팅을 열었습니다. 내 쪽은 22시, 그 쪽은 9시였습니다. 우리가 채팅할 때 우리는 동시에 있지만 동일한 시간 참조 시스템(벽에 걸린 시계)에 있지는 않습니다. 컴퓨터에서 논의되는 동기화는 실제로 후자의 "동일한 참조 시스템"을 논의하는 것입니다. 동기화는 참조 시스템을 함께 가져와 동일한 시스템에 배치하는 것을 의미합니다.
또 다른 예로, 우리가 인생에서 쉽게 말할 수 있는 것은 컴퓨터 동기화, 휴대폰 주소록 동기화, 사진 앨범 동기화, 무슨 뜻인가요? 이는 PC, 휴대폰, 서버 등 다양한 클라이언트의 콘텐츠를 일관되게 유지하기 위한 것입니다. 즉, 모든 사람이 일관된 참조 시스템에 배치됩니다. PC에는 사진 A가 있다고 하지 말고, 휴대폰에는 사진 A가 없고 사진 B가 있다고 합니다. 이때 PC에서 정보를 말하는 사람과 PC에서 정보를 말하는 사람이 있습니다. 전화도 같은 얘기를 하고 있어요. 그 이유는 모든 사람이 동일한 기준틀에 위치하지 않기 때문입니다.
그래서 동기화란 모든 사람이 벽에 걸린 시계를 일관되고 동일한 속도로 조정하는 것을 "동시에" 의미하며, 동시에 동시에 참조 시스템을 의미합니다. 같은 순간에 일이 나란히 일어나는 것보다. 당연히 비동기(비동기화)란 무엇입니까? 비동기란 모든 사람의 시간 참조 시스템이 다르다는 것을 의미합니다. 예를 들어 저는 중국 본토에 있고 우리는 미국에 있습니다. 이것은 비동기식이지 동일하지 않습니다. 주파수 대역에서.
사실 모든 독립적인 사람과 모든 독립적인 컴퓨팅 리소스는 고유한 참조 시스템을 나타냅니다. 작업을 다른 사람이나 다른 컴퓨팅 리소스에 배포하는 한 두 개의 참조 시스템이 나타납니다. 하나는 원래 주요 지점의 참조 시스템이고 다른 하나는 새 컴퓨팅 리소스의 참조 시스템입니다. 병렬 컴퓨팅에는 모든 계산 분기가 이 위치 노드에서 계산을 완료할 수 있도록 명령문 장벽을 사용하는 동기화 메커니즘이 있습니다. 왜 동기화 메커니즘이라고 합니까? 통합 참조 시스템에 대한 우리의 이해에 따르면, 다른 모든 계산 분기가 계산을 완료하도록 보장하고, 이는 또한 다른 분기가 사라지고 참조 시스템으로 기본 분기만 남도록 보장하는 것입니다. 그래서 모두가 같은 말을 하고, 같은 말을 오해 없이 할 수 있습니다.
반면에 js의 디자인을 더 깊이 이해하려면 싱글 코어 시분할 시스템 시대 등 컴퓨터 역사 초기로 돌아가야 한다고 생각합니다. 그 시대에는 운영체제의 하드웨어 한계가 브라우저의 js 엔진의 한계 못지않게 높았습니다. 동일한 제한 하에서 이전 운영 체제는 어떻게 극도로 제한된 컴퓨팅 리소스를 교묘하게 사용하여 전체 운영 체제에 부드러움, 매끄러움 및 강력한 기능의 환상을 제공할 수 있었습니까? 나는 이러한 js 디자인이 운영 체제의 초기 디자인과 밀접한 관련이 있음에 틀림없다고 생각합니다. 따라서 이 수준에서는 다시 운영 체제와 같은 기본으로 돌아갑니다. 실제로 현대 기술을 제대로 이해할 수 있는지 여부는 디자인의 역사를 철저히 이해하고 있는지, 그리고 그 시대에 각계각층의 대가들이 교묘하게 산을 가로질러 길을 열고 강을 건너는 다리를 건설했는지를 이해하는지 여부에 달려 있습니다. 자원 고갈. 현대 컴퓨터 하드웨어 자원이 아무리 풍부하더라도 목표와 비즈니스 간의 일차적, 이차적 관계로 인해 분명히 제한될 것입니다. 제한 속에서 어떻게 춤을 추고 창조하는지는 역사 전반에 걸쳐 추적될 수 있는 공통적인 문제입니다.
관련 추천:
JavaScript 비동기 프로그래밍 기술에 대한 자세한 설명
위 내용은 자바스크립트의 비동기 단일 스레드 분석(그림 및 텍스트)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!