时间:2022-11-29 09:53:17 | 栏目:JavaScript代码 | 点击:次
前言:
不论是工作还是面试,我们可能都经常会碰到需要知道代码的执行顺序的场景,所以打算花点时间彻底搞懂JavaScript的执行机制。
想要搞懂JavaScript执行机制,你需要清楚下面这些知识: (以浏览器环境为例,与Node环境不同)
我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程:
进程是一个具有独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体 进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。
进程具有的特征:
线程:
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
进程与线程的区别:
JS为什么是单线程?
JavaScript
从它诞生之初就是作为浏览器的脚本语言,主要用来处理用户交互以及操作DOM,这就决定了它只能是单线程的,否则会带来非常复杂的同步问题。
举个例子: 如果JS是多线程的,其中一个线程要修改一个DOM元素,另外一个线程想要删除这个DOM元素,这时候浏览器就不知道该听谁的。所以为了避免复杂性,从一诞生,JavaScript就被设计成单线程。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质
作为前端工程师,浏览器想必都不陌生,并且浏览器是多进程的。
浏览器组成部分:
JavaScript
代码cookie
的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术??注意:与大多数浏览器不同的是,谷歌(Chrome)浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程
浏览器包含哪些进程?
浏览器进程:
Renderer
)进程得到的内存中的Bitmap
(位图),绘制到用户界面上第三方插件进程:
GPU进程:
渲染进程:
渲染进程包含哪些线程?
GUI渲染线程:
JavaScript
引擎线程时,GUI渲染线程会被挂起,当任务队列空闲时,主线程才会执行GUI渲染JavaScript引擎线程:
JavaScript
脚本,执行代码(如V8引擎)定时触发器线程:
setTimeout,setInterval
)setTimeout
中低于4ms的时间间隔算为4ms事件触发线程:
异步请求线程:
XMLHttpRequest
连接后浏览器会开一个线程由于JavaScript
是单线程的,这就决定了它的任务不可能只有同步任务,那些耗时很长的任务如果也按同步任务执行的话将会导致页面阻塞,
所以JavaScript任务一般分为两类:
同步任务:
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务:
异步任务指的是,不进入主线程、而进入"任务队列"(Event queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
常见的异步任务: 定时器,ajax,事件绑定,回调函数,promise
,async await
等
Event Queue
读取对应的函数,进入主线程执行。monitoring process
进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue
那里检查是否有等待被调用的函数。宏任务与微任务:
JavaScript除了广义上的同步任务与异步任务,还有更精细的任务定义:
不同类型的任务会进入到不同的任务队列:
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
执行栈:
JavaScript代码都是在执行上下文中执行的,在JavaScript中有三种执行上下文:
通常来说我们的JS代码都不止一个上下文,那么这些上下文的执行顺序是怎样的呢?
我们都知道栈是一种后进先出的数据结构,我们JavaScript
中的执行栈就是一种这样的栈结构,当JS引擎执行代码时,会产生一个全局上下文并把它压入执行栈,每当遇到函数调用时,就会产生函数执行上下文并压入执行栈。引擎从栈顶开始执行函数,执行完后会弹出该执行上下文。
function add(){ console.log(1) foo() console.log(3) } function foo(){ console.log(2) } add()
我们来看下上面这段代码的执行栈是怎样的:
任务队列:
前面我们说到了JavaScript
中所有的任务分为同步任务与异步任务,同步任务,顾名思义就是立即执行的任务,它一般是直接进入到主线程中执行。而我们的异步任务则是进入任务队列等待主线程中的任务执行完再执行。
任务队列是一个事件的队列,表示相关的异步任务可以进入执行栈了。主线程读取任务队列就是读取里面有哪些事件。
队列是一种先进先出的数据结构。
上面我们说到异步任务又可以分为宏任务与微任务,所以任务队列也可以分为宏任务队列与微任务队列
Macrotask Queue
:进行比较大型的工作,常见的有setTimeout
,setInterval
,用户交互操作,UI渲染等;Microtask Queue
:进行较小的工作,常见的有Promise
,Process.nextTick
;Event-Loop
)。例题验证:
我们来看道题目进行验证
(async ()=>{ console.log(1) setTimeout(() => { console.log('setTimeout1') }, 0); function foo (){ return new Promise((res,rej) => { console.log(2) res(3) }) } new Promise((resolve,reject)=>{ console.log(4) resolve() console.log(5) }).then(()=> { console.log('6') }) const res = await foo(); console.log(res); console.log('7') setTimeout(_ => console.log('setTimeout2')) })()
打印顺序是:1,4,5,2,6,3,7,setTimeout1,setTimeout2
分析:
console.log(1)
,直接打印1,接着遇到定时器属于宏任务,放入宏任务队列new Promise
是一个同步任务,所以直接打印4,遇到resolve,也就是后面的then
函数,放入微任务队列,再打印5promise
,new promise
属于同步任务,所以会直接打印2,await返回的是一个promise
的回调,await后面的任务放入微任务队列setTimeout1
,setTimeout2
JavaScript
的任务队列中的异步任务还包括定时器事件,即指定某些代码在多长时间后执行。定时器功能主要由setTimeout()
和setInterval()
两个函数来完成,他们的内部执行机制完全一样,区别主要在于setTimeout
是一次执行的过程,setInterval
则是反复执行的过程。
setTimeout
函数接受两个参数,第一个是要执行的回调函数,第二个是推迟执行的时间(ms)
如果我们把推迟时间设为0,是不是就会立即执行呢?
setTimeout(()=>{ console.log(1) },0) console.log(2)
但事实并不是这样的,上面的打印结果是先打印2,再打印1。是不是觉得很蒙?
我们用上面的事件循环的规则来理解就很清晰了,全局代码执行,遇到定时器setTimeout
,放入宏任务队列,接着往下执行同步代码,打印2,执行栈任务执行完再去微任务队列,没有微任务再去看宏任务队列,有一个宏任务,执行打印1。
setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。
HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()
的效果要好于setTimeout()。
需要注意的是,setTimeout()
只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()
指定的时间执行。