vue 中 nextTick 源码和原理
1.示例
<div class="app">
<div ref="Div">{{msg}}</div>
<div v-if="look1">Message got outside $nextTick: {{msg1}}</div>
<div v-if="look1">Message got inside $nextTick: {{msg2}}</div>
<div v-if="look1">Message got outside $nextTick: {{msg3}}</div>
<button @click="changeMsg">
Change the Message
</button>
</div>
vue实例
new Vue({
el: '.app',
data: {
msg: 'Hello Vue.',
msg1: '',
msg2: '',
msg3: ''
},
methods: {
changeMsg() {
this.msg = "Hello world."
this.msg1 = this.$refs.Div.innerHTML
this.$nextTick(() => {
this.msg2 = this.$refs.Div.innerHTML
})
this.msg3 = this.$refs.Div.innerHTML
}
}
})
2.应用场景
1.在vue的生命周期里created()钩子函数进行DOM操作的时候一定要放在Vue.nextTick()的回调函数中
3.原理
1.简单理解,Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
2.Vue 在修改数据后,视图不会立刻更新,(因为视图的更新是一个异步的过程),而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新
3.在下次DOM更新循环之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM
4.一般我是用在数据渲染完毕之后执行某些操作
this.list =xx,xx,xx
this.$nextTick(()=>{
this.isLoading=false
})
5.nextTick是宏任务 6.数据 name 被 页面引用,name 会收集到 页面的 watcher
name 被修改时,会通知所有收集到的 watcher 进行更新(watcher.update)
this.name = 2
this.name = 3
this.name = 4
如果
name 一时间被修改三次时,按道理应该会通知三次 watcher 更新,那么页面会更新三次
但是最后只会更新一次
就是因为他们的合作
设置 nextTick 回调 + 过滤 watcher
当数据变化后,把 watcher.update 函数存放进 nextTick 的 回调数组中,并且会做过滤。
通过 watcher.id 来判断 回调数组 中是否已经存在这个 watcher 的更新函数
不存在,才 push
之后 nextTick 遍历回调数组,便会执行了更新
所以
当三次修改数据的时候,会准备 push进 回调数组 三个 watcher.update,但是只有第一次是 push 成功的,其他的会被过滤掉
所以,不管你修改多少次数据,nextTick 的回调数组中只存在唯一一个 watcher.update,从而页面只会更新一次
4.源码
1.源码的三个参数
callback:我们要执行的操作,可以放在这个函数当中,我们没执行一次$nextTick就会把回调函数放到一个异步队列当中;
pending:标识,用以判断在某个事件循环中是否为第一次加入,第一次加入的时候才触发异步执行的队列挂载
timerFunc:用来触发执行回调函数,也就是Promise.then或MutationObserver或setImmediate 或setTimeout的过程
var callbacks = []; // 缓存函数的数组
var pending = false; // 是否正在执行
var timerFunc; // 保存着要执行的函数
2.nextTick的实现单独有一个JS文件来维护它,在src/core/util/next-tick.js nextick 方法
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
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 源码主要分为两块:能力检测和能力检测以不同方式执行回调队列
5.nextTick 实现
1.首先 nextTick 把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中。 这么做是因为防止单个 cb 如果执行错误不至于让整个JS 线程挂掉。 每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。
然后检查 pending 状态,这个跟之前介绍的 queueWatcher 中的 waiting 是一个意思。 它是一个标记位,一开始是 false 在进入macroTimerFunc、microTimerFunc方法前被置为 true。因此下次调用 nextTick 就不会进入macroTimerFunc、microTimerFunc方法。 这两个方法中会在下一个 macro/micro tick 时候 flushCallbacks 异步的去执行callbacks队列中收集的任务,而 flushCallbacks 方法在执行一开始会把 pending 置 false。 因此下一次调用 nextTick 时候又能开启新一轮的 macroTimerFunc、microTimerFunc,这样就形成了 vue 中的 event loop。
最后检查是否传入了 cb。因为 nextTick 还支持 Promise 化的调用:nextTick().then(() => {})。所以如果没有传入 cb 就直接return了一个Promise实例,并且把resolve传递给_resolve。这样后者执行的时候就跳到我们调用的时候传递进 then 的方法中。
2.同步方式: 当把data中的name修改之后,此时会触发name的 setter 中的 dep.notify 通知依赖本data的render watcher去 update,update 会把 flushSchedulerQueue 函数传递给 nextTick,render watcher在 flushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的 DOM 元素还是原来的内容。