Vue的批量更新原理

簡(jiǎn)單的代碼開(kāi)始:
var app = new Vue({el: '#app',data: {message: 'Hello Vue!'},watch: {message (val) {console.log('mutation')}},mounted () {this.message = 1this.message = 2this.message = 3}})
首先看這個(gè)例子中,連續(xù)3次觸發(fā)了mutation,那么watch中的cb會(huì)被執(zhí)行幾次呢?
答案是一次。
那么為什么會(huì)是一次呢?本文會(huì)圍繞著這個(gè)問(wèn)題的解釋來(lái)粗淺地討論一下Vue中批量更新的原理。
首先要知道,msg這個(gè)key,是通過(guò)Object.defineProperty被監(jiān)聽(tīng)了的,Vue通過(guò)這個(gè)api實(shí)現(xiàn)在key被set的時(shí)候(也就是this.msg = xxx這種操作),觸發(fā)所有訂閱了這個(gè)key的Watcher的update方法。
這里引入了一個(gè)Watcher的概念,那么這個(gè)Watcher是什么呢?
從語(yǔ)義上理解,Wathcer其實(shí)類(lèi)似于一個(gè)key的觀察者,當(dāng)key被set的時(shí)候,Watcher會(huì)調(diào)用自身的update方法。
在 mountComponent 也就是加載組件的時(shí)候會(huì)調(diào)用 Watcher 創(chuàng)建觀察者。
new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)
監(jiān)聽(tīng)到一個(gè)set引起的mutation就立即同步執(zhí)行一次cb嗎?
顯然是不可以的。
舉個(gè)例子:
for (let i = 0; i < 1000; i++) {this.msg = `循環(huán)了${i}次`;}
假如對(duì)于這種操作,1000次里面每次Watcher都要響應(yīng)執(zhí)行一次update,那可能是有很大的性能開(kāi)銷(xiāo)的。像本文的這個(gè)例子中update其實(shí)就是一個(gè)console.log,但是如果cb是一個(gè)開(kāi)銷(xiāo)比較大的方法,那么就可能會(huì)引起性能問(wèn)題了。
所以u(píng)pdate操作一定是異步的。
雖然知道要通過(guò)異步來(lái)解決,但具體是如何解決的呢?Vue的做法是把調(diào)用cb放到了一個(gè)micro task或者macro task隊(duì)列中,具體放到微任務(wù)隊(duì)列還是宏任務(wù)隊(duì)列要看當(dāng)前的運(yùn)行環(huán)境是否支持Promise、MutationObserver、setImmediate這幾個(gè)相當(dāng)于放入微任務(wù)隊(duì)列的api,支持就會(huì)放在微任務(wù)隊(duì)列,不支持則使用setTimeout這個(gè)api把調(diào)用cb放到宏任務(wù)隊(duì)列里。
不管放到微任務(wù)隊(duì)列還是宏任務(wù)隊(duì)列,調(diào)用cb都會(huì)在所有的同步代碼執(zhí)行完畢后執(zhí)行。這一點(diǎn)涉及到event loop的知識(shí),因?yàn)榭偸窍葓?zhí)行所有的同步代碼,然后從微任務(wù)隊(duì)列中按順序執(zhí)行,微任務(wù)隊(duì)列空了才會(huì)從宏任務(wù)隊(duì)列中取出一條執(zhí)行。如果此時(shí)微任務(wù)隊(duì)列還有任務(wù),那么就會(huì)繼續(xù)按照這個(gè)循環(huán)執(zhí)行,這個(gè)就是event loop。
通俗地理解Vue的行為就是在監(jiān)聽(tīng)到key的mutation之后key的Watcher都會(huì)觸發(fā)update,想要調(diào)用自身的cb屬性,但是Vue僅僅是答應(yīng)會(huì)在未來(lái)的某個(gè)時(shí)刻執(zhí)行Watcher的這個(gè)update請(qǐng)求也就是調(diào)用它的cb,并且在調(diào)用之前都不會(huì)再受理該Watcher的update的請(qǐng)求。
下面看看源碼
function queueWatcher (watcher) {const id = watcher.idif (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}if (!waiting) {waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()return}nextTick(flushSchedulerQueue)}}}
重點(diǎn)是 nextTick 源碼
let timerFuncif (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) ||MutationObserver.toString() === '[object MutationObserverConstructor]')) {let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks)}} else {timerFunc = () => {setTimeout(flushCallbacks, 0)}}function nextTick (cb, ctx) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = truetimerFunc()}if (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}}
