置信大家在写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的源码如下
参考vue实战视频解说:进入学习
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 = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (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.x
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)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
目前浏览器平台并没有实现 nextTick 办法,所以 Vue.js 源码中别离用 Promise、setTimeout、setImmediate 等形式在 microtask(或是task)中创立一个事件,目标是在以后调用栈执行结束当前(不肯定立刻)才会去执行这个事件。
nextTick的调用形式
- 回调函数形式:
Vue.nextTick(callback)
- Promise形式:
Vue.nextTick().then(callback)
- 实例形式:
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里的innerText
let 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 world
this.$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 暗藏起来的输入框,并获取焦点或者取得宽低等的场景
发表回复