面试官:能简单说下你对JS执行机制的理解吗
我:阿巴阿八
然而真实的工作场景,一看控制台,不对啊,这和我预想的不一样啊,这代码有自己的想法,哪里出问题了,昂不想研究了,我换个法子实现吧...
今天我就要深度去挖掘导致代码不听话的原因,js执行机制。
JS执行机制有五大要点:
- 单线程
- 任务队列
- 事件和回调函数
- Event Loop
- 定时器
下面我们逐个拿下这些知识点。
1. JS为什么是单线程
不管是B站白嫖党的还是看书自学党,我们从最初接触到JS的时候都知道的一个结论:JS是一种轻量级的解释型的脚本语言,不同于其他语言的先编译后执行的规则,JS是在运行过程中逐行进行解释。这里的逐行就很有趣,仿佛就已经告诉我们它是单线程。
由此有了这些疑问“JS为什么是单线程?JS为什么不能是多线程,多线程效率不更高吗?”
首先回答第一个问题:JS为什么是单线程?
JS作为浏览器脚本语言,主要功能是操作DOM和用户互动,单就操作DOM这一个功能就注定了JS只能是单线程。也许这样说不太好理解,那解答第二个问题就更好理解了。
第二个问题:JS为什么不能是多线程,多线程效率不更高吗?
假设有两个线程A和B
线程A:我来了我来了,我的任务是创建一个DOM
线程B:我也来了我也来了,我的任务是删除一个DOM
浏览器:???
此时浏览器该解释哪一个线程?
此时要是有杠精就会说:“给其中一个线程设置优先级不就行了,先执行优先级高的那个”
“╰(艹皿艹 )那TM还不是单线程,还设置优先级,给卑微打工人留点头发吧”
虽然结果怎么着HTML5居然真的在Web Worker标准中提出允许创建多个线程(⊙x⊙;)
但是不方不方,创建的子线程还是要受主线程的控制,而且不能操作DOM,虚晃一枪问题不大。
所以JS为什么不能是多线程就和人不能在生物学角度同时拥有两个爸爸是一个道理。懂?
2. 任务队列
这里的任务队列是指主线程之外的任务,单线程中任务执行是需要排队的,而任务的执行时间又是不固定的;要是前面一个任务执行时间非常非常长长长长,后一个任务也只能干等着。
那是什么任务是需要这么长长长长时间呢,哦原来是闪电先生在输入啊 || 哦原来是闪电先生在输出啊。
这要是继续等下去,娃名字都起好了。
对此JS的设计者的解决方法是,挂起等待中的任务,先运行排在后面的任务,等闪电处理完有了结果,才回来把挂起的任务继续下去。
所以任务就被分为了两种,一种是同步任务,另一种是异步任务。
- 同步任务:在主线程上排队执行的任务,按照严格的次序依次执行
- 异步任务:不进入主线程、而进入“任务队列(task queue)”,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
具体的异步执行机制用文字解释如下:
- 所有同步任务都在主线程上,形成一个技术栈
- 主线程之外,还有一个任务队列,只要异步任务有了结果,就在任务队中放置一个事件
- 一旦所有同步任务执行完毕,系统会自动读取任务队列,看里面有哪些事件,对应的异步任务结束等待态,进入主线程,开始执行
- 主线程不断重复上面的第三步
只要主线程是空的就会去读取任务队列中的异步任务,整个过程是循环不断的,直到所有异步任务都执行完。
3. 事件和回调函数
OK,我们来到了第三个知识点事件和回调函数。
先解释这里的事件,回顾上一个知识点任务队列,我们知道是异步任务排队的地方;当IO设备(输入输出)完成了一个任务,就会在任务队列中添加一个事件,表示这个异步任务可以进入执行栈,然后主线程读取任务队列,执行里面的事件。
任务队列中的事件除了IO设备事件外,还有用户产生的事件如:鼠标点击、页面滚动...。这些用户事件只要指定了回调函数就会在发生时进入任务队列,等待主线程读取。
接下来解释回调函数(callback),回调函数就是那些被主线程挂起的代码,把任务的第二段单独写在一个函数里,等重新执行这个任务时,直接调用这个函数。所以异步任务必须指定回调函数,当主线程开始执行异步任务的事件时,就是执行对应的回调函数。
任务队列是先进先出的执行规则,排在前面的先被主线程读取。主线程的读取是自动的,只要执行栈一有空,任务队列的第一位事件就会自动进入主线程。
4. Event Loop
只有把前面的几个知识点吸收进去了才能更好的理解Event Loop。
提问
任务队列的事件是怎么来的?
主线程执行异步任务实际上执行的什么?
回答不上来就滚回去(这里的滚是指页面滚动)再看一遍,不要觉得我看了=我会了,检验学习成果的最好方法是输出,用自己的方式输出。
ok,回答上来的小伙伴继续。
上面说过主线程从任务队列中读取事件,整个过程是循环的,而这整个运行的机制就是Event Loop(事件循环)。
- 主线程运行时会产生堆(heap)和栈(stack),栈中的代码调用各种外部API(如数据请求)
- 外部API(异步任务)会在任务队列中注册各种事件(click、load、done)
- 栈中的同步代码执行完毕,就会去读取任务队列中那些事件对应的回调函数
以上几个步骤就是Event Loop的循环过程。
到这里还是很好理解的,只要同步任务执行结束了,栈一清空就会去查看任务队列里是不是有完成的异步操作,如果有执行排在第一的异步操作的回调函数,这个过程一直循环循环直到任务队列被清空。
5. 定时器
我们都知道JS有俩定时器setTimeout()和setInterval(),运行机制完全一样,不同在于一个是一次性的一个是无限次执行的。这次着重讨论setTimeout()。
setTimeout()接受两个参数,第一个是回调函数,第二个是延迟的毫秒数
console.log(1);
setTimeout(()=>{console.log(2);},0)
console.log(3);
//1,3,2
在这个setTimeout()里第二个参数传的是0,正常理解下就是不延迟,直接执行;那为什么结果还是和预想的不一样呢?这就是JS机制不一样的地方。
在JS中将所有定时都作为是异步任务,当定时器异步任务开始排队时,计时就已经开始了,当计时结束时,才向任务队列注册定时器的回调函数,至于什么时候执行嘛,这要看该回调函数排在队伍的哪里,如果前面还有其它事件就只能继续排着。
就算是setTimeout(fn,0)也只能是尽早安排执行,这个尽早就很微妙了,只有同步任务和任务队列之前所有的事件都处理完了才会执行。
所以这就是为什么有些情况下定时器没有在设定的毫秒结束后执行的原因,因为setTimeout()只是将事件插入任务队列,必须等到当前代码执行完,主线程才会去执行它的回调函数。
结语
以上就是JS执行机制的全部知识解析,这本来是很简单的知识,但我一直觉得这个很难,我这种级别的小辣鸡理解不了这么高深的知识。这篇文章写的断断续续,差不多用了10天,中间羊了7天没有打开这个文档,本来的12月更文挑战也就此以失败告终。但是还是觉得自己很棒又攻克了一个知识。
文章借鉴的是阮一峰老师的博客分享,老师写的简单易懂,只要静下心来甚至都不需要一个小时就能理解。很多东西表面看上去很难,当下定决心要攻克的时候适当的拆分,糅合,知识也是这样,经过一个这样的过程,这个知识基本就会在长时记忆里永久存在了。
今天是2022.12.29,我今天很健康我很棒!