事件循环浅析_Vue

在开发中查错时经常遇到数据源追溯问题,这跟事件循环密切相关,如上图的事件循环,我们要怎么去分析呢。

一、JS是单线程的
  • 为什么JS是单线程的?

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
例如: 如果JS是多线程的,现在有一个线程要修改元素中的内容, 一个线程要删除该元素, 这时浏览器应该以哪个线程为准?

  • JS代码的执行顺序

JS中的代码都是串行的, 前面没有执行完毕后面不能执行;
1.程序运行会从上至下依次执行所有的同步代码
2.在执行的过程中如果遇到异步代码会将异步代码放到事件环中
3.当所有同步代码都执行完毕后, JS会不断检测 事件循环的异步代码是否满足条件
4.一旦满足条件就执行满足条件的异步代码

console.log("1"); // 同步代码
setTimeout(function () { // 异步代码
    console.log("2");
}, 500);
console.log("3"); // 同步代码
alert("666"); // 同步代码
// 输出结果 1 3 666 点击后输出 2
二、浏览器事件循环
  • 事件循环的执行过程

1.从上至下执行所有同步代码
2.在执行过程中遇到宏任务就放到宏任务队列中,遇到微任务就放到微任务队列中
3.当所有同步代码执行完毕之后,就执行微任务队列中满足需求所有回调
4.当微任务队列所有满足需求回调执行完毕之后,就执行宏任务队列中满足需求所有回调
5.每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空
6.放到队列中的任务都采用"先进先出原则", 也就是多个任务同时满足条件, 那么会先执行先放进去的

  • 宏任务和微任务

在JS的异步代码中又区分"宏任务(MacroTask)"和"微任务(MicroTask)"
常见的宏任务和微任务:
MacroTask: setTimeout, setInterval, setImmediate(IE独有)
MicroTask: Promise, MutationObserver, process.nextTick(node独有)

MutationObserver用于监听节点变化,https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver

// MutationObserver是专门用于监听节点的变化
// html
<div></div>
<button class="add">添加节点</button>
<button class="del">删除节点</button>

// js
let oDiv = document.querySelector("div");
let oAddBtn = document.querySelector(".add");
let oDelBtn = document.querySelector(".del");
oAddBtn.onclick = function () {
    let op = document.createElement("p");
    op.innerText = "我是段落";
    oDiv.appendChild(op);
}
oDelBtn.onclick = function () {
    let op = document.querySelector("p");
    oDiv.removeChild(op);
}
let mb = new MutationObserver(function () {
    console.log("执行了");
});
mb.observe(oDiv, {
    "childList": true
});
console.log("同步代码start");
console.log("同步代码end");
// 输出结果:同步代码start
// 同步代码end
// 执行了*n

事件循环示例

// 1.宏任务
setTimeout(function () {
    console.log("setTimeout1");
    // 2.微任务 p1
    Promise.resolve().then(function () {
        console.log("Promise1");
    });
    // 2.微任务 p2
    Promise.resolve().then(function () {
        console.log("Promise2");
    });
}, 0);
// 1.宏任务
setTimeout(function () {
    console.log("setTimeout2");
    // 2.微任务 p3
    Promise.resolve().then(function () {
        console.log("Promise3");
    });
    // 2.微任务 p4
    Promise.resolve().then(function () {
        console.log("Promise4");
    });
}, 0);
// 输出见上面链接的图
三、node中的事件循环

和浏览器中一样NodeJS中也有事件循环(Event Loop),但是由于执行代码的宿主环境和应用场景不同,所以两者的事件循环也有所不同.

  • NodeJS事件循环和浏览器事循件环区别

1.任务队列个数不同
浏览器事件循环有2个事件队列(宏任务队列和微任务队列),NodeJS事件循环有6个事件队列
2.微任务队列不同
浏览器事件循环中有专门存储微任务的队列,NodeJS事件循环中没有专门存储微任务的队列
3.微任务执行时机不同
浏览器事件循环中每执行完一个宏任务都会去清空微任务队列
NodeJS事件循环中只有同步代码执行完毕和其它队列之间切换的时候回去清空微任务队列
4.微任务优先级不同
浏览器事件循环中如果多个微任务同时满足执行条件, 采用先进先出
NodeJS事件循环中如果多个微任务同时满足执行条件, 会按照优先级执行

  • node中的任务队列
    ┌───────────────────────┐
    ┌> │timers │执行setTimeout() 和 setInterval()中到期的callback
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │pending callbacks│执行系统操作的回调, 如:tcp, udp通信的错误callback
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │idle, prepare │只在内部使用
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │poll │执行与I/O相关的回调 │ (除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │check │执行setImmediate的callback
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    └─┤close callbacks │执行close事件的callback,例如socket.on("close",func)
    └───────────────────────┘

node事件循环注意点
1.和浏览器不同的是没有宏任务队列和微任务队列的概念
宏任务被放到了不同的队列中, 但是没有存放微任务的队列
微任务会在执行完同步代码和队列切换的时候执行
2.什么时候切换队列?
当队列为空(已经执行完毕或者没有满足条件回到)
或者执行的回调函数数量到达系统设定的阈值时任务队列就会切换
3.在NodeJS中process.nextTick微任务的优先级高于Promise.resolve微任务
4.执行完poll, 会查看check队列是否有内容, 有就切换到check
如果check队列没有内容, 就会查看timers是否有内容, 有就切换到timers
如果check队列和timers队列都没有内容, 为了避免资源浪费就会阻塞在poll

setTimeout(function () {
    console.log("setTimeout");
});   //timers
Promise.resolve().then(function () {
    console.log("Promise");
});  //等待切换执行
console.log("同步代码 Start");
process.nextTick(function () {
    console.log("process.nextTick");
});  //等待切换执行/*  */
setImmediate(function () {
    console.log("setImmediate");
});   //poll
console.log("同步代码 End");
/* 
    输出结果:同步代码 Start
             同步代码 End
             process.nextTick
             Promise
             setTimeout
             setImmediate
*/
四、Vue.nextTick中的事件循环
  • nextTick用途:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
let vm = new Vue({
    data() {
        return {
            message: 'start'
        }
    }
})
// 改变数据
vm.message = 'changed'

// 设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'

//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
    console.log(vm.$el.textContent) //可以得到'changed'
})

Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

  • 补充:Vue中的nextTick的实现流程判断如下

1.当前环境有提供原生的 Promise ? Promise.resolve().then(flushCallbacks) :
2.是 ie 环境 ? setImmediate(flushCallbacks) :
3.有提供原生的 MutationObserver ? new MutationObserver(flushCallbacks) :
4.setTimeout(flushCallbacks, 0);