js 代码的运行机制
前言: 自己从一开始学习 javaScript 的时候,踩过很多很多坑,初学之路上也问过很多大佬许多为什么...现在回过头感叹,当时问的某些问题确实是有一丢丢幼稚。但是作为一个过来者,我深知这些问题的对于很多“后来者”来说,同样是非常宝贵的经验。所以全文会以“假如我是一个初学者,如果当初有人这样告诉我,那么我大概也能明白”。的角度去解释每一个细小的知识点,让你一步一步进阶。
作为一个淋过雨的人,想为后来者撑一把伞。
一. 什么是即时编译型语言
我相信每一个学习前端的都知道 JavaScript 是一门单线程解释型语言,或者更贴切的叫法为即时编译型语言。首先我们先不看单线程这个词。那么所谓的即时编译型语言这个名词到底是什么意思呢?
你可能没有深入了解过 java,go 这种编译型语言。js 和它们好像看起来就只差了两个字 ---“即时”。但是它们之间的行为差距是非常巨大的。别着急,我们一步一步理解。
我们首先看一下 script 这个单词的翻译。
这里我想引用单词 scripted 的翻译”照稿子念的“来进行接下来的讲解。我们先记住这个翻译。
我先问个问题:电影我们都很喜欢看🎬,对吧?那电影是需要“剧本”这个东西才可以进行的。剧本 包括了故事情节,每一个人的台词等非常重要的元素。
现在我们想象一个画面,一个已经完成的剧本摆在你面前。(就好比是一份需求文档)你需要用程序去把这个剧本里的画面去描绘出来。我来简单解释一下用即时编译型语言和编译型语言之间的差距。
OK,我们先拿编译型语言GO” 来编写看看是什么样子的。首先我们需要把这个剧本整个读一遍。是的,没错,你需要从第一页到最后一页通读整个剧本,看看里面有没有错别字。然后如果有错别字,你就需要先修改完错别字再进行拍摄。并且尴尬的是拍摄完以后,发现某些镜头不满意,那么我们就需要先修改剧本,然后把这部电影从头开始再进行拍摄。
真实的过程:我们100行 go 代码全部从头到尾走一遍,然后编译成计算机所认识的二进制代码,也就是常见的.exe 可执行文件,最后计算机整体执行整个文件。
那现在我们如果使用即时编译型语言JS来编写会是什么情况呢?我们拿到剧本,看到第一页。emmm,剧情大概描述了“是一个大雪纷飞的场景”,ok,看到这你就可以直接拿起摄像机开始拍了,后面的剧情你不需要知道,没错,你可以看到哪里拍到哪里。从而引出上面我们提到的我们就是照稿子念的。看到哪念到哪里。如果有台词错误,我们可以随之马上修改,因为我们是边读边写的。(Chrome v8引擎在拿到剧本后所做的事情)
真实的过程:我们100行 js 代码,V8 读到第一行,就把第一行先翻译成二进制代码执行了再说,后面的代码我压根不关心有没有错,先让计算机帮我们运行了再说。
聪明的你可能发现了,即时编译型语言 js 多了一个V8引擎翻译的环节。那么自然而然在执行速度上,就会略微逊色于编译型语言。
二. JS 设计为单线程的原因
我们先假设 JS 是多线程看看会造成什么后果。现在 JS 不再是从上到下一行一行执行了,那么它在执行的期间,难免就会遇到同一时间。某一个线程接收到信号,我需要修改 body 的背景颜色为红色。而另外一个线程接受到信号,需要把整个 body 的背景颜色修改为蓝色,那么我们到底听谁的?(注意你是多线程,必定会存在同一时间点两个线程做同一件事,所以这里我们不能按照哪个线程在后面就听谁的想法去思考)
所以 JS 是单线程的原因是因为它要干的事情决定的。并不是多线程可以充分利用 cpu 的高性能,我们就无脑选择多线程。
也正是 js 是单线程即时编译型语言的特点,我们在最开始学习 html 标签的时候,就会被老早的告知:“要把 <srcipt> 标签放在 body 后面。”
- 假设我们现在 <srcipt> 标签放在 <div> 标签前。那么假设我的 js 代码里存在 通过id 获取元素的这个方法。
document.getElementById("app")
那么就会造成,app 元素本身还没渲染出来,但是你就通过函数获取我。那么你肯定就只能拿到 undefined 了。
三. 单线程的后果
既然是 js 是单线程,那么我们就难免会遇到单线程难以解决的问题。假设我现在在进行一个很大的操作开销。
我们用 window.alert
,这个代码来“模拟”很大的开销的场景。(可能有些不太妥当)。我们都知道,这个方法会在用户加载页面的时候弹出一个对话框,来提醒某些事情。
上面是很简单的代码,我们看一下效果。
- 很明显可以看到,当我们进行“某些需要花时间”的操作的时候,由于我们的代码是从上而下执行的,势必就会造成我后面的代码需要花时间等你去执行完以后,我才可以去执行。
- 这种现象类比于到现实生活都是让人闹笑话的行为。我现在需要煮大米,我坐上电饭煲以后提示我30分钟后煮熟。那么在这三十分钟之内我什么其他事情都不能干了,我只能在电饭煲前面傻等着大米好了以后,我才能去买菜,炒菜。
- 所以既然 单线程 的本质我们无法更改,那么我们到底该如何解决这个问题呢?没错,JS 采用了事件循环(event loop) 的机制来解决这个办法。
四. 事件循环(event loop)
看过我上篇手把手教你实现防抖函数的同学大概会清楚一丢丢这个事情。在这里我再拓展讲解一下。
让我们再回到上面煮大米的现实案例。我现在被告知大米三十分钟以后做好,那么我在当下发现菜还没有买,我需要去买菜。我于是拿起手机定了个闹钟⏰,记下《30分钟以后,大米做好》这件事,然后我继续去买菜。当然做菜只能是在买菜之后才可以进行的事情,所以买菜和做菜之间的顺序还是不能打乱的。
那类比到我们的代码上。
我给做饭开启一个定时器,让他3秒后告诉我饭做好了,此时我的菜也买好炒好了。(我的页面也刷新好了,你随便怎么提醒我都不会影响我的页面了)
我们来试一下
可以很清楚的看到,我们的页面是先渲染出了样式,再弹出的提醒。这个“花费大时间的操作”并没有影响我们的使用体验。
这是我们的代码在执行时候的第一步。
编译器在执行到某一行的时候,会区分当前⬆是同步任务还是异步任务。如果是同步任务,那么推送到主线程上,如果是异步任务,那么推送到任务队列里去乖乖排队,你不能影响我的主线程。(类比上面就是 setTimeout 的回调函数window.alert
作为一个异步任务以后就被送到了任务队列里去等待执行,等待的时间就是 setTimeout 第二个参数所设置的时间。同理你也能反推到我们的 dom 加载被放到了主线程,第一时间执行。)
接下来主线程任务执行完毕后,就会去询问任务队列,看看有没有任务了。
- 有的话,就把它放进主线程去执行。没有的话,那我就隔一段时间问一下任务队列有没有,隔一段时间就问一次,无限重复下去。这就形成了“事件循环”(这是简易版,其实背后还有很多操作,但是大致的概念是这样的)
结语
本原意我是直接想开始编写《手把手教你实现一个 Promise》的。但是我害怕有的读者不是特别清楚原理。如果上来就描述 Promise 里宏任务和微任务的概念的话,害怕有的新人读者读不下去,所以就打算慢慢来讲解。
《手把手带你实现 Promise》 同样会像《手把手教你实现 防抖函数》一样,会带你一步一步去构思这个过程。觉得文章写的还行的读者可以点个关注,防止以后找不到哦~
特别感谢
@风潋水漪
我们公司的后端大佬~对于《编译型语言和非编译型语言区别》环节提出的修改意见🎁