今天作了一個需求,場景是這樣的:html
在頁面拉取一個接口,這個接口返回一些數據,這些數據是這個頁面的一個浮層組件要依賴的,而後我在接口一返回數據就展現了這個浮層組件,展現的同時,上報一些數據給後臺(這些數據就是父組件從接口拿的),這個時候,神奇的事情發生了,雖然我拿到數據了,可是浮層展示的時候,這些數據還未更新到組件上去。vue
父組件:react
<template>
.....
<pop ref="pop" :name="name"/>
</template>
<script>
export default {
.....
created() {
....
// 請求數據,並從接口獲取數據
Data.get({
url: xxxx,
success: (data) => {
// 問題出如今這裏,咱們賦值之後直接調用show方法,去展示,show方法調用的同時上報數據,而上報的數據這個時候還未更新到子組件
this.name = data.name
this.$refs.pop.show()
}
})
}
}
</script>
複製代碼
子組件bash
<template>
<div v-show="isShow">
......
</div>
</template>
<script>
export default {
.....
props: ['name'],
methods: {
show() {
this.isShow = true
// 上報
Report('xxx', {name: this.name})
}
}
}
</script>
複製代碼
緣由vue官網上有解析(cn.vuejs.org/v2/guide/re…)閉包
可能你尚未注意到,Vue 異步執行 DOM 更新。只要觀察到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據改變。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做上很是重要。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel,若是執行環境不支持,會採用 setTimeout(fn, 0) 代替。app
這句話就是說,當咱們在父組件設置this.name=name的時候,vue並不會直接更新到子組件中(dom的更新也同樣未當即執行),而是把這些更新操做所有放入到一個隊列當中,同個組件的全部這些賦值操做,都做爲一個watcher的更新操做放入這個隊列當中,而後等到事件循環結束的時候,一次性從這個隊列當中獲取全部的wathcer執行更新操做。在咱們這個例子當中,就是咱們在調用show的時候,實際上,咱們的this.name=name並未真正執行,而是被放入隊列中。vue的這種作法是基於優化而作的,毋庸置疑,否則咱們若是有n多個賦值vue就執行n多個dom更新,那效率將會很是的低效和不可取的。dom
下文中的更新操做指對data的值進行更新的操做,在vue中,都會被放入隊列異步執行。異步
一、使用nextTick來延遲執行show方法(籠統得說,執行全部須要在數據真正更新後的操做
經過上面的分析咱們知道,咱們的全部的對vue實例的更新操做,都會先被放入一個隊列當中,延遲異步執行,這些異步操做,要麼是microtask,要麼是macrotask(是microtask仍是macroktask取決於環境,nextTick的源碼中有所體現),根據事件循環機制,先入隊列的先執行,因此若是咱們在nextTick當中執行操做就會變成這樣。 async
二、使用setTimeout來延遲執行show方法,原理同上ide
因此咱們的解決方法能夠是:
this.name = data.name
setTimeout(() => {
this.$refs.pop.show()
})
複製代碼
或者
this.name = data.name
this.$nextTick(() => {
this.$refs.pop.show()
})
複製代碼
其實nextTick的實現原理是挺簡單的,簡單點說,就是實現異步,經過不一樣的執行環境,用不一樣的方式來實現,保證nextTick裏面的回調函數可以異步執行。爲何要這麼作呢?由於vue對dom的更新也是異步的呀。
下面貼出源碼:
/**
* Defer a task to execute it asynchronously.
*/
export const nextTick = (function () {
const callbacks = []
let pending = false
let timerFunc
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// the nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore if */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
// 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)
}
} 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
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick (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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
複製代碼
首先咱們看到這個是利用了閉包的特性,返回queueNextTick,因此咱們實際調用的nextTick其實就是調用queueNextTick,一調用這個方法,就會把nextTick的回調放入隊列callbacks當中,等到合適的時機,會將callbacks中的全部回調取出來執行,以達到延遲執行的目的。爲啥要用閉包呢,我以爲有兩個緣由:
一、共享變量,好比callbacks、pending和timerFunc。
二、避免反覆判斷,便是避免反覆判斷timerFunc是利用Promise仍是利用MutationObserver或是setTimeout來實現異步,這是當即執行函數的一種運用。
這裏有兩個最主要的方法須要解釋下:
一、nextTickHandler
這個函數,就是把隊列中的回調,所有取出來執行,相似於microtask的任務隊列。咱們經過調用Vue.$nextTick就會把回調所有放入這個隊列當中,等到要執行的時候,調用nextTickHandler所有取出來執行。
二、timerFunc
這個變量,一執行就會經過Promise/Mutationobserver/Settimeout實現異步,把nextTickHandler放入到真正的異步任務隊列當中,等到事件循環結束,就從任務隊列當中取出nextTickHandler來執行,nextTickHandler一執行,callbacks裏面的全部回調就會被取出來執行來,這樣就達到來延遲執行nextTick傳的回調的效果。
經過這個簡單的源碼分析,咱們能夠得出兩個結論
一、nextTick會根據不一樣的執行環境,異步任務可能爲microtask或者macrotask,而不是固定不變的。因此,若是你想讓nextTick裏面的異步任務通通當作是microtask的話,你會遇到坑的。
二、nextTick的並不能保證必定能獲取獲得更新後的dom,這取決於你是先進行數據賦值仍是先調用nextTick。好比:
new Vue({
el: '#app',
data() {
return {
id: 2
}
},
created() {
},
mounted() {
this.$nextTick(() => {
console.log(document.getElementById('id').textContent) // 這裏打印出來的是2,由於先調用了nextTick
})
this.id = 3
}
})
複製代碼
若是想要獲取更新後的DOM或者子組件(依賴父組件的傳值),能夠在更新操做以後當即使用Vue.nextTick(callback),注意這裏的前後順序,先進行更新操做,再調用nextTick獲取更新後的DOM/子組件,源碼裏面咱們知道nextTick是沒法保證必定是可以獲取獲得更新後的DOM/子組件的