单线程的 JavaScript 一段一段地执行,前面的执行完了,再执行后面的,试想一个,如果前一个任务需要执行很久,比如接口请求、I/O 操作,此时后面的任务只能干巴巴地等待么?干等不仅浪费了资源,而且页面的交互程度也很差。JavaScript 意识到了这个问题,他们将任务分成了同步任务和异步任务,对于二者有不同的处理。
JavaScript 在运行时会将变量存放在堆(heap)和栈(stack)中,堆中通常存放着一些对象,而变量及对象的指针则存放在栈中。JavaScript 在执行时,同步任务会排好队,在主线程上按照顺序执行,前面的执行完了再执行后面的,排队的地方叫执行栈(execution context stack)。JavaScript 对异步任务不会停下来等待,而是将其挂起,继续执行执行栈中的同步任务,当异步任务有返回结果时,异步任务会加入与执行栈不一样的队列,即任务队列(task queue),所以任务队列中存放的是异步任务执行完成后的结果,通常是回调函数。
当执行栈的同步任务已经执行完成,此时主线程闲下来,它便会去查看任务队列是否有任务,如果有,主线程会将最先进入任务队列的任务加入到执行栈中执行,执行栈中的任务执行完了之后,主线程便又去任务队列中查看是否有任务可执行。主线程去任务队列读取任务到执行栈中去执行,这个过程是循环往复的,这便是 Event Loop,事件循环。
网上有张流传甚广的图对这一过程进行了总结,在图中我们可以看到,JavaScript 在运行时产生了堆和栈,ajax、setTimeout 等异步任务被挂起,异步任务的返回结果加入任务队列,主线程会循环往复地读取任务队列中的任务,加入执行栈中执行。
为了更好的理解 JavaScript 的执行机制,我们来看个小例子。
1 console.log(1) 2 setTimeout(function() { 3 console.log(2) 4 }, 300) 5 console.log(3)
输出的结果是 1,3,2。setTimeout 是一个定时器,延迟 300 毫秒执行,所以 300 毫秒后,打印 2 的回调函数才会进入任务队列,等到执行栈中的代码执行完成后,也就是打印出 1 和 3 后,打印出 2 的回调函数才进入执行栈执行。
如果将 setTimeout 的第二个参数设置为 0,它表示主线程空闲之后尽早执行它的回调,HTML5 规定 setTimeout 的第二个参数不得小于 4 毫秒。
1 setTimeout(function() { 2 console.log(1) 3 }, 0) 4 console.log(2) 5 6 // 2,1
对于 setTimeout 还有一个需要注意的是,它的延迟时间并不是等待多少毫秒后就一定会执行,始终是要等待主线程已经空闲了才会去读取它,如果执行栈中的任务需要很长时间才能执行完,那任务队列中的任务只能等待。我们可以通过一个例子来体验一下。
1 var enterTime = Date.now() 2 3 function sleep(time) { 4 for(var temp = Date.now(); Date.now() - temp <= time;); 5 } 6 7 setTimeout(function() { 8 var exeTime = Date.now() 9 console.log(exeTime - enterTime) 10 }, 300) 11 12 sleep(1000) // 睡眠 1 秒
我们定义了一个 sleep 函数,设置了 1 秒的执行时间,所以 setTimeout 要等待的时间肯定大于 1 秒,而不是 300 毫秒后就执行了。上述代码的执行结果是 1000 左右,值不固定,可以复制代码到控制台执行看看。