kekobin / blog

blog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Javascript零碎之事件循环机制

kekobin opened this issue · comments

单线程和异步

js的任务分为 同步 和 异步 两种,它们的处理方式也不同,同步任务是直接在主线程上排队执行,异步任务则会被放到任务队列中,若有多个任务(异步任务)则要在任务队列中排队等待,任务队列类似一个缓冲区,任务下一步会被移到调用栈(call stack),然后主线程执行调用栈的任务。

单线程是指js引擎中负责解析执行js代码的线程只有一个(主线程),即每次只能做一件事,而我们知道一个ajax请求,主线程在等待它响应的同时是会去做其它事的,浏览器先在事件表注册ajax的回调函数,响应回来后回调函数被添加到任务队列中等待执行,不会造成线程阻塞,所以说js处理ajax请求的方式是异步的。

总而言之,检查调用栈是否为空,以及确定把哪个task加入调用栈的这个过程就是事件循环,而js实现异步的核心就是事件循环。

macrotask & microtask

macrotask

包含执行整体的js代码,事件回调,XHR回调,定时器(setTimeout/setInterval/setImmediate),IO操作,UI render

microtask

更新应用程序状态的任务,包括promise回调,MutationObserver,process.nextTick,Object.observe
其中setImmediate和process.nextTick是nodejs的实现。

事件处理过程

大致如下图所示:

image

总结起来,一次事件循环的步骤包括:

  1. 检查macrotask队列是否为空,非空则到2,为空则到3
  2. 执行macrotask中的一个任务
  3. 继续检查microtask队列是否为空,若有则到4,否则到5
  4. 取出microtask中的任务执行,执行完成返回到步骤3
  5. 执行视图更新

这里需要注意,由于整体script就是一个宏任务,所以一开始在主线程执行完后,一定是去执行一次微任务,如下面的执行顺序一样。

记住: 事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。

mactotask & microtask的执行顺序
image

由一个例子开始

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

这是常见的同步与异步的逻辑,那么就有一个逻辑的执行先后问题。可以想想上面的执行结果会是怎样呢?

任务队列

所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中执行;而异步任务会通过任务队列( Event Queue )的机制来进行协调。

具体的可以用下面的图来大致说明一下:
image

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

所以可以看到上面首先会输出: 'script start'、script end',即一定是主线程的同步任务全部执行完后才会去执行异步任务。

现在来分析下上面代码的执行流程:

  1. 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
  2. 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
  3. 遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函1. 数,将其分到微任务 Event Queue 中,记为 then2
  4. 遇到 console.log,输出 script end
  5. 整体script宏任务执行完后,执行所有的微任务,即首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有的微任务
  6. 然后继续取出一个宏任务进行执行,即执行 setTimeout 任务,输出 setTimeout 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout。

特别注意一下示例情况:

setTimeout(function() {console.log('timer1')}, 0)

setTimeout(function() {console.log('timer2')}, 0)

new Promise(function executor(resolve) {
  console.log('promise 1')
  setTimeout(() => {
    resolve();
  }, 50) // 这里的延时会使得then回调在setTimeout之后执行,因为延迟时间比所有setTimeout都长。
  console.log('promise 2')
}).then(function() {
  console.log('promise then')
})

console.log('end')

运行结果如下:
image

new Promise 中的代码无论是否在 resolve()前还是后,都会立即执行,只是then回调才会被推到微任务队列中。

额外知识点集

V8引擎

  • emory Heap(内存堆) — 内存分配地址的地方
  • Call Stack(调用堆栈) — 代码执行的地方

Runtime(运行时)

javascript运行时,一方面引用引擎自身提供的api进行解析,一方面使用浏览器提供的web api(DOM、AJAX、setTimeout等)进行工作.
image

调用栈

JavaScript是一种单线程编程语言,这意味着它只有一个调用堆栈。因此,它一次只能做一件事。

调用栈是一种数据结构,它记录了我们在程序中的位置。如果我们运行到一个函数,它就会将其放置到栈顶,当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。

"堆栈溢出",当你达到调用栈最大的大小的时候就会发生这种情况,而且这相当容易发生,特别是在你写递归的时候却没有全方位的测试它。

参考

深入理解JavaScript事件循环机制
深入理解js事件循环机制(浏览器篇)
JavaScript 运行机制详解:再谈Event Loop