详解高级前端JS运行原理和机制重点介绍js作为一种浏览器脚本语言。它的主要目的是与用户交互和操作DOM。所以js是单线程的,避免了同时操作同一个DOM的矛盾。为了利用多核CPU的计算能力,H5的WebWorker实现的“多线程”实际上是指“多个子线程”,完全由主线程控制,不允许DOM操作;js引擎有一个监控进程process,会不断的检查主线程的执行栈是否为Empty,一旦为空就会去EventQueue中检查是否有函数等待调用。这个过程是循环的,所以整个运行机制也称为事件循环(eventloop);所有同步任务都在主线程上执行,形成执行上下文栈;如果在执行微任务的过程中,有一个新的微任务加入到微任务队列中,新的微任务会被加入到队列的尾部,然后执行;二、相关概念1、为什么JS是单线程的?也就是说,一次只能做一件事。那么,为什么JavaScript不能有多线程呢?这样可以提高效率。JavaScript的单线程与它的使用有关。作为一种浏览器脚本语言,JavaScript的主要目的是与用户交互和操作DOM。这就决定了它只能是单线程的,否则会带来非常复杂的同步问题。例如,假设JavaScript同时有两个线程,一个线程向某个DOM节点添加内容,另一个线程删除这个节点,那么浏览器应该以哪个线程为准呢?,JavaScript是单线程的,这已经成为这门语言的核心特性,以后不会改变;为了利用多核CPU的计算能力,HTML5提出了WebWorker标准,允许JavaScript脚本创建多个线程,但子线程完全受限于主线程控制,不得操作DOM。因此,这个新标准并没有改变JavaScript的单线程特性;2、为什么JS需要异步?JS如果没有异步,只能自上而下执行。如果上一行的解析时间很长,那么后面的代码就会阻塞。对于用户来说,阻塞就意味着“卡住”,导致用户体验不佳;3、JS单线程是如何实现异步的?由于JS是单线程的,只能在一个线程上执行,如何实现异步呢?就是通过事件循环(eventloop),了解事件循环机制,了解JS的执行机制。4、任务队列“任务队列”是一个事件队列(也可以理解为消息队列)。当一个IO设备完成一个任务时,一个事件被添加到“任务队列”中,表明相关的异步任务可以进入“执行”。stack”。主线程读取“任务队列”,即读取其中的事件;“任务队列”中的事件,除了IO设备事件外,还包括一些用户产生的事件(如鼠标点击,页面滚动等)。只要指定了回调函数,这些事件就会在发生时进入“任务队列”,等待主线程读取。所谓“回调函数”(callback)就是将被主线程挂起的代码,一个异步任务必须指定一个回调函数,当主线程开始执行一个异步任务时,它会执行相应的回调函数;“任务队列”是先进先出的-out数据结构,前面的事件先被主线程读取,主线程的读取过程基本是自动的,执行栈一清空,“任务队列”上的第一个事件会自动进入主线程,但是由于后文提到的“定时器”功能,main线程首先要检查执行时间,有些事件只能在指定时间后才返回到主线程。读取一个异步任务,首先将异步任务放入事件表(Eventtable),当放入事件表的异步任务完成某事或满足一定条件(如setTimeout事件到达,鼠标点击,数据文件被获取),将这些异步任务压入事件队列(EventQueue),此时的异步任务是只有执行栈空闲时才能读取的异步任务;5.EventLoop主线程从“任务队列”中读取事件是一个连续的循环,所以整个运行机制也称为EventLoop(事件循环);EventLoop是javascript的执行机制6.setTimeout(fn,0)setTimeout(fn,0)的意思是指定一个任务在主线程最早可用空闲时间执行,即执行越早越好。它将一个事件添加到“任务队列”的末尾,因此它不会被执行,直到同步任务和“任务队列”的现有事件都被处理完。HTML5标准规定setTimeout()的第二个参数的最小值(最短间隔)不得低于4毫秒,低于这个值会自动增加。在此之前,旧版浏览器将最小间隔设置为10毫秒。另外,对于那些DOM变化(尤其是那些涉及页面重新渲染的),它们通常不会立即执行,而是每16毫秒执行一次。这时候使用requestAnimationFrame()的效果比setTimeout()要好。需要注意的是,setTimeout()只是将事件插入“任务队列”,主线程要等到当前代码(执行栈)执行完毕后,才会执行它指定的回调函数。如果当前代码耗时很长,可能要等待很长时间,所以没办法保证回调函数会在setTimeout()指定的时间执行,所有任务都需要排队,只有上一个任务完成后才会执行下一个任务。如果前一个任务耗时很长,后一个任务就得一直等下去。如果排队是因为计算量大,CPU太忙,那就算了,但大部分时间CPU是空闲的,因为IO设备(输入输出设备)很慢(比如Ajax操作读来自网络的数据),所以我们必须等待结果出来才能继续。JavaScript语言的设计者意识到,此时主线程可以完全忽略IO设备,暂停等待的任务,运行最先排队的任务。等到IO设备返回结果,再回去继续执行挂起的任务。因此,所有的任务可以分为两种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务是指在主线程上排队等待执行的任务。只有执行完上一个任务,才能执行下一个任务;异步任务是指不进入主线程而是进入“任务队列”(taskqueue)的任务,只有当“任务队列”通知主线程有异步任务可以执行时,任务才会进入执行的主线程。同步和异步任务进入不同的执行“地方”,同步进入主线程,异步进入EventTable和注册函数。当EventTable中指定的事情完成后,这个函数就会被移入EventQueue中。主线程中的任务执行完后为空,会去EventQueue中读取对应的函数,进入主线程执行。上述过程会不断重复,这就是常说的事件循环(eventloop)。我们不禁要问,我们怎么知道主线程执行栈是空的呢?js引擎有一个监控进程process,会不断的检查主线程执行栈是否为空。一旦为空,就会去EventQueue中查看是否有Waiting等待函数被调用;2、JavaScript宏任务和微任务,你认为同步和异步执行机制流程是JavaScript执行机制的全部吗?不,除了广义上的同步任务和异步任务,JavaScript还有其他任务有更详细的定义:宏任务(macrotask):包括整体代码脚本,setTimeout,setInterval微任务(micro-task):Promise,process.nextTick不同类型的任务会进入对应的EventQueue。事件循环的顺序决定了js代码的执行顺序。进入整体代码(宏任务)后,开始第一个循环。然后执行所有微任务。然后再从宏任务开始,找到其中一个要执行的任务队列,然后执行所有的微任务。3.举例说明JavaScript3.1的执行机制。同步控制台.log(1);控制台日志(2);console.log(3);/*执行结果:1、2、3个同步任务,依次执行*/3.2,同步和异步console.log(1);setTimeout(function(){console.log(2);},1000)console.log(3);/*执行结果:1,3,2个同步任务,按顺序一步步执行异步任务,放入消息队列,等待同步任务执行结束,读取消息队列并执行*/3.3,进一步分析异步任务console.log(1);setTimeout(function(){console.log(2);},1000)setTimeout(function(){console.log(3);},0)console.log(4);/*猜测:1,4,2,3但实际是:1,4,3,2分析:同步任务,按顺序一步步执行异步任务,在读取异步任务时,将异步任务放在Eventtable(事件表)中,当满足某些条件或指定的事情完成时(这里的时间分别为0ms和1000ms)。当指定的事件完成后,从Eventtable中注册到EventQueue(事件队列)中。当同步事件完成后,从EventQueue中读取事件并执行。(因为3的事情先完成,所以先从Eventtable注册到EventQueue,所以先执行的是3而不是之前的2)*/3.4、宏任务和微任务console.log(1);setTimeout(function(){console.log(2)},1000);newPromise(function(resolve){console.log(3);resolve();}).then(function(){console.log(4)});console.log(5);/*同步异步判断的结果应该是:1,3,5,2,4但实际上结果是:1,3,5,4,2为什么会这样??因为用同步和异步的方式来解释执行机制是不准确的,更准确的说法是宏任务和微任务:所以执行机制是:执行宏任务===>执行微任务===>执行另一个宏任务===>连续循环的意思是:在一个事件循环中,执行第一个宏任务,执行宏任务,执行当前事件循环中的微任务,执行完进入下一个事件循环,或者执行下一个宏任务*/3.5、是否充分理解JavaScript执行机制实例console.log('1');setTimeout(function(){console.log('2');process.nextTick(function(){console.log('3');})newPromise(function(resolve){console.log('4');resolve();}).then(function(){console.log('5')})})过程。nextTick(function(){console.log('6');})newPromise(function(resolve){console.log('7');resolve();}).then(function(){console.log('8')})setTimeout(function(){console.log('9');process.nextTick(function(){console.log('10');})newPromise(function(resolve){console.log('11');解决();}).then(函数(){console.log('12')})})/*1.第一轮事件循环流程分析如下:整体脚本作为第一个宏任务进入主线程。console.log,输出1遇到setTimeout,其回调函数分发给宏任务EventQueue。我们暂且记录为setTimeout1。当遇到process.nextTick()时,它的回调函数被分发到微任务EventQueue中。我们将其表示为process1。遇到Promise,直接执行newPromise,输出7。然后分发到微任务EventQueue。我们将其表示为then1。我们又遇到了setTimeout,它的回调函数被分发到宏任务EventQueue中,我们记录为setTimeout2。宏任务EventQueue微任务EventQueuesetTimeout1process1setTimeout2then1上表是第一轮事件循环宏任务结束时各个EventQueue的情况,此时已经输出了1和7。我们找到了两个微任务,process1和then1。执行process1,输出6。执行then1,输出8。好了,第一轮事件循环正式结束,这一轮的结果就是输出1、7、6、8。2.那么第二轮时间循环从setTimeout1宏任务开始:先输出2。接下来遇到process.nextTick(),同样分发到微任务EventQueue,记录为process2。newPromise立即执行输出4,然后也分发到微任务EventQueue,记录为then2。ThemacrotaskEventQueuemicrotaskEventQueuesetTimeout2process2then2第二轮事件循环macrotask结束,我们发现有两个微任务process2和then2可以执行。输出3.输出5.第二轮事件循环结束,第二轮输出为2,4,3,5.3.第三轮事件循环开始,只剩下setTimeout2执行。直接输出9。将process.nextTick()分配给微任务EventQueue。将其表示为process3。直接执行newPromise,输出11。将then分发给微任务EventQueue,记为then3。ThemacrotaskEventQueuemicrotaskEventQueueprocess3then3第三轮事件循环宏任务执行完毕,执行两个微任务process3和then3。输出10.输出12.第三轮事件循环结束,第三轮输出9,11,10,12.整个代码,一共三个事件循环,完整输出为1,7,6,8,2,4,3,5,9,11,10,12.*/总结javascript是一种单线程语言;EventLoop是javascript的执行机制;客户端还需要学习前端语言,后面会讲解一些前端知识点
