专业网站制作案例,免费的外网连接器,平面设计素材网,有没有做公司网站的目录 前言一、Vue异步更新队列二、nextTick 用法三、原理分析四、nextTick 源码解析1#xff09;环境判断2#xff09;nextTick() 五、补充 前言
在我们使用Vue的过程中#xff0c;基本大部分的 watcher 更新都需要经过 异步更新 的处理。而 nextTick 则是异步更新的核心。… 目录 前言一、Vue异步更新队列二、nextTick 用法三、原理分析四、nextTick 源码解析1环境判断2nextTick() 五、补充 前言
在我们使用Vue的过程中基本大部分的 watcher 更新都需要经过 异步更新 的处理。而 nextTick 则是异步更新的核心。
官方对其的定义: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法获取更新后的 DOM。 一、Vue异步更新队列
Vue 可以做到 数据驱动视图更新我们简单写一个案例实现下
templateh1 styletext-align:center clickhandleCount{{ value }}/h1
/templatescript
export default {data () {return {value: 0}},methods: {handleCount () {for (let i 0; i 10; i) {this.value iconsole.log(this.value)}}}
}
/scriptvue异步更新dom 当我们触发这个事件视图中的 value 肯定会发生一些变化。 这里可以思考下Vue是如何管理这个变化的过程呢比如上面这个案例value 被循环了10次那 Vue 会去渲染dom视图10次吗显然是不会的毕竟这个性能代价太大了。其实我们只需要 value 最后一次的赋值。
实际上 Vue 是 异步更新 视图的也就是说等 handleCount() 事件执行完检查发现只需要更新 value然后再一次性更新数据和Dom避免无效更新。
总之Vue 的 数据更新 和 DOM更新 都是异步的Vue 会将数据变更添加到队列中在下一个事件循环中进行批量更新然后异步地将变更应用于实际的 DOM 元素以保持视图与数据的同步。
Vue官方文档也印证了我们的想法如下 Vue 在更新 DOM 时是 异步 执行的。只要侦听到 数据变化 Vue 将开启一个队列并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后在下一个的事件循环“tick”中Vue 刷新队列并执行实际 (已去重的) 工作。 详细可见Vue官方文档 - 异步更新队列
二、nextTick 用法
看例子比如当 DOM 内容改变后我们需要获取最新的元素高度。
templatediv{{ name }}/div
/templatescript
export default {data () {return {name: }},methods: {},mounted () {console.log(this.$el.clientHeight)this.name 铁锤妹妹console.log(this.name, name)console.log(this.$el.clientHeight)this.$nextTick(() {console.log(this.$el.clientHeight)})}
}
/script从打印结果可以看出name数据虽然更新了但是前两次元素高度都是0只有在 nextTick 中才能拿到更新后的 Dom 值具体是什么原因呢下面就分析下它的原理吧。
这个实例也可参考学习watch监听和$nextTick结合使用处理数据渲染完成后的操作方法
三、原理分析
在执行 this.name 铁锤妹妹 的时候就会触发 Watcher 更新watcher 会把自己放入一个队列。
// src/core/observer/watcher.ts
update () {if (this.lazy) {// 如果是计算属性this.dirty true} else if (this.sync) {// 如果要同步更新this.run()} else {// 将 Watcher 对象添加到调度器队列中以便在适当的时机执行其更新操作。queueWatcher(this)}
}用队列的原因是比如多个数据变更直接更新视图多次的话性能就会降低所以对视图更新做一个异步更新的队列避免不必要的计算和 DOM 操作。在下一轮事件循环的时候刷新队列并执行已去重的工作nextTick的回调函数组件重新渲染更新视图。 然后调用 nextTick() 响应式派发更新的源码如下
// src/core/observer/scheduler.tsexport function queueWatcher(watcher: Watcher) {// ...// 因为每次派发更新都会引起渲染所以把所有 watcher 都放到 nextTick 里调用nextTick(flushSchedulerQueue)
}function flushSchedulerQueue () {queue.sort(sortCompareFn)for (index 0; index queue.length; index) {watcher queue[index]watcher.run()// ...省略细节代码}
}这里参数 flushSchedulerQueue 方法就会被放入事件循环主线程任务执行完之后就会执行这个函数对 watcher 队列排序、遍历、执行 watcher 对应的 run() 方法然后render更新视图。
也就是说 this.name 铁锤妹妹 的时候任务队列简单理解成这样 [flushSchedulerQueue] 。
下一行 console.log(this.name, name) 检验下 name 数据是否更新。
然后下一行 console.log(this.$el.clientHeight) 由于更新视图任务 flushSchedulerQueue 在任务队列中还没有执行所以无法拿到更新后的视图。
然后执行 this.$nextTick(fn) 时候添加一个异步任务这时任务队列简单理解成这样 [flushSchedulerQueue, fn] 。
然后 同步任务 都执行完毕接着按顺序执行任务队列中的 异步任务。第一个任务执行就会更新视图后面自然能得到更新后的视图了。
四、nextTick 源码解析
1环境判断
主要判断用哪个宏任务或者微任务因为宏任务耗费时间大于微任务所以优先使用 微任务判断顺序如下 Promise 》 MutationObserver 》 setImmediate 》 setTimeout
// src/core/util/next-tick.tsexport let isUsingMicroTask false // 是否启用微任务开关const callbacks: ArrayFunction [] //回调队列
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]()}
}// timerFunc就是nextTick传进来的回调等... 细节不展开
let timerFunc
// 判断当前环境是否支持原生 Promise
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) ||// PhantomJS and iOS 7.xMutationObserver.toString() [object MutationObserverConstructor])
) {// 当原生 Promise 不可用时timerFunc 使用原生 MutationObserver// MutationObserver不要在意它的功能其实就是个可以达到微任务效果的备胎let counter 1const observer new MutationObserver(flushCallbacks)const textNode document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc () {// 使用 MutationObservercounter (counter 1) % 2textNode.data String(counter)}isUsingMicroTask true
} else if (typeof setImmediate ! undefined isNative(setImmediate)) {// 使用setImmediate虽然也是宏任务但是比setTimeout更好timerFunc () {setImmediate(flushCallbacks)}
} else {// 最后的倔强timerFunc 使用 setTimeouttimerFunc () {setTimeout(flushCallbacks, 0)}
}然后进入核心的 nextTick。
2nextTick()
这里代码不多主要逻辑就是
把传入的回调函数放进回调队列 callbacks。执行保存的异步任务 timeFunc就会遍历 callbacks 执行相应的回调函数了。
export function nextTick(cb?: (...args: any[]) any, ctx?: object) {let _resolve// 把回调函数放入回调队列callbacks.push(() {if (cb) {try {cb.call(ctx)} catch (e: any) {handleError(e, ctx, nextTick)}} else if (_resolve) {_resolve(ctx)}})if (!pending) {// 如果异步开关是开的就关上表示正在执行回调函数然后执行回调函数pending truetimerFunc()}// 如果没有提供回调并且支持 Promise就返回一个 Promiseif (!cb typeof Promise ! undefined) {return new Promise(resolve {_resolve resolve})}
}可以看到最后有返回一个 Promise 是可以让我们在不传参的时候用的如下
this.$nextTick().then((){ ... })五、补充 在 vue 生命周期中如果在 created() 钩子进行 DOM 操作也一定要放在 nextTick() 的回调函数中。因为在 created() 钩子函数中页面的 DOM 还 未渲染这时候也没办法操作 DOM所以此时如果想要操作 DOM必须将操作的代码放在 nextTick() 的回调函数中 本文到此也就结束了希望对大家有所帮助。