從原始碼裡解析vue中的nextTick的用法
今天做了一個需求,場景是這樣的:
在頁面拉取一個介面,這個介面返回一些資料,這些資料是這個頁面的一個浮層元件要依賴的,然後我在介面一返回資料就展示了這個浮層元件,展示的同時,上報一些資料給後臺(這些資料就是父元件從介面拿的),這個時候,神奇的事情發生了,雖然我拿到資料了,但是浮層展現的時候,這些資料還未更新到元件上去。
父元件:
<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>
子元件
<template>
<div v-show="isShow">
......
</div>
</template>
<script>
export default {
.....
props: ['name'],
methods: {
show() {
this.isShow = true
// 上報
Report('xxx', {name: this.name})
}
}
}
</script>
問題分析:
原因vue官網上有解析
可能你還沒有注意到,Vue 非同步執行 DOM 更新。只要觀察到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料改變。如果同一個 watcher 被多次觸發,只會被推入到佇列中一次。這種在緩衝時去除重複資料對於避免不必要的計算和 DOM 操作上非常重要。然後,在下一個的事件迴圈“tick”中,Vue 重新整理佇列並執行實際 (已去重的) 工作。Vue 在內部嘗試對非同步佇列使用原生的 Promise.then 和 MessageChannel,如果執行環境不支援,會採用 setTimeout(fn, 0) 代替。
這句話就是說,當我們在父元件設定this.name=name的時候,vue並不會直接更新到子元件中(dom的更新也一樣未立即執行),而是把這些更新操作全部放入到一個隊列當中,同個元件的所有這些賦值操作,都作為一個watcher的更新操作放入這個隊列當中,然後等到事件迴圈結束的時候,一次性從這個隊列當中獲取所有的wathcer執行更新操作。在我們這個例子當中,就是我們在呼叫show的時候,實際上,我們的this.name=name並未真正執行,而是被放入佇列中。vue的這種做法是基於優化而做的,毋庸置疑,不然我們如果有n多個賦值vue就執行n多個dom更新,那效率將會非常的低效和不可取的。
解決方案:
1、 使用nextTick來延遲執行show方法(籠統得說,執行所有需要在資料真正更新後的操作
通過上面的分析我們知道,我們的所有的對vue例項的更新操作,都會先被放入一個隊列當中,延遲非同步執行,這些非同步操作,要麼是microtask,要麼是macrotask(是microtask還是macroktask取決於環境,nextTick的原始碼中有所體現),根據事件迴圈機制,先入佇列的先執行,所以如果我們在nextTick當中執行操作就會變成這樣。 2、 使用setTimeout來延遲執行show方法,原理同上
所以我們的解決方法可以是:
this.name = data.name
setTimeout(() => {
this.$refs.pop.show()
})
或者
this.name = data.name
this.$nextTick(() => {
this.$refs.pop.show()
})
前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,群內有大量PDF可供自取,更有乾貨實戰專案視訊進群免費領取。
nextTick的實現原理
其實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
})
}
}
})()
前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,群內有大量PDF可供自取,更有乾貨實戰專案視訊進群免費領取。
首先我們看到這個是利用了閉包的特性,返回queueNextTick,所以我們實際呼叫的nextTick其實就是呼叫queueNextTick,一呼叫這個方法,就會把nextTick的回撥放入佇列callbacks當中,等到合適的時機,會將callbacks中的所有回撥取出來執行,以達到延遲執行的目的。為啥要用閉包呢,我覺得有兩個原因:
1、共享變數,比如callbacks、pending和timerFunc。
2、避免反覆判斷,即是避免反覆判斷timerFunc是利用Promise還是利用MutationObserver或是setTimeout來實現非同步,這是函式柯里化的一種運用。
這裡有兩個最主要的方法需要解釋下:
1、 nextTickHandler 這個函式,就是把佇列中的回撥,全部取出來執行,類似於microtask的任務佇列。我們通過呼叫Vue.$nextTick就會把回撥全部放入這個隊列當中,等到要執行的時候,呼叫nextTickHandler全部取出來執行。
2、 timerFunc 這個變數,它的作用就是通過Promise/Mutationobserver/Settimeout把nextTickHandler放入到真正的任務隊列當中,等到事件迴圈結束,就從任務隊列當中取出nextTickHandler來執行,nextTickHandler一執行,callbacks裡面的所有回撥就會被取出來執行來,這樣就達到來延遲執行nextTick傳的回撥的效果。
通過這個簡單的原始碼分析,我們可以得出兩個結論
1、nextTick會根據不同的執行環境,非同步任務可能為microtask或者macrotask,而不是固定不變的。所以,如果你想讓nextTick裡面的非同步任務統統看成是microtask的話,你會遇到坑的。
2、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
}
})
前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,群內有大量PDF可供自取,更有乾貨實戰專案視訊進群免費領取。
結論
如果想要獲取更新後的DOM或者子元件(依賴父元件的傳值),可以在更新操作之後立即使用Vue.nextTick(callback),注意這裡的先後順序,先進行更新操作,再呼叫nextTick獲取更新後的DOM/子元件,原始碼裡面我們知道nextTick是無法保證一定是能夠獲取得到更新後的DOM/子元件的
以上所述是小編給大家介紹的vue中的nextTick的使用,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回覆大家的。在此也非常感謝大家對指令碼之家網站的支援!