前端边角料 | 聊聊 Vue 的 $nextTick 原理__Vue.js
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利
关键词:
Vue
$nextTick
异步更新队列
微任务
宏任务
EventLoop
目录
什么是 $nextTick $nextTick 的用途 源码解析 杂谈 参考资料
什么是 $nextTick
官方文档说明:https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
$nextTick 的用途
需要在视图更新之后,基于新的视图进行操作 在 created 周期中使用,因为在 created() 钩子函数执行的时候 DOM 其实并未进行任何渲染,其实相当于 mounted() 钩子函数的应用 用法一:
<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
})
在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
}
})
用法二,因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}
源码解析
理解 flushCallbacks
// 同步执行回调函数队列
function flushCallbacks () {
// 设置当前执行状态为非等待状态,即执行回调队列中
pending = false
// 浅拷贝回调队列数组
const copies = callbacks.slice(0)
// 重置回调队列,等价于 callbacks = []
callbacks.length = 0
// 循环执行回调函数
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
理解 timerFunc
// 根据不同的平台(浏览器、APP的 WebView),
// 不同的版本(IOS版本,浏览器版本),
// 分别设置优先级由 Promise -> MutationObserver -> setImmediate -> setTimeout 的方法赋值给 timerFunc
// 目的是在执行 timerFunc 时将其设置为 微任务 -> 宏任务,应用于浏览器的 EventLoop 之中
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} 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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
理解 nextTick
// 导出供 Vue 内部,及 vm / Vue 外部调用的 nextTick 方法
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将传入的回调函数放入回调队列中
callbacks.push(() => {
if (cb) {
// 防止由某一个回调函数报错导致整个 JS 线程挂掉的情况,使用了 try ... catch
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果当前的 nextTick 并没有闲置,则设置其为非闲置,并开始执行回调函数队列
if (!pending) {
pending = true
timerFunc()
}
// 如果没有传入回调函数,则将 nextTick 当做一个 Promise 返回
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
杂谈
以下均为个人观点
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用 nextTick 的核心利用了如 Promise 、 MutationObserver 、 setImmediate 、 setTimeout 的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列 nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 Dom 更新数据时机的后续逻辑处理 nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例 引入异步更新队列机制的原因: 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/Dom 的渲染,可以减少一些无用渲染 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要
参考资料
本文使用 mdnice 排版
浏览知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: hauk0101 原文链接:https://juejin.im/post/6856288219584331790