上次大家跟我吃饱喝足又撸了一遍 PromiseA+, 想必大家肯定满脑子想的都是西瓜可乐......
什么西瓜可乐! 明明是 Promise!
呃, 清醒一下, 今天大家搬个小板凳, 听我说说 JS 中比较有意思的事件环, 在了解事件环之前呢, 我们先来了解几个基本概念.
栈(Stack)
栈是一种遵循后进先出 (LIFO) 的数据集合, 新添加或待删除的元素都保存在栈的末尾, 称作栈顶, 另一端称作栈底. 在栈里, 新元素都靠近栈顶, 旧元素都接近栈底
感觉说起来并不是很好理解, 我们举个例子, 比如有一个乒乓球盒, 我们不停的向球盒中放进乒乓球, 那么最先放进去的乒乓球一定是在最下面, 最后放进去的一定是在最上面, 那么如果我们想要把这些球取出来是不是就必须依次从上到下才能拿出来, 这个模型就是后进先出, 就是我们后进入球盒的球反而最先出来.
栈的概念其实在我们 js 中十分的重要, 大家都知道我们 js 是一个单线程语言, 那么他单线程在哪里呢, 就在他的主工作线程, 也就是我们常说的执行上下文, 这个执行上下文就是栈空间, 我们来看一段代码:
- console.log('1');
- function a(){
- console.log('2');
- function b(){
- console.log('3')
- }
- b()
- }
- a()
复制代码
我们知道函数执行的时候会将这个函数放入到我们的执行上下文中, 当函数执行完毕之后会弹出执行栈, 那么根据这个原理我们就能知道这段代码的运行过程是
首先我们代码执行的时候会有一个全局上下文, 此时代码运行, 全局上下文进行执行栈, 处在栈底的位置
我们遇到 console.log('1'), 这个函数在调用的时候进入执行栈, 当这句话执行完毕也就是到了下一行的时候我们 console 这个函数就会出栈, 此时栈中仍然只有全局上下文
接着运行代码, 这里注意的是我们遇到的函数声明都不会进入执行栈, 只有当我们的函数被调用被执行的时候才会进入, 这个原理和我们执行栈的名字也就一模一样, 接着我们遇到了 a(); 这句代码这个时候我们的 a 函数就进入了执行栈, 然后进入到我们 a 的函数内部中, 此时我们的函数执行栈应该是
全局上下文 -- a
接着我运行 console.log('2'), 执行栈变成
全局上下文 --a--console
, 接着我们的 console 运行完毕, 我们执行栈恢复成
全局上下文 -- a
接着我们遇到了 b(); 那么 b 进入我们的执行栈,
全局上下文 --a--b
,
接着进入 b 函数的内部, 执行 console.log('3')的时候执行栈为
全局上下文 --a--b--console
, 执行完毕之后回复成
全局上下文 --a--b
然后我们的 b 函数就执行完毕, 然后就被弹出执行栈, 那么执行栈就变成
全局上下文 --a
然后我们的 a 函数就执行完毕, 然后就被弹出执行栈, 那么执行栈就变成全局上下文
然后我们的全局上下文会在我们的浏览器关闭的时候出栈
我们的执行上下文的执行过程就是这样, 是不是清楚了很多~
通过上面的执行上下文我们可以发现几个特点:
执行上下文是单线程
执行上下文是同步执行代码
当有函数被调用的时候, 这个函数会进入执行上下文
代码运行会产生一个全局的上下文, 只有当浏览器关闭才会出栈
队列(Queue)
队列是一种遵循先进先出 (FIFO) 的数据集合, 新的条目会被加到队列的末尾, 旧的条目会从队列的头部被移出.
这里我们可以看到队列和栈不同的地方是栈是后进先出类似于乒乓球盒, 而队列是先进先出, 也就是说最先进入的会最先出去. 同样我们举个例子, 队列就好比是我们排队过安检, 最先来到的人排在队伍的首位, 后来的人接着排在队伍的后面, 然后安检员会从队伍的首端进行安检, 检完一个人就放行一个人, 是不是这样的一个队伍就是先进先出的一个过程.
队列这里我们就要提到两个概念, 宏任务(macro task), 微任务(micro task).
任务队列
Js 的事件执行分为宏仁务和微任务
宏仁务主要是由 script(全局任务),setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
微任务主要是 process.nextTick, Promise.then, Object.observer, MutationObserver.
浏览器事件环
js 执行代码的过程中如果遇到了上述的任务代码之后, 会先把这些代码的回调放入对应的任务队列中去, 然后继续执行主线程的代码知道执行上下文中的函数全部执行完毕了之后, 会先去微任务队列中执行相关的任务, 微任务队列清空之后, 在从宏仁务队列中拿出任务放到执行上下文中, 然后继续循环.
执行代码, 遇到宏仁务放入宏仁务队列, 遇到微任务放入微任务队列, 执行其他函数的时候放入执行上下文
执行上下文中全部执行完毕后, 执行微任务队列
微任务队列执行完毕后, 再到宏仁务队列中取出第一项放入执行上下文中执行
接着就不停循环 1-3 的步骤, 这就是浏览器环境中的 js 事件环
- // 学了上面的事件环 我们来看一道面试题
- setTimeout(function () {
- console.log(1);
- }, 0);
- Promise.resolve(function () {
- console.log(2);
- })
- new Promise(function (resolve) {
- console.log(3);
- });
- console.log(4);
- // 上述代码的输出结果是什么???
复制代码
思考思考思考思考~~~
正确答案是 3 4 1, 是不是和你想的一样? 我们来看一下代码的运行流程
- // 遇到 setTimeout 将 setTimeout 回调放入宏仁务队列中
- setTimeout(function () {
- console.log(1);
- }, 0);
- // 遇到了 promise, 但是并没有 then 方法回调 所以这句代码会在执行过程中进入我们当前的执行上下文 紧接着就出栈了
- Promise.resolve(function () {
- console.log(2);
- })
- // 遇到了一个 new Promise, 不知道大家还记不记得我们上一篇文章中讲到 Promise 有一个原则就是在初始化 Promise 的时候 Promise 内部的构造器函数会立即执行 因此 在这里会立即输出一个 3, 所以这个 3 是第一个输入的
- new Promise(function (resolve) {
- console.log(3);
- });
- // 然后输入第二个输出 4 当代码执行完毕后回去微任务队列查找有没有任务, 发现微任务队列是空的, 那么就去宏仁务队列中查找, 发现有一个我们刚刚放进去的 setTimeout 回调函数, 那么就取出这个任务进行执行, 所以紧接着输出 1
- console.log(4);
复制代码
看到上述的讲解, 大家是不是都明白了, 是不是直呼简单~
那我们接下来来看看 node 环境中的事件执行环
NodeJs 事件环
浏览器的 Event Loop 遵循的是 html5 标准, 而 NodeJs 的 Event Loop 遵循的是 libuv 标准, 因此呢在事件的执行中就会有一定的差异, 大家都知道 nodejs 其实是 js 的一种 runtime, 也就是运行环境, 那么在这种环境中 nodejs 的 api 大部分都是通过回调函数, 事件发布订阅的方式来执行的, 那么在这样的环境中我们代码的执行顺序究竟是怎么样的呢, 也就是我们不同的回调函数究竟是怎么分类的然后是按照什么顺序执行的, 其实就是由我们的 libuv 所决定的.
- > timers
- pending callbacks
- idle, prepare
- incoming:
- poll < connections,
- data, etc.
- check
- close callbacks
复制代码
我们先来看下这六个任务是用来干什么的
timers: 这个阶段执行 setTimeout()和 setInterval()设定的回调.
pending callbacks: 上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行.
idle, prepare: 仅内部使用.
poll: 执行 I/O callback, 在适当的条件下会阻塞在这个阶段
check: 执行 setImmediate()设定的回调.
close callbacks: 执行比如 socket.on('close', ...)的回调.
我们再来看网上找到的一张 nodejs 执行图, 我们能看到图中有六个步骤 , 当代码执行中如果我们遇到了这六个步骤中的回调函数, 就放入对应的队列中, 然后当我们同步人物执行完毕的时候就会切换到下一个阶段, 也就是 timer 阶段, 然后 timer 阶段执行过程中会把这个阶段的所有回调函数全部执行了然后再进入下一个阶段, 需要注意的是我们在每次阶段发生切换的时候都会先执行一次微任务队列中的所有任务, 然后再进入到下一个任务阶段中去, 所以我们就能总结出 nodejs 的事件环顺序
同步代码执行, 清空微任务队列, 执行 timer 阶段的回调函数(也就是 setTimeout,setInterval)
全部执行完毕, 清空微任务队列, 执行 pending callbacks 阶段的回调函数
全部执行完毕, 清空微任务队列, 执行 idle, prepare 阶段的回调函数
全部执行完毕, 清空微任务队列, 执行 poll 阶段的回调函数
全部执行完毕, 清空微任务队列, 执行 check 阶段的回调函数(也就是 setImmediate)
全部执行完毕, 清空微任务队列, 执行 close callbacks 阶段的回调函数
然后循环 1-6 阶段
那我们来练练手~~~
- // 我们来对着我们的执行阶段看看
- let fs = require('fs');
- // 遇到 setTimeout 放入 timer 回调中
- setTimeout(function(){
- Promise.resolve().then(()=>{
- console.log('then1');
- })
- },0);
- // 放入微任务队列中
- Promise.resolve().then(()=>{
- console.log('then2');
- });
- // i/o 操作 放入 pending callbacks 回调中
- fs.readFile('./text.md',function(){
- // 放入 check 阶段
- setImmediate(()=>{
- console.log('setImmediate')
- });
- // 放入微任务队列中
- process.nextTick(function(){
- console.log('nextTick')
- })
- });
复制代码
首先同步代码执行完毕, 我们先清空微任务, 此时输出 then2, 然后切换到 timer 阶段, 执行 timer 回调, 输出 then1, 然后执行 i/o 操作回调, 然后清空微任务队列, 输出 nextTick, 接着进入 check 阶段, 清空 check 阶段回调输出 setImmediate
所有的规则看着都云里雾里, 但是呢只要我们总结出来了规律, 理解了他们的运行机制那么我们就掌握了这些规则, 好咯, 今天又学了这么多, 不说了不说了, 赶紧滚去写业务代码了.............
来源: https://juejin.im/post/5b69b07d6fb9a04f86065596