本文从nextTick API 的概念到使用,再到源码,层层剖析。系统地回顾nextTick的相关用法,以及内部调用逻辑。
概念
官方解释:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
大白话:nextTick是vue中的批量异步更新策略。监听到组件的变化时,不会立即更新,会开启一个队列,同一watcher只入队一次。在下一次事件循环时,刷新队列执行更新。
原理
使用nextTick接收传入的回调函数,将回调函数暂时存放到一个队列中,开启异步更新(调用timerfuc函数)。在timerfuc中会优先使用Promise微任务去执行队列中的所有回调,在下一次事件循环时,更新页面。
在timerfuc中还存在一个降级的过程,是考虑浏览器的兼容设置的。 降级顺序: Promise => MutationObserver => setImmediate => setTimeout
可以看到nextTick其实是利用了浏览器的事件循环机制。会将nextTick中的任务存放到微任务队列中去(如果浏览器兼容微任务)。
当函数执行栈为空时,会去判断微任务队列中是否存在微任务,如果存在就会先去执行微任务队列中的任务(可以执行nextTick中的任务),然后再去执行宏任务。
关于JS运行机制的细节可以点击 JS运行机制 这篇文章。
如何工作
使用
nextTick会接收两个传入对象,一个是用户的回调,还有一个是上下文对象。在组件内,用户使用的时候,this
已经自动绑定到当前的 Vue 实例上,所有就不需要全局的Vue去调用 Vue.nextTick(cb)
。
<div id="app"> <h1>异步更新</h1> <p id="p1">{{count}}</p> </div> <script> const vm = new Vue({ el: '#app', data: { count: '原始值' }, mounted() { this.count = Math.random() console.log('第一次改动值:', this.count) this.$nextTick(() => { console.log('innerHTML', p1.innerHTML); }) }, }) </script>
我们可以看到在页面加载之后,count成功获取到了第一次改动的值,并在页面上更新。
源码剖析
那么,页面它是怎么渲染的呢?
老样子,我们通过控制台打断点来看看源码内部到底发生了什么吧 ^_^ !
首先从this.count
的赋值,因为变量是通过this获取的,并不是使用this内部的$data,所以会走到变量的代理proxy
中,然后进入了defineRective
中的set拦截。
在set拦截中,回去通知dep做更新操作。
在notify
中执行watcher
的更新函数update。在此之前,我们可以看出,存在一个watcher的排序,源码中将先创建的更高级的watcher放到前面,先执行。
通过遍历执行update,尝试将传入的watcher实例入队,启动异步任务。
在nextTick中添加一个flushSchedulerQueue
回调。并将其放到callbacks
数组中。
启动异步任务timerfuc
:
在异步任务中,执行flushCallbacks
。
是遍历执行callbacks
数组就相当于在执行之前的flushSchedulerQueue
回调。
而在flushSchedulerQueue
回调中,是遍历所有的watchers,执行他们的run函数。
在watcher的run函数中,会执行自身的get。真正会走到组件的新updateComponent
。在updateComponent
内部,会先执行render()得到虚拟dom,执行_update()接收vnode,再执行patch(oldvnode,vnode),真实dom变更。
大家可以通过看这张nextTick流程图,再配合控制台流程进行解读。
应用
1、获取dom更新后最新数值。 根据本文案例,我们将count的修改n遍后,想获取dom更新后最新的count值时:
mounted() { this.count = Math.random() console.log('第一次改动值:', this.count) this.count = Math.random() console.log('第二次改动值:', this.count) this.count = Math.random() console.log('第三次改动值:', this.count) this.$nextTick(() => { console.log('innerHTML', p1.innerHTML); }) },
注意点
- 当我将
nextTick
放到最前面时,将会获取不到最新值,值会是原始值。
在nextTick
时,想要的变量还未进入到异步更新的队列中。在异步更新队列中nextTick
先进入,然后才是相关变量进入队列当中,所以nextTick
输出的会是原始值。
- 放到第一次或第二次修改值后面就不会发生变化。
这是因为相关watcher只入队一次。在第一次修改值的时候,count变量就会进入异步更新队列中,然后nextTick
才会进入。根据队列特性,会先执行count变量的修改,再执行nextTick
。所以仍然可以获取到count的最新值。
- 如果前面有变量修改,
nextTick
会先于Promise
执行。
mounted() { this.count = Math.random() console.log('第一次改动值:', this.count) Promise.resolve().then((res) => { console.log('Promise', p1.innerHTML); }) this.$nextTick(() => { console.log('innerHTML', p1.innerHTML); }) this.count = Math.random() console.log('第二次改动值:', this.count) this.count = Math.random() console.log('第三次改动值:', this.count) },
在修改相关值时,已经触发异步更新队列,会将相关变量先存放到队列当中,然后再去执行Promise
,将其放入到异步队列中。
2、点击获取元素的宽高、滚动位置等