Vue异步更新队列nextTick原理
异步更新队列
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
})
再如:
for(let i=0;i<n;i++){ this.someData+=i }
如上面的例子,如果我们多次修改someDate
的值,并不是每次修改的结果都会渲染到页面上。因为我们只需要看到最终的值。vue是在执行完一次事件循环后把缓冲的数据更改更新到页面上
vm.$nextTick(callback)
,在下一次dom更新之后执行对应的回调,这样我们获取到的dom就是更新过的dom
下面了解下nextTick
的主要应用的场景及原因。
- 在Vue生命周期的
created()
钩子函数进行的DOM操作一定要放在Vue.nextTick()
的回调函数中
在created()
钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()
的回调函数中。
- 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进
Vue.nextTick()
的回调函数中。
msg1和msg3显示的内容还是变换之前的,而msg2显示的内容是变换之后的。其根本原因是因为Vue中DOM更新是异步的
获取this.Message的两种情况:
在nextick中可以获取更新后的dom
nextTick实现原理:
环境判断:
判断支持哪个宏任务或微任务,顺序如下
Promise
MutationObserver
setImmediate
setTimeout
export let isUsingMicroTask = false // 是否启用微任务开关
const callbacks = [] // 回调队列
let pending = false // 异步控制开关,标记是否正在执行回调函数
// 该方法负责执行队列中的全部回调
function flushCallbacks () {
// 重置异步开关
pending = false
// 防止nextTick里有nextTick出现的问题
// 所以执行之前先备份并清空回调队列
const copies = callbacks.slice(0)
callbacks.length = 0
// 执行任务队列
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 用来保存调用异步任务方法
// 判断当前环境是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 保存一个异步任务
const p = Promise.resolve()
timerFunc = () => {
// 执行回调函数
p.then(flushCallbacks)
// ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
// 所以用一个空的计时器来强制刷新任务队列
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 不支持 Promise 的话,在支持MutationObserver的非 IE 环境下
// 如 PhantomJS, iOS7, Android 4.4
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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 以上都不支持的情况下,使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
用timerfunc保存支持的宏任务或微任务,调用此函数会执行并清空任务回调队列callbacks
执行清空回调队列,
nextTick()
把传入的回调函数放进callbacks中
执行timerfunc,就会遍历callbacks队列执行相应的回调函数
/*存放异步执行的回调*/
const callbacks = []
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc
/*
推送到队列中下一个tick时执行
cb 回调函数
ctx 上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 第一步 传入的cb会被push进callbacks中存放起来
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁
if (!pending) {
// 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上)
pending = true
// 调用判断Promise,MutationObserver,setTimeout的优先级
timerFunc()
}
// 第三步执行返回的状态
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
原理总结:
环境判断,判断支持哪个宏微任务,promise、mutationobserver、settimeout等,把timerfunc执行函数放到支持的宏或微任务中,
把nexttick传入的回调函数放进callbacks回调队列中,用pending检查上一个任务队列是否执行完毕,执行完毕则调用timerfunc执行函数,来遍历执行callbacks