相信大家在写vue项目的时候,一定会发现一个神奇的api,Vue.nextTick
。为什么说它神奇呢,那是因为在你做某些操作不生效时,将操作写在Vue.nextTick
内,就神奇的生效了。那这是什么原因呢?
让我们一起来研究一下。
简述
- vue 实现响应式并不是数据发生变化后 DOM 立即变化,而是按照一定策略异步执行 DOM 更新的* vue 在修改数据后,视图不会立刻进行更新,而是要等同一事件循环机制内所有数据变化完成后,再统一进行DOM更新*
nextTick
可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。事件循环机制
======
在讨论Vue.nextTick
之前,需要先搞清楚事件循环机制,算是实现的基石了,那我们来看一下。
在浏览器环境中,我们可以将我们的执行任务分为宏任务和微任务,
- 宏任务: 包括
整体代码script
,setTimeout
,setInterval
、setImmediate
、 I/O 操作、UI 渲染 - 微任务:
Promise.then
、MuationObserver
事件循环的顺序,决定js代码的执行顺序。事件循环如下:
用代码解释,浏览器中事件循环的顺序同如下代码:
for (macroTask of macroTaskQueue) { // 1. 执行一个宏任务handleMacroTask();// 2. 执行所有的微任务for (microTask of microTaskQueue) { handleMicroTask(microTask);}
}
vue数据驱动视图的处理(异步变化DOM)
<template><div><div>{{count}}</div><div @click="handleClick">click</div></div>
</template>
export default {data () {return {number: 0};},methods: {handleClick () {for(let i = 0; i < 10000; i++) {this.count++;}}}
}
分析上述代码:
- 当点击按钮时,count会被循环改变10000次。那么每次count+1,都会触发count的
setter
方法,然后修改真实DOM。按此逻辑,这整个过程,DOM会被更新10000次,我们都知道DOM的操作是非常昂贵的,而且这样的操作完全没有必要。所以vue内部在派发更新时做了优化 - 也就是,并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列queueWatcher里,然后在 nextTick 后执行 flushSchedulerQueue处理
- 当 count 增加 10000 次时,vue内部会先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行。并不需要在下一个 tick 的时候执行 10000 个同样的 Watcher 对象去修改界面,而是只需要执行一个 Watcher 对象,使其将界面上的 0 变成 10000 即可
Vue.nextTick
原理
由上一节我们知道,Vue中 数据变化 => DOM变化 是异步过程,一旦观察到数据变化,Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher
(Vue源码中的Wacher类是用来更新Dep类收集到的依赖的)推送进这个队列。
如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
nextTick
的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback)
,JS是单线程的,拥有事件循环机制,nextTick
的实现就是利用了事件循环的宏任务和微任务。
vue中next-tick.js的源码如下
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
// 首先定义一个 callbacks 数组用来存储 nextTick,在下一个 tick 处理这些回调函数之前,
// 所有的 cb 都会被存在这个 callbacks 数组中
const callbacks = []
// pending 是一个标记位,代表一个等待的状态
let pending = false
// 最后执行 flushCallbacks() 方法,遍历callbacks数组,依次执行里边的每个函数
function flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}
}
let timerFunc
/*判断采用哪种异步回调方式由于微任务优先级高,首先尝试微任务模拟1.首先尝试使用Promise.then(微任务)2.尝试使用MuationObserver(微任务)回调3.尝试使用 setImmediate(宏任务)回调4.最后尝试使用setTimeout(宏任务)回调*/
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]'
)) {let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks)}
} else {timerFunc = () => {setTimeout(flushCallbacks, 0)}
}
export function nextTick (cb?: Function, ctx?: Object) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = truetimerFunc()}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}
参考vue实战视频讲解:进入学习
目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。
nextTick的调用方式
1.回调函数方式:Vue.nextTick(callback)
2.Promise方式:Vue.nextTick().then(callback)
3.实例方式:vm.$nextTick(callback)
Vue.nextTick
的应用
created生命周期中操作DOM
created钩子函数执行的时候DOM 其实并未进行挂载和渲染,此时就是无法操作DOM的,我们将操作DOM的代码中放到nextTick中,等待下一轮事件循环开始,DOM就已经进行挂载好了,而与这个操作对应的就是mounted钩子函数,因为在mounted执行的时候所有的DOM挂载已完成。
created(){vm.$nextTick(() => {//不使用this.$nextTick()方法操作DOM会报错this.$refs.test.innerHTML="created中操作了DOM"});
}
修改数据,获取DOM值
当我们修改了data里的数据时,并不能立刻通过操作DOM去获取到里面的值
<template><div class="test"><p ref='msg' id="msg">{{msg}}</p></div>
</template>
<script> export default {name: 'Test',data () {return {msg:"hello world",}},methods: {changeMsg() {this.msg = "hello Vue"// vue数据改变,改变了DOM里的innerTextlet msgEle = this.$refs.msg.innerText//后续js对dom的操作console.log(msgEle)// hello world// 输出可以看到data里的数据修改后DOM并没有立即更新,后续的DOM不是最新的this.$nextTick(() => {console.log(this.$refs.msg.innerText) // hello Vue})this.$nextTick().then(() => {console.log(this.$refs.msg.innerText) // hello Vue})},changeMsg2() {this.$nextTick(() => {console.log(this.$refs.msg.innerText) // 1.hello world })this.msg = "hello Vue" // 2.console.log(this.$refs.msg.innerText) // hello worldthis.$nextTick().then(() => {console.log(this.$refs.msg.innerText) // hello Vue})// nextTick中先添加的先执行,执行1后,才会执行2(Vue操作Dom的异步)}}} </script>
v-show/v-if由隐藏变为显示
点击按钮显示原本以 v-show=false或v-if 隐藏起来的输入框,并获取焦点或者获得宽高等的场景
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享