關於數據響應化,問一個常見的問題:html
下面示例代碼中的兩個輸出console.log(p1.innerHTML)
,分別是什麼?爲何?html5
<!DOCTYPE html>
<html>
<body>
<div id="demo">
<h1>異步更新</h1>
<p id="p1">{{foo}}</p>
</div>
<script>
const app = new Vue({
el: '#demo',
data: { foo: '' },
mounted() {
setInterval(() => {
this.foo = 't1'
this.foo = 't2'
this.foo = 't3'
console.log(p1.innerHTML) //此時,頁面的展現值?
this.$nextTick(() => {
console.log(p1.innerHTML) //此時,頁面的展現值?
})
}, 1000);
}
});
</script>
</body>
</html>
複製代碼
這個問題的第一問「是什麼」,並不複雜。難的是"爲何"。該問題的本質涉及到 Vue 的異步更新問題。算法
首先,須要明確的是:Vue 的更新 DOM 的操做是異步的,批量的。之因此這麼作的原因也很簡單:更新 DOM 的操做是昂貴的,消耗較大。如上面的展現例子所示,Vue 內部會連續更新三次 DOM 麼?那顯然是不合理的。批量、異步的操做才更優雅。express
咱們想要去源碼看看,Vue 更新 DOM 的批量與異步操做,究竟是如何作的呢?bash
首先界定一個界限:咱們不會立馬深刻到虛擬 DOM 的生成與頁面更新的 patch 算法中去,只是想要看看這個批量與異步的過程,解決剛剛提到的問題。app
從以前的筆記內容可知:數據響應的核心方法defineReactive()
中,當數據發生變化的時候,會調用Dep.notify()
方法,通知對應的Watcher執行updateComponent()
操做,繼而從新渲染執行更新頁面。異步
讓咱們從Dep的notify()
方法提及。async
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
...//省略
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
複製代碼
可知,其內部是執行的是相關聯的 Watcher 的update()
方法。函數
import { queueWatcher } from './scheduler'
export default class Watcher {
...//省略
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {//若是是同步
this.run()
} else {
queueWatcher(this) //Watcher的入隊操做
}
}
//實際執行的更新方法,會被scheduler調用
run () {
if (this.active) {
//this.get()是掛載時傳入的updateComponent()方法
const value = this.get()
//若是是組件的Watcher,不會有返回值value,不會執行下一步
//只有用戶自定義Watcher纔會進入if
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
複製代碼
看到這裏,提問一哈:若是在同一時刻,組件實例中的 data 修改了屢次,其對應的 Watcher 也會執行queueWatcher(this)
屢次,那麼是否會在當前隊列中存在多個一樣的Watcher呢?oop
帶着這個問題,查看同一文件夾下schedule.js
的queueWatcher()
方法:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
//去重
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
//異步刷新隊列
nextTick(flushSchedulerQueue)
}
}
}
複製代碼
代碼中看到:每一個 Watcher 都會有一個 id 標識,只有全新的 Watcher 纔會入隊。批量的過程咱們看到了,將是將 Watcher 放入到隊列裏面去,而後批量操做更新。
看了這個批量更新的操做,有人會問:屢次數據響應化,只有第一次更新的 Watcher 纔會進入隊列,是否是意味着只有第一次的數據響應化才生效,然後幾回的數據響應化無效了呢?
回答:並非這樣的,數據響應化一直都在進行,變化的數據也一直在變。須要明確其和批量更新隊列之間的關聯,發生在 Watcher 的 run() 方法上。當執行 run() 方法的時候,其獲取的 data 是最新的 data。
講了批量,那麼異步的過程是怎樣的呢?讓咱們來看看nextTick()
函數內部,瞭解一些關於異步操做的知識點:
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
//關於timerFunc的選取過程
let timerFunc
//優先選擇Promise,由於Promise是基於微任務的
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
//次優選擇MutationObserver,MutationObserver也是基於微任務的
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
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
//若是以上二者都不行,那麼選擇setImmediate(),它是基於宏任務的
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最無奈的選擇,選擇setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
//nextTick: 按照特定異步策略timerFunc() 執行隊列操做
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()
}
}
複製代碼
關於宏任務與微任務,能夠查看更多有意思的頁面: