一、vm.$nextTick
1. 官方用法
- 将回调延迟到下次DOM更新循环后执行。
- 在修改数据之后立即使用它,然后等待DOM更新。
- 它跟全局方法
Vue.nextTick
一样,不同的是回调的this
自动绑定到调用它的实例上。
2. 示例
methods: {
example: function () {
this.message = 'changed' // 修改数据
// DOM 还没有更新
this.$nextTick( function() {
// DOM 现在更新了
this.doSomethingElse()// `this` 绑定到当前实例
})
}
}
3. nextTick源码
isNative()
:判断所传参数是否在当前环境原生支持;四个判断
对当前环境进行降级处理,尝试使用原生的Promise.then
、MutationObserver
、setImmediate
,都不支持的情况下最后使用setTimeout
。降级处理的目的是将flushCallbacks
函数放入任务队列(微任务、宏任务)
,等待下一次事件循环时来执行。- 参考链接:Vue中$nextTick源码解析
二、异步更新队列
1. Vue 在更新 DOM 时是异步执行的
- 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
- 如果同一个 watcher 被多次触发,只会被推入到队列中一次。(这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的)然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
- Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。
2. 示例
在组件内使用 vm.$nextTick()
实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
Vue.component('example', {
template: '<span>{{ message }}</span>',
data() {
return {
message: '未更新'
}
},
methods: {
updateMessage () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
}
})
三、JS运行机制
1. Event Loop
- JS分为
同步任务
和异步任务
。同步任务进入主线程
;异步任务进入事件表(EventTable)
。 - 同步任务在主线程按顺序执行;异步任务在事件表中注册函数,满足条件后进入
任务队列
。 - 主线程的同步任务执行完毕后,查看任务队列中是否有可执行的异步任务。
- 有,异步任务进入主线程执行;
- 没有,开始下一个循环。
2. 任务队列
任务队列
是一个事件的队列,除IO设备的事件以外,包括用户产生的事件(鼠标点击、页面滚动等);任务队列
是一个先进先出的数据结构,排在前面的事件优先被主线程读取;任务队列
除了放置异步任务的事件,还放置定时事件,即指定某些代码在多少时间后执行。
3. 定时器:setTimeout
setTimeout
接受两个参数:回调函数、推迟执行的毫秒数;- 如果第二个参数为0,表示当前代码执行完以后立即执行指定的回调函数;
setTimeout(fn,0)
,指定某个任务在主线程最早可得的空闲时间执行,尽可能早的执行(在任务队列的尾部添加一个事件,等到同步任务和任务队列现有的事件处理完,才会得到执行);- 若前面的代码耗时很长,没办法保证,回调函数一定在
setTimeout()
指定的时间执行。
4. 宏任务、微任务
-
宏任务(macro-task
):包括整段script代码、setImmediate
、setTimeout
、setInterval
、I/O等;# 浏览器 Node setImmediate
❌ ✅ setTimeout
✅ ✅ setInterval
✅ ✅ I/O
✅ ✅ requestAnimationFrame
✅ ❌ -
微任务(micro-task):
Promise系列:.then.catch.finally
、MutaionObserver
、process.nextTick
;# 浏览器 Node Promise.then catch finally
✅ ✅ MutationObserver
✅ ❌ process.nextTick
❌ ✅
5. Async await
- 函数带上
async
,函数返回值必定是Promise对象,会自动用Promise.resolve()
包装起来成为一个Promise对象(因此await
后面的代码相当于放到了Promise.then
的回调函数中去); await
通过阻塞后面的代码来实现,但是await表达式
执行顺序是从右往左
,即先执行await
右侧的代码,遇到await
后再阻塞后面的代码;await
等的是右侧表达式的结果(右侧是一个函数,则结果是这个函数的返回值,右侧是一个值则结果就为此值);
四、举个🌰
1. 例一
1.1 打印顺序?
console.log(1)
setTimeout(() => {
console.log(2)
}, 2000)
setTimeout(() => {
console.log(3)
Promise.resolve().then(() => {
console.log(4)
})
setTimeout(() => {
console.log(5)
}, 3000)
}, 1000)
new Promise((resolve, reject) => {
console.log(6)
resolve()
}).then(() => {
console.log(7)
})
console.log(8)
1.2 思路&结果
console.log(1) //同步代码,立即执行,打印1
setTimeout(() => { //宏任务,等待2000后执行
console.log(2) //进入任务队列【3,2】,
}, 2000)
setTimeout(() => {//宏任务,等待1000后执行
console.log(3) //,进入任务队列【3】
Promise.resolve().then(() => {//微任务,进入当前宏任务下的微任务队列
console.log(4)
})
setTimeout(() => {//宏任务,等待3000后执行
console.log(5) //进入任务队列【3,2,5】,
}, 3000)
}, 1000)
new Promise((resolve, reject) => {
console.log(6)//同步代码,立即执行,打印6
resolve()
}).then(() => {
console.log(7)//微任务,进入当前宏任务下的微任务队列
})
console.log(8)//同步代码,立即执行,打印8
①主线程执行同步代码,也就是开始一个宏任务的执行:即打印1、6、8
;然后依次执行当次循环宏任务产生的所有微任务:即打印7
;
②主线程读取任务列表中的下一个宏任务:即打印3
;并依次执行当前宏任务下的微任务:即打印4
;
③主线程读取任务列表中的下一个宏任务:即打印2
;并依次执行当前宏任务下的微任务:没有微任务;
④主线程读取任务列表中的下一个宏任务:即打印5
;并依次执行当前宏任务下的微任务:没有微任务;
结果:1、6、8、7、3、4、2、5
2. 例二
2.1 打印顺序?
console.log('1');
setTimeout(function() {
console.log('2');
Promise.resolve().then(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
}, 100)
Promise.resolve().then(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
Promise.resolve().then(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
async function async1() {
console.log('13')
await async2()
console.log('14')
}
async function async2() {
console.log('15')
}
async1()
console.log('16')
2.2 思路&结果
console.log('1');//同步任务,立即执行,打印1
setTimeout(function() {//宏任务①,进入任务队列【②,①】
console.log('2'); //等待100后执行
Promise.resolve().then(function() {
console.log('3'); //微任务,入①的微任务队列【3】
})
new Promise(function(resolve) {
console.log('4');//等待100后执行
resolve();
}).then(function() {
console.log('5');//微任务,入①的微任务队列【3,5】
})
}, 100)
Promise.resolve().then(function() {
console.log('6'); //微任务,入微任务列表【6】
})
new Promise(function(resolve) {
console.log('7'); //同步代码
resolve();
}).then(function() {
console.log('8') //微任务,入微任务列表【6,8】
})
setTimeout(function() { //宏任务②,进入任务队列【②】
console.log('9'); //尽早的执行
Promise.resolve().then(function() {
console.log('10'); //微任务,入②的微任务列表【10】
})
new Promise(function(resolve) {
console.log('11'); //尽早的执行
resolve();
}).then(function() {
console.log('12'); //微任务,入②的微任务列表【10,12】
})
})
async function async1() {
console.log('13') //同步代码,立即执行,打印13
await async2() //从右往左执行,先运行async2():打印15,后阻塞代码
console.log('14') //放到Promise的then回调函数中执行,即微任务,入微任务列表【6,8,14】
}
async function async2() {
console.log('15')
}
async1()
console.log('16') //同步任务,立即执行,打印16
①主线程执行同步代码,也就是开始一个宏任务的执行:即打印1、7、13、15、16
;然后依次执行当次循环宏任务产生的所有微任务:即打印6、8、14
;
②主线程读取任务列表中的下一个宏任务:即打印9、11
;并依次执行当前宏任务下的微任务:即打印10、12
;
③主线程读取任务列表中的下一个宏任务:即打印2、4
;并依次执行当前宏任务下的微任务:即打印3、5
;
结果:1、7、13、15、16、6、8、14、9、11、10、12、2、4、3、5