一開始就只想搞清楚nextTick的一個原理,誰知道,跟吃了辣條一下,停不下來,從nextTick的源碼到Watcher源碼再到Dep源碼,震驚,而後再結合本身以前看掘金小冊理解的雙向綁定-響應式系統
,感受有一種頓悟
的感受,總之,這是我我的的理解,請大佬們指教,若有轉載,請附上原文連接,畢竟我copy源碼也挺累的~javascript
由於這篇文章,有挺多源代碼的,通常來講,換做是我,我也會一掃而過,一目十行,可是筆者我!真心!但願!大家可以耐住性子!去看!源碼中,會有一丟丟註釋,必定要看尤大大做者給的註釋html
若是有什麼地方寫錯了,懇請大佬們指教,互相進步~前端
那麼怎麼說nextTick呢?該從何提及,怪難爲情的,仍是讓咱們先來看個例子吧vue
<template>
<div>
<div ref="username">{{ username }}</div>
<button @click="handleChangeName">click</button>
</div>
</template>
複製代碼
export default {
data () {
return {
username: 'PDK'
}
},
methods: {
handleChangeName () {
this.username = '彭道寬'
console.log(this.$refs.username.innerText) // PDK
}
}
}
複製代碼
震驚!!!,打印出來的竟然的 "PDK",怎麼回事,我明明修改了username,將值賦爲"彭道寬",爲何仍是打印以前的值,而真實獲取到DOM結點的innerText並無獲得預期中的「彭道寬」, 爲啥子 ?java
不方,咱們再看一個例子,請看:react
export default {
data () {
return {
username: 'PDK',
age: 18
}
},
mounted() {
this.age = 19
this.age = 20
this.age = 21
},
watch: {
age() {
console.log(this.age)
}
}
}
複製代碼
這段腳本執行咱們猜想會依次打印:19,20,21。可是實際效果中,只會輸出一次:21。爲何會出現這樣的狀況?ios
事不過三,因此咱們再來看一個例子git
export default {
data () {
return {
number: 0
}
},
methods: {
handleClick () {
for(let i = 0; i < 10000; i++) {
this.number++
}
}
}
}
複製代碼
在點擊click觸發handleClick()事件以後,number會被遍歷增長10000次,在vue的雙向綁定-響應式系統中,會通過 「setter -> Dep -> Watcher -> patch -> 視圖」 這個流水線。那麼是否是能夠這麼理解,每次number++,都會通過這個「流水線」來修改真實的DOM,而後DOM被更新了10000次。github
可是身爲一位「資深」的前端小白來講,都知道,前端對性能的看中,而頻繁的操做DOM,那但是一大「忌諱」啊。Vue.js 確定不會以如此低效的方法來處理。Vue.js在默認狀況下,每次觸發某個數據的 setter 方法後,對應的 Watcher 對象其實會被 push 進一個隊列 queue 中,在下一個 tick 的時候將這個隊列 queue 所有拿出來 run一遍。這裏咱們看看Vue官網的描述 : Vue 異步執行
DOM 更新。只要觀察到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據改變。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做上很是重要。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。express
Vue在修改數據的時候,不會立馬就去修改數據,例如,當你設置 vm.someData = 'new value' ,該組件不會當即從新渲染。當刷新隊列時,組件會在事件循環隊列清空時的下一個 tick 更新, 爲了在數據變化以後等待 Vue 完成更新 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 === 'new message') // false, message還未更新
Vue.nextTick(function () {
console.log(vm.$el.textContent === 'new message') // true, nextTick裏面的代碼會在DOM更新後執行
})
複製代碼
上面一直扯扯扯,那麼到底什麼是 下一個tick
?
nextTick函數其實作了兩件事情,一是生成一個timerFunc,把回調做爲microTask或macroTask參與到事件循環中來。二是把回調函數放入一個callbacks隊列,等待適當的時機執行
nextTick在官網當中的定義:
在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。
在 Vue 2.4 以前都是使用的 microtasks(微任務)
,可是 microtasks 的優先級太高,在某些狀況下可能會出現比事件冒泡更快的狀況,但若是都使用 macrotasks(宏任務)
又可能會出現渲染的性能問題。因此在新版本中,會默認使用 microtasks,但在特殊狀況下會使用 macrotasks。好比 v-on。對於不知道JavaScript運行機制的,能夠去看看阮一峯老師的JavaScript 運行機制詳解:再談Event Loop、又或者看看個人Event Loop
哎呀媽,又扯遠了,回到正題,咱們先去看看vue中的源碼 :
/* @flow */
/* globals MessageChannel */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
const callbacks = [] // 定義一個callbacks數組來模擬事件隊列
let pending = false // 一個標記位,若是已經有timerFunc被推送到任務隊列中去則不須要重複推送
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 敲重點!!!!!下面這段英文註釋很重要!!!!!
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
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)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
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
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
複製代碼
來來來,咱們仔細的扯一扯~
首先由於目前瀏覽器平臺並無實現 nextTick 方法,因此 Vue.js 源碼中分別用 Promise
、setTimeout
、setImmediate
等方式在 microtask(或是macrotasks)中建立一個事件,目的是在當前調用棧執行完畢之後(不必定當即)纔會去執行這個事件
對於實現 macrotasks ,會先判斷是否能使用 setImmediate ,不能的話降級爲 MessageChannel ,以上都不行的話就使用 setTimeout。 注意,是對實現宏任務的判斷
問題來了?爲何要優先定義 setImmediate
和 MessageChannel
建立,macroTasks而不是 setTimeout
呢?
HTML5中規定setTimeout的最小時間延遲是4ms,也就是說理想環境下異步回調最快也是4ms才能觸發。Vue使用這麼多函數來模擬異步任務,其目的只有一個,就是讓回調異步且儘早調用。而 MessageChannel 和 setImmediate 的延遲明顯是小於 setTimeout的
// 是否可使用 setImmediate
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) { // 是否可使用 MessageChannel
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1) // 利用消息管道,經過postMessage方法把1傳遞給channel.port2
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0) // 利用setTimeout來實現
}
}
複製代碼
setImmediate 和 MessageChannel 都不行的狀況下,使用 setTimeout,delay = 0 以後,執行flushCallbacks(),下邊是flushCallbacks的代碼
// setTimeout 會在 macrotasks 中建立一個事件 flushCallbacks ,flushCallbacks 則會在執行時將 callbacks 中的全部 cb 依次執行。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
複製代碼
前面說了,nextTick
同時也支持 Promise 的使用,會判斷是否實現了 Promise
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 將回調函數整合至一個數組,推送到隊列中下一個tick時執行
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { // pengding = false的話,說明不須要不存在,尚未timerFunc被推送到任務隊列中
pending = true
if (useMacroTask) {
macroTimerFunc() // 執行宏任務
} else {
microTimerFunc() // 執行微任務
}
}
// 判斷是否可使用 promise
// 能夠的話給 _resolve 賦值
// 回調函數以 promise 的方式調用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
複製代碼
ok,上邊nextTick的源碼比較少,看得大概大概的了,可是呢,仍是很懵,因此我又去github看了一下watcher.js的源碼,回到開頭的第三個例子,就是那個循環10000次的那個小坑逼,來,咱們看下源碼再說,源碼裏的代碼太多,我挑着copy,嗯,湊合看吧
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
noop
} from '../util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler' // 這個很也重要,眼熟它
import Dep, { pushTarget, popTarget } from './dep' // 眼熟這個,這個是將 watcher 添加到 Dep 中,去看看源碼
import type { SimpleSet } from '../util/index'
let uid = 0 // 這個也很重要,眼熟它
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */
export default class Watcher {
// 其中的一些我也不知道,我只能從字面上理解,若有大佬,請告知一聲
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, // 咱們的options
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watch = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // 看到沒有,咱們相似於給每一個 Watcher對象起個名字,用id來標記每個Watcher對象
this.active = true
this.dirty = this.lazy
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get() // 執行get()方法
}
get () {
pushTarget(this) // 調用Dep中的pushTarget()方法,具體源碼下邊貼出
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 {
if (this.deep) {
traverse(value)
}
popTarget() // 調用Dep中的popTarget()方法,具體源碼下邊貼出
this.cleanupDeps()
}
return value
}
// 添加到dep中
addDep(dep: Dep) {
const id = dep.id // Dep 中,存在一個id和subs數組(用來存放全部的watcher)
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this) // 調用dep.addSub方法,將這個watcher對象添加到數組中
}
}
}
...
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) // queueWatcher()方法,下邊會給出源代碼
}
}
run () {
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
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) // 回調函數
}
}
}
}
...
}
複製代碼
太長了?染陌大佬的《剖析 Vue.js 內部運行機制》中給出了一個簡單而有利於理解的代碼(羣主,我不是打廣告的,別踢我)
let uid = 0;
class Watcher {
constructor () {
this.id = ++uid;
}
update () {
console.log('watch' + this.id + ' update');
queueWatcher(this);
}
run () {
console.log('watch' + this.id + '視圖更新啦~');
}
}
複製代碼
夠抽象吧!再看看這個代碼,比較一看,你會發現,都出現了一個 queueWatcher的玩意,因而我去把源碼也看了一下。下邊是它的源代碼(選擇copy)
import {
warn,
nextTick, // 看到沒有,咱們一開始要講的老大哥出現了!!!!
devtools
} from '../util/index'
export const MAX_UPDATE_COUNT = 100
/** * Flush both queues and run the watchers. */
function flushSchedulerQueue () {
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.
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() // 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
}
}
}
...
}
/** * 看註釋看註釋!!!!!! * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 獲取watcher的id
// 檢驗id是否存在,已經存在則直接跳過,不存在則標記哈希表has,用於下次檢驗
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 若是沒有flush掉,直接push到隊列中便可
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.
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 // 標誌位,它保證flushSchedulerQueue回調只容許被置入callbacks一次。
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 看到沒有,調用了nextTick
// 這裏面的nextTick(flushSchedulerQueue)中的flushSchedulerQueue函數其實就是watcher的視圖更新。
// 每次調用的時候會把它push到callbacks中來異步執行。
}
}
}
複製代碼
哎呀媽,咱們再來看看Dep中的源碼
import type Watcher from './watcher' // 眼熟它
import { remove } from '../util/index'
import config from '../config'
let uid = 0
/** * A dep is an observable that can have multiple * directives subscribing to it. */
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 將全部的watcher對象添加到數組中
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
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.sort((a, b) => a.id - b.id)
}
// 經過循環,來調用每個watcher,而且 每一個watcher都有一個update()方法,通知視圖更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
// 說白了,在數據【依賴收集】過程就是把 Watcher 實例存放到對應的 Dep 對象中去
// 這時候 Dep.target 已經指向了這個 new 出來的 Watcher 對象
// get 方法可讓當前的 Watcher 對象(Dep.target)存放到它的 subs 數組中
// 在數據變化時,set 會調用 Dep 對象的 notify 方法通知它內部全部的 Watcher 對象進行視圖更新。
複製代碼
真的是寫這篇文章,花了一下午,也在掘金找了一些文章,可是都不夠詳細,而且不少時候,感受不少文章都是千篇一概,借鑑了別人的理解,而後本身同時看染陌大佬的講解,以及本身去看了源碼,才大概看懂,果真,看的文章再多,還不如去看源碼來的實在!!!
《個人博客》: github.com/PDKSophia/b…
《剖析 Vue.js 內部運行機制》: juejin.im/book/5a3666…
《Vue官網之異步更新隊列》: cn.vuejs.org/v2/guide/re…
《MessageChannel API》: developer.mozilla.org/zh-CN/docs/…
《Vue中DOM的異步更新策略以及nextTick機制》: funteas.com/topic/5a8dc…
《Vue.js 源碼之nextTick》: github.com/vuejs/vue/b…
《Vue.js 源碼之Watcher》: github.com/vuejs/vue/b…
《Vue.js 源碼之Dep》: github.com/vuejs/vue/b…