Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript零碎之事件循环机制 #51

Open
kekobin opened this issue Oct 10, 2019 · 0 comments
Open

Javascript零碎之事件循环机制 #51

kekobin opened this issue Oct 10, 2019 · 0 comments

Comments

@kekobin
Copy link
Owner

kekobin commented Oct 10, 2019

单线程和异步

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant