1. 浏览器执行中的线程
主线程: js引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个线程上。
工作线程:也称幕后线程,这个线程可能存在于浏览器和js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件
任务队列(Event Queue): 所有的任务都可以分为同步任务和异步任务
- 同步任务即为
立即执行的任务
,一般直接进入主线程中执行 - 异步任务不进入主线程,先放到辅助线程中处理,一般分为"发起函数"和"回调函数"(如setTimeout(fn(), 100)中setTimeout为发起函数,fn为回调函数),先执行发起函数(即下图中的"
挂起
"),在异步任务有了结果后,将注册的回调函数
放到任务队列
中,等主线程空闲的时候读取,采用先进先出
的机制来进行协调
下面是两个同步任务与异步任务流程图:
同步任务和异步任务进入不同的执行环境,同步任务进入主线程栈,异步任务在相应辅助线程中处理完成后,即异步函数达到触发条件了,就把回调函数推入任务队列中,而不是说注册一个异步任务就会被放在这个任务队列中进入任务队列,主线程中的任务执行完毕之后,就会去任务队列中读取对应的任务,推入主线程执行。这个过程就是我们所说的『Event loop (事件循环)』,每一次循环称为一个tick
2.宏任务(MacroTask)与微任务(MicroTask)
在JavaScript中,除了广义的同步任务和异步任务,还可以细分,一种是宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。
每次单个宏任务执行完毕后,检查微任务队列是否为空,不为空就按照先进先出的原则执行完微任务,清空微任务队列后,再执行下一个宏任务
2.1 常见宏任务与微任务
常见的macrotask宏任务有:
-
run-script标签(同步的代码执行)
-
setTimeout
-
setInterval
-
setImmediate (Node环境中)
-
requestAnimationFrame
-
I/O
-
UI
-
rendering
常见的microtask微任务有:
- process.nextTick (Node环境中)
- Promise callback (promise.then/catch等)
- MutationObserve
我们来看一个例子
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
运行步骤如下:
1.首先将整体代码作为一个宏任务执行,console.log(‘start’)执行,打印start
2.遇到异步进程setTimeout(fn(), time),先执行发起函数setTimeout,然后将回调函数fn放入宏任务队列
3.接着是执行new Promise(promise函数内部是同步处理的
,不会放到队列中,放入队列中的是它的then或catch回调),打印promise,然后再将后面的setTimeout放入宏任务队列。
4.执行同步任务script end
5.再检查微任务队列,执行异步微任务 promise.then,打印then1
6.微任务队列清空后,则开始检查宏任务队列,执行异步宏任务的回调 timeout1
7.执行异步宏任务回调timeout2
所以js中事件循环的机制为:先同步,后异步,先微任务, 再宏任务
然后记住一个特别重要容易错的:
new Promise中传入的函数立即执行,是同步函数!!!
new Promise中传入的函数立即执行,是同步函数!!!
new Promise中传入的函数立即执行,是同步函数!!!
new Promise(resolve => {}) // 立即执行
let p1 = new Promise() // 立即执行
但是如果是放在函数中返回
,那就调用执行
,例
function p1() {
return new Promise() // 不执行
}
p1() // 调用执行
上边我们说了微任务与宏任务的执行顺序,下面我们再来看下它们与dom渲染的关系以及为什么要先执行微任务再执行宏任务呢
2.2 eventloop 与 dom渲染的关系
1.callstack清空后(结束一个轮循),即所有同步任务完成后
2.先看有没有dom渲染
,如果有dom更新就去执行渲染
3.然后开始下一个eventloop
我们来测试一下,给dom元素添加三个标签,然后使用alert阻断一下,我们发现alert前控制台已经打印length为3,所以已经执行了我们的js代码,但是alert后页面才显示3个p标签,这就证明同步完成后是先去执行渲染的
window.onload = function() {
let p1 = document.createElement('p')
p1.innerHTML = 'p1'
let p2 = document.createElement('p')
p2.innerHTML = 'p2'
let p3 = document.createElement('p')
p3.innerHTML = 'p3'
let container = document.getElementById('container')
container.appendChild(p1)
container.appendChild(p2)
container.appendChild(p3)
console.log('length', container.children)
// alert('本次call stack 执行结束,dom结构 已更新但 未渲染')
// alert会阻断 js执行,也会阻断dom渲染,便于查看效果
}
alert点击确定后
2.3.宏任务与微任务的区别:
微任务在dom渲染前执行!!!
宏任务在dom渲染后执行!!!
我们依旧使用两个例子来测试一下,发现打印promise时页面是没有元素的,而打印setTimeout时页面已经输出三个元素
// 微任务 - dom渲染前
Promise.resolve().then(() => {
console.log('promise-length', container.children)
alert('promise')
})
// 宏任务 - dom渲染后
setTimeout(() => {
console.log('settimeout-length', container.children)
alert('settimeout')
}, 2000)
2.4 为什么先执行微任务再执行宏任务
因为微任务执行的时候不是放到web API中执行的,而是放到 微任务队列
中去执行的
1. Promise、async/await 等微任务是es6语法规定的,是js引擎执行
2. setTimeout等宏任务是浏览器语法规定的,是浏览器宿主环境执行
所以我们event loop过程应为:
1. call stack清空
2. 执行当前微任务
3. 尝试dom渲染
4. 触发event loop,执行宏任务
3.nextTick
我们先来看一个例子,每次点击按钮都向页面添加3个节点,并且打印节点长度
<template>
<div id="app">
<ul ref="ul1">
<li v-for="(item, index) in list" :key="index">{{item}}</li>
</ul>
<button @click="addItem">添加一项</button>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
list: ['a', 'b', 'c']
}
},
methods: {
addItem() {
console.log(11,Date.now(), `${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 获取dom元素
const ulElem = this.$refs.ul1;
console.log(ulElem.childNodes.length)
}
}
}
</script>
我们点击3次添加事件,发现dom中已有12个元素,但是我们获取到的却是9,
这证明什么呢,证明我们的打印事件在页面还没渲染完成之前就执行了!因为vue的异步渲染,data改变之后,dom不会立刻渲染,所以我们需要在页面渲染完成之后再执行相应的js,以获取最新节点,这时候就用到nextTick了
// 获取dom元素
this.$nextTick(() => {
const ulElem = this.$refs.ul1;
console.log(ulElem.childNodes.length)
})
所以我们要记住以下几点:
Vue是 异步渲染
(防止修改一次就渲染一次)
nextTick 汇总data 修改,等 dom 渲染完后再回调,只会渲染一次
减少 dom 操作次数
,提高性能
nextTick会在同步任务
之后立即执行
,为一个单独的队列
所以呢,我们上边所说的事件循环的过程再加上nextTick应为:
1. call stack清空
2. 执行当前微任务
3. 尝试dom渲染
4. 触发nextTick回调
5. 触发event loop,执行宏任务
好啦!以上应该就是我们事件循环会涉及到的大部分内容啦!如果再有其他的我都会补充到这里哦,欢迎小伙伴们积极留言讨论~