前言
有人說:是爲了提高性能,對,根本上也是這麼個道理 ;那到底是如何做的呢 ?
其實在vue
中,響應式數據是組件級的,也就是說,每一次的更新都是渲染整個組件,如果是同步的話,根據我們前邊理解的響應式數據原理,一旦修改了data屬性,便會觸發對應的 watcher
,然後調用對應 watcher
下的 update
方法更新視圖,那麼結果顯而易見,太頻繁了 !如下代碼:
// 省略多餘模板語法
data () {
a:1,
b:2,
c:3
}
//如果我們按照同步的邏輯,修改data屬性,this.a = 10; this.b = 20; this.c = 30;
//就會調用三次update渲染視圖,豈不是很耗性能 ?而且體驗也不好。
所以 vue 採用的是異步渲染
接下來,我們來了解一下 ;前邊也有講過響應式數據原理,不瞭解的童鞋可以回過頭去看看Go,這裏我就接着數據更新方法update
開始;
src/croe/observer/watcher.js
166 行,這裏的更新先不考慮計算屬性和同步,我們直接看向queueWatcher
update () {
/* istanbul ignore else */
if (this.lazy) { // 計算屬性 依賴的數據發生變化了 會讓計算屬性的watcher的dirty變成true
this.dirty = true
} else if (this.sync) { // 同步watcher
this.run()
} else {
queueWatcher(this) // 將要更新的 watcher 放入隊列
}
}
src/core/observer.scheduler.js
164行,queueWatcher
方法;實現一個watcher
隊列 ,每一次的update
都放入到隊列中,然後進行統一異步處理 。 看代碼:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 過濾 watcher,多個data屬性依賴同一個watcher ,一個組件只有一個watcher
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) // 將watcher放到隊列中
} else { // 通過對 id 的判斷,這裏的 id 是自加1,可查看 watcher.js 源碼,
// 如果已經刷新了,則賦值當前的id , 如果id超過了,將運行如下代碼
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) { // 如果不等了,則進行刷新
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue() // 該方法做了刷新前的 beforUpdate 方法調用,然後 watcher.run()
return
}
nextTick(flushSchedulerQueue) // 在下一次tick中刷新 watcher 隊列 (借用nextTick)
//(包含同一watcher的多個data屬性),
// 這裏的nextTick 就是我們的常用api => this.$nextTick()
}
}
}
好了,通過源碼簡單的分析,明白爲啥 vue
爲啥採用異步更新了吧,原因很簡單,因爲vue
是組件級更新視圖,每一次update
都要渲染整個組件,爲了提高性能,採用了隊列的形式,存儲同一個watcher
的所有data屬性變化,然後統一調用nextTick
方法進行更新渲染(有且只調用一次)。
問題來了,nextTick
方法是異步的 ,那麼它又是如何實現的異步更新呢 ?來看張圖
從圖來看,調用了 nextTick
之後,將watcher
隊列回調函數暫時存入了一個數組callbacks
中,然後才依次調用 timeFun()
方法執行,而真正讓watcher
異步的關鍵就在這兒,我們通過代碼來看一下:
首先進入 nextTick
函數 src/core/util/next-tick.js
87 行
export function nextTick (cb?: Function, ctx?: Object) {
// flushSchedulerQueue 會使用 nextTick 保證當前視圖渲染完成
let _resolve
callbacks.push(() => { // 暫存 watcher 隊列
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { // 狀態改變後,調用 timerFun() 方法
pending = true
timerFunc() // 重點,重點,重點! 我們進去看一下
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc
方法 src/core/util/next-tick.js
33 行 ,看如下代碼,會Js的童鞋應該就可以看出來是什麼東西;它對當前的環境進行了判斷,如果支持promise
就用 promise
依次往下: MutationObserver , setImmediate , setTimeout
這四個分別都是異步解決方案,除了 setTimeout
是宏觀任務以外,其它三個都是微觀任務;
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks) // 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]'
)) {
// 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) // setImmediate 回調裏邊
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0) // setTimeout 回調裏邊
}
}
總結
nextTick 方法主要是使用了宏任務和微任務,定義了一個異步方法.多次調用 nextTick 會將方法存入
隊列中,通過這個異步方法清空當前隊列。 所以這個 nextTick 方法就是異步方法 。
而我們平常使用的api :vue.nextTick()
也是如此 .