Vue中有個API是nextTick
,官方文檔是這樣介紹做用的:html
將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。
理解這部份內容,有助於理解Vue對頁面的渲染過程,同時也能夠了解到beforeUpdate
和updated
的使用。另外就是經過了解nextTick
的調用瞭解vue內部是如何使用Promise
的。這部份內容和以前介紹計算屬性的內容也有關聯,能夠比照着看。vue
首先看一下我建立的例子:express
<!-- HTML 部分 --> <div id="test"> <p>{{ name }}的年齡是{{ age }}</p> <!-- <p> {{ info }} </p> --> <div>體重<input type="text" v-model="age" /></div> <button @click="setAge">設置年齡爲100</button> </div>
// js 部分 new Vue({ el: '#test', data() { return { name: 'tuanzi', age: 2 } }, beforeUpdate() { console.log('before update') debugger }, updated() { console.log('updated') debugger }, methods: { setAge() { this.age = 190 debugger this.$nextTick(() => { console.log('next tick', this.age) debugger }) } } })
當頁面渲染完成,點擊按鈕觸發事件以後,都會發生什麼呢~~promise
直接介紹計算屬性的時候說過,當頁面初次加載渲染,會調用模板中的值,這時會觸發該值的getter
設置。因此對於咱們這裏,data中的name
和age
都會訂閱updateComponent
這個方法,這裏咱們看下這個函數的定義:dom
updateComponent = () => { vm._update(vm._render(), hydrating) }
簡而言之,這時用來渲染頁面的,因此當代碼執行到this.age = 190
,這裏就會觸發age
的setter
屬性,該屬性會調用dep.notify
方法:異步
// 通知 notify() { // stabilize the subscriber list first // 淺拷貝訂閱列表 const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order // 關閉異步,則subs不在調度中排序 // 爲了保證他們能正確的執行,如今就帶他們進行排序 subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
這裏的this.subs
就是頁面初始化過程當中,age
這個屬性收集到的依賴關係,也就是renderWatcher
實例。接着調用renderWatcher
的update
方法。async
/** * Subscriber interface. * Will be called when a dependency changes. */ update() { // debugger /* istanbul ignore else */ if (this.lazy) { // 執行 computedWacher 會運行到這裏 this.dirty = true } else if (this.sync) { this.run() } else { // 運行 renderWatcher queueWatcher(this) } }
那爲了更好的理解這裏,我把renderWatcher
的實例化的代碼也貼出來:ide
// we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */ )
所以,renderWatcher
是沒有設置lazy
這個屬性的,同時我也沒有手動設置sync
屬性,所以代碼會執行到queueWatcher(this)
。注意這裏的this
,當前屬於renderWatcher
實例對象,所以這裏傳遞的this就是該對象。函數
// 將一個watcher實例推入隊列準備執行 // 若是隊列中存在相同的watcher則跳過這個watcher // 除非隊列正在刷新 export function queueWatcher(watcher: Watcher) { const id = watcher.id debugger if (has[id] == null) { has[id] = true if (!flushing) { // 沒有在刷新隊列,則推入新的watcher實例 queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. // 隊列已經刷新,則用傳入的watcher實例的id和隊列中的id比較,按大小順序插入隊列 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() return } debugger nextTick(flushSchedulerQueue) } } }
這段代碼比較簡單,就說一點。代碼裏有個判斷是config.async
,這是Vue私有對象上的值,默認的是true
,所以代碼會執行到nextTick
這裏,此時會傳入一個回調函數flushSchedulerQueue
,咱們這裏先不說,以後用的的時候再介紹。如今看看nextTick
的實現。oop
const callbacks = [] let pending = false export function nextTick(cb?: Function, ctx?: Object) { debugger 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 }) } }
pendding
用來判斷是否存在等待的隊列,callbacks
是執行回調的隊列。那對於此時此刻,就是向callbacks
推入一個回調函數,其中要執行的部分就是flushSchedulerQueue
。由於是初次調用這個函數,這裏的就會調用到timerFunc
。
let timerFunc const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // 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) }
如今毫無由於的是timerFunc
這個函數會被調用。可是有個問題,p.then(flushCallbacks)
這句話會執行麼?來看個例子:
function callback() { console.log('callback') } let p = Promise.resolve() function func() { p.then(callback) } console.log('this is start') func() console.log('this is pre promise 1') let a = 1 console.log('this is pre promise 2') console.log(a)
思考一下結果是什麼吧。看看和答案是否一致:
說回上面,p.then(flushCallbacks)
這句話在這裏會執行,可是是將flushCallbacks
這個方法推入了微任務隊列,要等其餘的同步代碼執行完成,執行棧空了以後纔會調用。因此對於renderWatcher
來講,目前就算執行完了。
接下來代碼執行到這裏:
this.$nextTick(() => { console.log('next tick', this.age) debugger })
看下$nextTick
的定義:
Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) }
這裏定義$nextTick
是定義在Vue的原型對象上,因此在頁面中能夠經過this.$nextTick
調用,同時傳入的this
就是當前頁的實例。因此看會nextTick
定義的部分,惟一的區別是,這是的pendding
是false
,所以不會再調用一次timerFunc
。
setAge
裏的同步代碼都執行完了,所以就輪到flushCallbacks
出場。來看下定義:
function flushCallbacks() { debugger pending = false const copies = callbacks.slice(0) callbacks.length = 0 console.log(copies) for (let i = 0; i < copies.length; i++) { copies[i]() } }
這裏定義的位置和定義nextTick
是在同一個文件裏,所以pendding
和callbacks
是共享的。主要就看copies[i]()
這一段。通過前面的執行,此時callbacks.length
的值應該是2。copies[1]
指的就是先前推動隊列的flushSchedulerQueue
。
/** * Flush both queues and run the watchers. * * 刷新隊列而且運行watcher */ function flushSchedulerQueue() { currentFlushTimestamp = getNow() flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. // 給刷新隊列排序,緣由以下: // 1. 組件的更新是從父組件開始,子組件結束 // 2. 組件的 userWatcher 的運行老是先於 renderWatcher // 3. 若是父組件的watcher運行期間,子組件被銷燬了,後續運行能夠跳過被銷燬的子組件 queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn('You may have an infinite update loop ' + (watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.`), watcher.vm) break } } }
watcher.before
這個方法是存在的,先前的代碼中有,在初始化renderWatcher
時傳入了這個參數。這裏就調用了callHook(vm, 'beforeUpdate')
,因此能看出來,此時beforeUpdate
執行了。接着執行watcher.run()
。run
是Watcher
類上定義的一個方法。
/** * Scheduler job interface. * Will be called by the scheduler. */ run() { debugger if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value 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) } } } }
this.active
初始化的值就是true,get
方法以前的文章也提到過,這裏再貼一遍代碼:
/** * Evaluate the getter, and re-collect dependencies. */ get() { // debugger pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }
這部分代碼以前說過,這裏就再也不說了,只提一點,此時的this.getter
執行的是updateComponent
,其實也就是裏面定義的vm._update(vm._render(), hydrating)
。關於render
和update
我會在分析虛擬dom時介紹。
如今須要知道的是,頁面此時會從新渲染,我在setAge
方法中修改了age
的值,當vm._update
執行完,就會發現頁面上的值變化了。那接着就執行callbacks
中的下一個值,也就是我寫在$nextTick
中的回調函數,這個就很簡單,不必再說。點擊按鈕到如今新的頁面渲染完成,執行的結果就是:
before update updated next tick 100
這裏就把整個流程講完了,可是我想到vue文檔中說的:
在修改數據以後當即使用它,而後等待 DOM 更新
假設我如今要是把$nextTick
放到修改值以前呢。把setAge
修改一下。
setAge() { this.$nextTick(() => { console.log('next tick', this.age) debugger }) debugger this.age = 100 }
思考一下,此時點擊按鈕,頁面會打印出什麼東西。按照邏輯,由於$nextTick
寫在了前面,所以會被先推動callbacks
中,也就會被第一個執行。因此此時我覺得打印出來的age
仍是2。但我既然都這樣說了,那結果確定是和我覺得的不同,但我有一部分想的沒錯,就是優先推入,優先調用。當我忘了一點,你們也能夠會想一下,renderWatcher
是如何被觸發的?
$nextTick
回調如今是進入了微任務隊列,因此會繼續執行接下來的賦值。此時會觸發age
設置的setter
裏的dep.notify
。但在調用以前,新的值就已經傳給age了。因此當$nextTick
裏的回調執行時,會觸發age
的getter
,拿到的值就是新的值。
整個nextTick
事件就介紹完了。