用法
Vue.nextTick( [callback, context] )
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用
Vue.nextTick()
.then(function () {
// DOM 更新了
})
2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持 Promise (IE:你们都看我干嘛),你得自己提供 polyfill。
Vue.nextTick与vm.$nextTick方法作用是一样的,区别是后者回调的this会自动绑定到调用它的实例上。
为什么Vue.js使用异步更新队列
Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。
在Vue.js中,当状态发生变化时,watcher会得到通知,然后触发虚拟DOM的渲染流程。要注意的是,watcher触发渲染这个操作并不是同步的,而是异步的。
如果在同一轮事件循环中有两个数据发生了变化,那么组件的watcher会收到两份通知,从而进行两次渲染。但是,没必要渲染两次,虚拟DOM会对整个组件进行渲染。我们只需要等所有状态都修改完之后,一次性将整个组件的DOM渲染到最新就好了。
Vue.js解决这个问题的方法是,将收到通知的watcher实例添加到队列中缓存起来,并且在添加到队列之前检查其中是否已经存在相同的watcher,只有不存在时,才将watcher添加到队列中。在下一次事件循环的时候,Vue.js会让队列中的watcher触发渲染流程并清空队列。
简单来说,Vue在修改数据之后,视图并不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
事件循环
推荐阅读: juejin.cn/post/693211…
执行栈中的所有任务执行完成之后,会去检查微任务队列中是否有事件存在,如果存在,就会一次调用微任务队列中事件对应的回调,直到为空。然后再去宏任务队列中取出一个事件,把对应的回调加入到执行栈,执行栈中所有任务执行完毕之后,又会继续检查微任务队列是否有事件存在。重复此过程就形成了一个无限循环,叫做事件循环。
宏任务(macrotask):setTimeout、setInterval、setImmediate、I/O、UI rendering
微任务(microtask):promise.then、process.nextTick、MutationObserver、queneMicrotask(开启一个微任务)
执行栈
在执行一个方法的时候,js会生成一个与这个方法对应的执行环境,又叫执行上下文。这个执行环境中有这个方法的私有作用域、上层作用域的指向、方法的参数、私有作用域中定义的变量以及this对象。而这个执行环境会被添加到一个栈中,这个栈就是执行栈。
函数多了,就有多个函数执行上下文,每次调用函数就会创建一个新的执行上下文。js引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
如果对以上两个概念还是很模糊的话建议先再去看一遍基础,我就假装你们都懂啦,接下来看~
实现原理
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
完整代码
注:2021.02.23从github上下载下来的源码,不同版本实现方式有些许差异。
// isUsingMicroTask : 是否将回调添加到宏任务队列
export let isUsingMicroTask = false
// 存储用户注册的回调
const callbacks = []
// 标记是否已向任务队列中添加了一个任务
let pending = false
// 将callbacks中的所有函数依次执行,然后情况列表
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 封装好的将任务添加到任务队列的函数
let timerFunc
// 支持promise就使用promise,微任务中优先级最高
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// noop是vue内部封装好的空函数
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 如果不支持promise就用 MutationObserver,它会在指定的DOM发生变化时被调用
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// 如果不支持 MutationObserver 的话就用 setImmediate,但是这个特性只有最新版IE和node支持,此时降级为宏任务
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 如果上面这些都不支持的话就用setTimeout,此时降级为宏任务
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx) // 有回调的话就执行回调
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx) // 没有回调但支持promise时,执行promise的resolve
// 这样做的原因:官方文档中说过,如果没有提供回调且在支持Promise的环境中,则返回一个Promise
}
})
// 判断任务队列中没有任务
if (!pending) {
pending = true
timerFunc() // 将任务添加到任务队列
}
// 没有回调但支持promise时,_resolve为resolve函数
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
小疑问
为什么先考虑用setImmediate,最后再是setTimeout呢?
在每次轮训检查中,各观察者的优先级分别是:idle观察者 > I/O观察者 > check观察者。
idle观察者:process.nextTick
I/O观察者:一般性的I/O回调,如网络,文件,数据库I/O等
check观察者:setTimeout>setImmediate
那不应该是setTimeout优先级高吗?!
在HTML5规定setTimeout的最小间隔时间是4ms,也就是说0实际上也会别默认设置为最小值4ms。我们把这个延迟加大到一定程度,setImmediate就会早于setTimeout执行了,因为进入macro-task 循环的时候,setTimeout的定时器还没到。
在新版的Node中,process.nextTick执行完后,会循环遍历setImmediate,将setImmediate都执行完毕后再跳出循环。 ——《深入浅出Node.js》
🌰
来个例子巩固一下!(要是错了就尴尬了= =)
<template>
<div>
<ul>
<li class="example" v-for="item in list1">{{item}}</li>
</ul>
<ul>
<li class="example" v-for="item in list2">{{item}}</li>
</ul>
<ol>
<li class="example" v-for="item in list3">{{item}}</li>
</ol>
<ol>
<li class="example" v-for="item in list4">{{item}}</li>
</ol>
<ol>
<li class="example" v-for="item in list5">{{item}}</li>
</ol>
</div>
</template>
<script type="text/javascript">
export default {
data() {
return {
list1: [],
list2: [],
list3: [],
list4: [],
list5: []
}
},
created() {
this.composeList12()
this.composeList34()
this.composeList5()
this.$nextTick(function() {
// DOM 更新了
console.log('finished test ' + new Date().toString(),document.querySelectorAll('.example').length)
})
},
methods: {
composeList12() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list1, i, 'I am a 测试信息~~啦啦啦' + i)
}
console.log('finished list1 ' + new Date().toString(),document.querySelectorAll('.example').length)
for (let i = 0; i < count; i++) {
this.$set(me.list2, i, 'I am a 测试信息~~啦啦啦' + i)
}
console.log('finished list2 ' + new Date().toString(),document.querySelectorAll('.example').length)
this.$nextTick(function() {
// DOM 更新了
console.log('finished tick1&2 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
},
composeList34() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list3, i, 'I am a 测试信息~~啦啦啦' + i)
}
console.log('finished list3 ' + new Date().toString(),document.querySelectorAll('.example').length)
this.$nextTick(function() {
// DOM 更新了
console.log('finished tick3 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
setTimeout(me.setTimeout1, 0)
},
setTimeout1() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list4, i, 'I am a 测试信息~~啦啦啦' + i)
}
console.log('finished list4 ' + new Date().toString(),document.querySelectorAll('.example').length)
me.$nextTick(function() {
// DOM 更新了
console.log('finished tick4 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
},
composeList5() {
let me = this
let count = 10000
this.$nextTick(function() {
// DOM 更新了
console.log('finished tick5-1 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
setTimeout(me.setTimeout2, 0)
},
setTimeout2() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list5, i, 'I am a 测试信息~~啦啦啦' + i)
}
console.log('finished list5 ' + new Date().toString(),document.querySelectorAll('.example').length)
me.$nextTick(function() {
// DOM 更新了
console.log('finished tick5 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
}
}
}
</script>
参考
vue官方文档: cn.vuejs.org/v2/api/#Vue…
Vue.nextTick 的原理和用途: segmentfault.com/a/119000001…