当前位置: 首页 > 后端技术 > Node.js

深入理解JavaScript运行机制

时间:2023-04-03 16:50:19 Node.js

JavaScript单线程机制JavaScript(也是语言的核心)的语言特点之一就是单线程。什么是单线程?简单地说,同一时间只能做一件事。当有多个任务时,一个人只能按顺序完成一个,然后再执行下一个。为什么JS是单线程的?JS最初是为在浏览器中使用而设计的。作为一种浏览器脚本语言,JavaScript的主要用途是与用户交互和操作DOM。如果浏览器中的JS是多线程的,会带来非常复杂的同步问题。例如,假设JavaScript同时有两个线程。一个线程向某个DOM节点添加内容,另一个线程删除该节点。浏览器应该以哪个线程为基础?因此,为了避免复杂性,JavaScript从诞生之日起就是单线程的。为了提高CPU利用率,HTML5提出了WebWorker标准,允许JavaScript脚本创建多个线程,但子线程完全由主线程控制,不得操作DOM。所以这个标准并没有改变JavaScript单线程任务队列同步和异步的本质同步和异步的重点是消息通知机制的同步:调用之后,得到结果之前,调用不返回,一旦调用返回,得到返回值。简而言之,调用者主动等待调用结果是异步的:调用者调用后直接返回,所以没有返回结果。也就是说,当一个异步过程调用发出时,调用者不会立即得到结果,而是在调用发出后,被调用者通过状态、通知或回调函数来处理调用。阻塞与非阻塞阻塞与非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态。阻塞调用是指在调用结果返回之前,当前线程将被挂起。调用线程只有在得到结果后才会返回。非阻塞调用是指调用在不能立即得到结果之前不会阻塞当前线程。单线程是指同一时间只能做一件事情,前一件事情做完之后才能执行后面的事件。.遇到需要时间的IO事件,问题就来了。你必须等到这些事件结束,但此时CPU是空闲的。这会浪费大量的计算机性能。JavaScript语言的设计者意识到,此时主线程完全可以忽略IO设备,挂起等待的任务,运行最先排队的任务。等到IO设备返回结果,再回去继续执行挂起的任务。因此,所有的任务可以分为两种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务是指在主线程上排队等待执行的任务,只有上一个任务执行完才能执行下一个任务;异步任务是指不进入主线程而是进入“任务队列”(taskqueue)的任务,只有当“任务队列”通知主线程有异步任务可以执行时,任务才会进入执行的主线程。(1)所有的同步任务都在主线程上执行,形成一个执行上下文栈(2)除了主线程之外,还有一个“任务队列”。只要异步任务有运行结果,就在“任务队列”中放入一个事件(3)一旦“执行栈”中的所有同步任务都执行完毕,系统就会去读取“任务队列”,看看有什么在它的事件中。那些对应的异步任务结束等待状态,进入执行栈,开始执行(4)主线程重复EventLoop上面的第三步。主线程从任务队列中读取事件。这个过程是连续的,所以整个这个运行机制也称为事件循环(eventloop)。上图中,主线程运行时,会产生堆和栈。对象可以存放在堆中,变量、函数、函数可以存放在栈中。堆栈中的指针、代码语句和其他代码调用各种外部API,它们将各种事件(点击、加载、完成)添加到“任务队列”中。WebAPI是独立的线程,不同于组件,不会阻塞主线程。线程执行,比如获取后台数据,同步的话会阻塞。例如,一个HTTP请求会开启一个线程。当执行栈中的任务完成后,主线程会读取事件队列(先进先出)并执行相应的回调函数作为例子,看下面代码functionread(){console.log(1);setTimeout(function(){console.log(2);setTimeout(function(){console.log(4)});});setTimeout(function(){console.log(5)})console.log(3);}read();代码执行结果:13254先执行同步代码,打印1、3、setTimeout异步代码到事件中在队列中,先放的先执行,后放的后执行。定时器“任务队列”可以放置定时事件,即指定某些代码在定时器功能主要由setTimeout()和setInterval()组成后执行多长时间。函数,它们的内部运行机制是完全一样的,不同的是前者指定的代码执行一次,而后者是重复执行,主要以setTimeout为例说明setTimeout()接受两个参数,第一个是一个回调函数,第二个是延迟执行的毫秒数setTimeout(function(){console.log(3)},2000);setTimeout(function(){console.log(1);setTimeout(function(){console.log(2);},1000);},1000);执行结果为:132setTimeout()将事件放入等待任务队列。主任务队列中的任务执行完毕后,执行等待任务队列。写的延迟是3秒,但是函数实际执行是5、6秒。这是怎么回事?setTimeout()只是将事件插入“任务队列”。它必须等到当前代码(执行栈)执行完毕后,主线程才会执行它指定的回调函数。如果当前代码耗时较长,可能会耗时较长,因此无法保证回调函数会在setTimeout()指定的时间执行Promise和process.nextTick(callback)。除了广义的同步任务和异步任务,我们还有更细化的任务定义:process.nextTick:在事件循环的下一个周期调用callback回调函数。作用是推迟一个函数,直到代码编写的下一个同步方法执行完毕或者异步方法的事件回调函数开始执行时;它类似于setTimeout(fn,0)函数的作用,但是它的效率要高很多不同类型的Task会进入对应的EventQueue。比如setTimeout和setInterval会进入相同的EventQueue事件循环顺序,这决定了js代码的执行顺序。进入整体代码(宏任务)后,开始第一个循环。然后执行所有微任务。然后再从宏任务开始,找到其中一个要执行的任务队列,然后执行所有的微任务。事件循环、macrotask和microtask的关系如下:macrotask=>执行结束=>有可执行的microtasks=>执行所有microtasks=>开始一个新的macrotaskmacrotask=>执行结束=>没有可执行的microtask=>开始anewmacrotask我们用一段代码来说明:setTimeout(function(){console.log('setTimeout');});newPromise(function(resolve){console.log('promise');})。然后(函数(){console.log('then');});console.log('console');此代码用作宏任务。进入主线程时,首先遇到setTimeout,然后会注册回调函数后,分发到宏任务EventQueue。接下来,当遇到Promise时,立即执行新的Promise。then函数分发给微任务。当EventQueue遇到console.log()时,立即执行——整体代码脚本作为第一个macrotask执行结束,还有哪些microtasks?我们发现microtaskEventQueue中的第一轮事件循环执行结束,我们开始第二轮循环,当然是从macrotaskEventQueue开始。我们在宏任务EventQueue中找到setTimeout对应的回调函数。执行结束后,我们来看下一段代码说明:process.nextTick(functionA(){console.log(1);process.nextTick(functionB(){console.log(2);});});setTimeout(functiontimeout(){console.log('TIMEOUTFIRED');},0)上面代码的执行结果:12TIMEOUTFIRED上面代码中,由于进程。nextTick方法指定的回调函数总是在当前“执行栈”的末尾触发,所以不仅函数A在setTimeout指定的回调函数超时前执行,函数B也会在超时前执行。这意味着如果有多个process.nextTick语句(无论是否嵌套),它们都将在当前“执行堆栈”上执行。我们看下一段代码说明:functiona(){setTimeout(function(){console.log('a2');},0);process.nextTick(function(){console.log('a1')});}functionb(){process.nextTick(function(){console.log('b1');})}a();b();一个函数执行会形成一个执行栈,任务队列中的回调函数一次只取一个,执行时会形成一个执行栈,当你第一次运行这个脚本时,这个脚本中的所有同步代码都会在执行栈中执行,b将在执行栈中执行。当它们在第一个宏任务a中一起执行时,a2会被放入宏任务队列,a1则被放入微任务队列。b执行时,将b1放入微任务队列--------------------第一个宏任务执行-----------------------宏任务执行完后,会清空微任务队列,即a1和b1都会执行,输出a1和b1-----------------第一个微任务队列清空----------------------------然后取下一个macrotaskqueuetask中的宏,即a2执行。输出a2为什么macrotask要和microtask配对,因为这样最合理。微任务是需要在空闲时立即执行的任务。与微任务相比,宏任务可以在后面执行。虽然都是异步任务,但是通过这个优先级的设置,达到了控制异步回调执行顺序的目的。值得注意的是:同步代码执行完后,会先清空microtask,然后将macrotask队列中第一个事件对应的callback取出到执行栈中执行,然后microtask会被再次清除,依此类推。通过以上三段代码,你知道JS的执行顺序了吗?下面来分析一段复杂的代码,看看你是否真的掌握了js的执行机制{console.log('process.nextTick2');});},0);newPromise(function(resolve,reject){console.log('promise');resolve();}).then(function(){console.log('promisethen');});process.nextTick(function(){console.log('process.nextTick1');});console.log('main2');上面代码的执行结果是:main1=>promise=>main2=>process.nextTick1=>promisethen=>setTimeout=>process.nextTick2系统开始执行脚本,这个脚本是一个宏任务执行所有的同步代码块中的代码,输出main1next1进入microtask,setTimeout+nextTick2(下一轮)进入macrotask队列,promise构造函数部分是同步的,立即执行输出promise,promisethenintomicrotask下面的同步代码输出main2,然后执行microtask并输出nextTick1,promise然后执行macrotask并输出setTimeout,将nexttick2放入microtask队列然后执行microtasknexttick2nextTick是node自己定义实现的一个概念,它的回调调用入口在MakeCallback的最后函数在eventloop过程中,driver调用清除js层的队列,最后执行microtasks,妥善处理可能触发的promise,显然process.nextTick1>promise.then