置信大家在写 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 暗藏起来的输入框,并获取焦点或者取得宽低等的场景