vue的DOM更新時異步執行的,只要監聽到數據變化,Vue將開啓一個隊列,並緩存在同一事件循環中發生的全部數據變動,若是同一個Watcher被屢次觸發,只會被推入到隊列中一次,避免了沒必要要的重複計算和頻繁的DOM操做,而後在下一個事件循環"tick"中(注意下一個tick多是當前的tick微任務執行階段執行,也可能在下一個tick執行,主要取決於nextTick函數使用的是Promise/MutationObserver仍是setTimeout),Vue刷新隊列並執行更新試圖等操做.前端
例如, 當你設置vm.somData = 'new value',該組件不會當即從新渲染,當刷新組件時,組件會在下一個事件循環的"tick"中更新.雖然大多數狀況下,咱們並不須要關心這個過程,可是若是咱們想在數據改變以後進行獲取更新後的DOM,咱們就須要調用Vue.nextTick(callback),這樣回調函數會在DOM更新完成後調用.vue
<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更新數據
console.log(vm.$el.textContent) // '123'
Vue.nextTick(function() {
console.log(vm.$el.textContent) // 'new message'
})
複製代碼
這就很像家裏有一個熊孩子今天要開party,可想而知會把家裏弄得亂七八糟,烏煙瘴氣,你要負責過後打掃戰場,可是你要是弄亂一點去收拾一點,就很浪費精力,得不償失.因此正確的方式應該是任由他折騰,等戰鬥結束徹底結束後,再去清洗和整理.node
閱讀過vue源碼的都知道,當某個響應式數據發生變化的時候,它的setter函數就會通知閉包中的Dep,Dep則會觸發對應的Watcher對象的update方法,咱們來看一下update的實現:git
update() {
if(this.lazy) {
this.dirty = true
} else if(this.sync) {
/*同步執行則run直接渲染視圖*/
this.run()
} else {
/*異步則推送到觀察者隊列中,下一個tick時調用*/
queueWatcher(this)
}
}
// queueWatcher函數
// 將觀察者對象push進隊列,並記錄觀察者的id
// 若是對應的觀察者已存在,則跳過,避免重複的計算
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if(!has[id]) {
has[id] = true
if(!flushing) {
/*若是沒有被flush掉,直接push到隊列中便可*/
queue.push(watcher)
} else {
//...
}
// queue the flush
if(!wating) {
wating = true
nextTick(flushSchedulerQueue)
}
}
}
複製代碼
經過查看源碼咱們發現,watcher的update操做都被存入一個隊列queue了,等到下一個tick運行時,這些watcher會被遍歷執行,更新視圖.es6
那麼, 什麼是下一個tick?github
想要知道什麼是下一個tick,咱們先要了解下Event Loop(事件循環).js執行時單線程的,它是基於事件循環的,事件循環機制控制着js全部任務的有序執行,js中的任務分爲同步任務和異步任務,事件循環大體分爲如下步驟:web
- 全部同步任務在主線程上執行,造成一個執行棧.
- 主線程以外,還有一個任務隊列,這個隊列用於存放異步任務, 只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件.
- 執行棧上的同步任務執行完畢後,主線程會讀取任務隊列中的任務執行,對應的異步任務結束等待狀態,進入執行棧,開始執行.
- 主線程不斷重複以上操做,造成事件循環.
for(let i =0; i < 100; i++) {
dom.style.left = i + 'px'
}
複製代碼
瀏覽器會進行100次dom更新嗎?顯然這樣太損耗性能了,事實上這100次for循環同屬一個task,瀏覽器只會在改task執行完後進行一次DOM更新.這也就意味着,只要讓nextTick中的回調放在UI Render後執行,就能夠訪問到更新後的DOM了.這樣咱們很天然的想到把這些回調邏輯放入任務隊列中去執行.vue-router
主線程的執行過程就是一個tick,全部的異步結果都是經過"任務隊列"來調度,可想而知Vue中的DOM的異步更新任務也是存放在任務隊列中的,下面咱們就來看看nextTick的具體實現邏輯.segmentfault
js中的任務隊列分爲宏任務(macrotask)隊列和微任務(microtask )隊列,每次事件循環結束後,都會先清空微任務隊列中的微任務,而後纔會開始執行下一個宏任務,微任務比宏任務有着更高的優先級.(注: 瀏覽器和NodeJs的事件循環的執行邏輯不同,這裏咱們只研究瀏覽器中事件循環的執行邏輯,想要了解nodejs中的執行邏輯,可參考: segmentfault.com/a/119000001….)api
因此事實上,咱們調用nextTick的時候,就是在更新DOM那個microtask後執行了咱們傳入的回調函數,從而確保咱們的代碼在DOM更新後執行
nextTick的源碼, 建議你們對照着源碼來閱讀接下來的內容.
vue是如何監聽到DOM更新完畢,並執行咱們傳入的回調函數呢? HTML5新增了一個屬性MutationObserver,用於監聽DOM修改事件,可以監聽到節點的屬性,文本內容,子節點等的改動,是一個功能強大的利器,基本用法以下:
// MO基本用法
var observer = new MutationObserver(function() {
// 這裏是回調函數
console.log("DOM 被修改了!");
});
var article = document.querySelector('article');
observer.observer(article); // 監聽dom改變後執行回調
複製代碼
那麼vue是否是用MO來監聽DOM更新完畢的呢? 打開vue的源碼,確實看到這樣的代碼:
// MutationObserver 有更普遍的支持,但在iOS上的觸摸事件處理程序中存在bug
// 因此咱們優先採用原生的promise.來建立微任務
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]'
)) {
// Promise是es6新增的api,在不支持原生Promise的瀏覽器中,咱們採用HTML5的新屬性MutationObserver來監聽DOM更新
// 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)) {
// setImmediate, 目前只有IE和Node.js支持
// 技術上它是利用宏任務隊列,
// 可是它還是比setTimeout更好的選擇
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// setTimeout是以上方案都不支持的最後的選擇
// 儘管它有執行延遲,可能形成屢次渲染
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 暴露出nextTick方法,控制在下一個tick中執行傳入的回調
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
})
}
}
複製代碼
關於Vue中DOM的異步更新以及Vue.nextTick的原理解析就說到這兒了,後續會推出vue-router的源碼解析,持續關注奧~若是你有什麼建議,困惑或想法,歡迎留言或者加微信lj_de_wei_xin
與我交流~