一、nextTick 的理解
-
nextTick
是Vue
的一個核心實現,在介紹Vue
的nextTick
之前,爲了方便大家理解,先簡單介紹一下JS
的運行機制。 -
JS
運行機制,JS
執行是單線程的,它是基於事件循環的。事件循環大致分爲以下幾個步驟:
- 所有同步任務都在主線程上執行,形成一個執行棧(
execution context stack
)。 - 主線程之外,還存在一個"任務隊列"(
task queue
)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。 - 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
- 主線程不斷重複上面的第三步。
-
主線程的執行過程就是一個
tick
,而所有的異步結果都是通過 “任務隊列” 來調度。 消息隊列中存放的是一個個的任務(task
)。 規範中規定task
分爲兩大類,分別是macro task
和micro task
,並且每個macro task
結束後,都要清空所有的micro task
。 -
關於
macro task
和micro task
的概念,這裏不會細講,簡單通過一段代碼演示他們的執行順序:
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
在瀏覽器環境中,常見的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常見的 micro task 有 MutationObsever 和 Promise.then。
Vue
的實現,在Vue
源碼2.5+
後,nextTick
的實現單獨有一個JS
文件來維護它,它的源碼並不多,總共也就100
多行。接下來我們來看一下它的實現,在src/core/util/next-tick.js
中:
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
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]()
}
}
// 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
})
}
}
-
next-tick.js
申明瞭microTimerFunc
和macroTimerFunc
兩個變量,它們分別對應的是micro task
的函數和macro task
的函數。對於macro task
的實現,優先檢測是否支持原生setImmediate
,這是一個高版本IE
和Edge
才支持的特性,不支持的話再去檢測是否支持原生的MessageChannel
,如果也不支持的話就會降級爲setTimeout 0
;而對於micro task
的實現,則檢測瀏覽器是否原生支持Promise
,不支持的話直接指向macro task
的實現。 -
next-tick.js
對外暴露了兩個函數,先來看nextTick
,這就是我們在上一節執行nextTick(flushSchedulerQueue)
所用到的函數。它的邏輯也很簡單,把傳入的回調函數cb
壓入callbacks
數組,最後一次性地根據useMacroTask
條件執行macroTimerFunc
或者是microTimerFunc
,而它們都會在下一個tick
執行flushCallbacks
,flushCallbacks
的邏輯非常簡單,對callbacks
遍歷,然後執行相應的回調函數。 -
這裏使用
callbacks
而不是直接在nextTick
中執行回調函數的原因是保證在同一個tick
內多次執行nextTick
,不會開啓多個異步任務,而把這些異步任務都壓成一個同步任務,在下一個tick
執行完畢。 -
nextTick
函數最後還有一段邏輯,如下所示:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
這是當
nextTick
不傳cb
參數的時候,提供一個 Promise 化的調用,比如:
nextTick().then(() => {})
當
_resolve
函數執行,就會跳到then
的邏輯中。
-
next-tick.js
還對外暴露了withMacroTask
函數,它是對函數做一層包裝,確保函數執行過程中對數據任意的修改,觸發變化執行nextTick
的時候強制走macroTimerFunc
。比如對於一些DOM
交互事件,如v-on
綁定的事件回調函數的處理,會強制走macro task
。 -
總結:通過對
nextTick
的分析,並結合setter
分析,我們瞭解到數據的變化到DOM
的重新渲染是一個異步過程,發生在下一個tick
。這就是我們平時在開發的過程中,比如從服務端接口去獲取數據的時候,數據做了修改,如果我們的某些方法去依賴了數據修改後的DOM
變化,我們就必須在nextTick
後執行。比如下面的僞代碼:
getData(res).then(()=>{
this.xxx = res.data
this.$nextTick(() => {
// 這裏我們可以獲取變化後的 DOM
})
})
Vue.js 提供了 2 種調用
nextTick
的方式,一種是全局 APIVue.nextTick
,一種是實例上的方法vm.$nextTick
,無論我們使用哪一種,最後都是調用next-tick.js
中實現的nextTick
方法。
二、檢測變化的注意事項
-
對響應式數據對象以及它的
getter
和setter
部分做了瞭解,但是對於一些特殊情況是需要注意的,接下來我們就從源碼的角度來看Vue
是如何處理這些特殊情況的。 -
對象添加屬性,對於使用
Object.defineProperty
實現響應式的對象,當我們去給這個對象添加一個新的屬性的時候,是不能夠觸發它的setter
的,比如:
var vm = new Vue({
data:{
a:1
}
})
// vm.b 是非響應的
vm.b = 2
但是添加新屬性的場景我們在平時開發中會經常遇到,那麼 Vue 爲了解決這個問題,定義了一個全局 API
Vue.set
方法,它在src/core/global-api/index.js
中初始化:
Vue.set = set
這個
set
方法的定義在src/core/observer/index.js
中:
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
set
方法接收三個參數,target
可能是數組或者是普通對象,key
代表的是數組的下標或者是對象的鍵值,val
代表添加的值。首先判斷如果target
是數組且key
是一個合法的下標,則之前通過splice
去添加進數組然後返回,這裏的splice
其實已經不僅僅是原生數組的splice
了,後面詳細介紹數組的邏輯。接着又判斷key
已經存在於target
中,則直接賦值返回,因爲這樣的變化是可以觀測到了。接着再獲取到target.__ob__
並賦值給ob
,之前分析過它是在Observer
的構造函數執行的時候初始化的,表示Observer
的一個實例,如果它不存在,則說明target
不是一個響應式的對象,則直接賦值並返回。最後通過defineReactive(ob.value, key, val)
把新添加的屬性變成響應式對象,然後再通過ob.dep.notify()
手動的觸發依賴通知,還記得我們在給對象添加getter
的時候有這麼一段邏輯:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// ...
})
}
-
在
getter
過程中判斷了childOb
,並調用了childOb.dep.depend()
收集了依賴,這就是爲什麼執行Vue.set
的時候通過ob.dep.notify()
能夠通知到watcher
,從而讓添加新的屬性到對象也可以檢測到變化。這裏如果value
是個數組,那麼就通過dependArray
把數組每個元素也去做依賴收集。 -
數組,
Vue
也是不能檢測到以下變動的數組,如下所示:
- 當你利用索引直接設置一個項時,例如:
vm.items[indexOfItem] = newValue
- 當你修改數組的長度時,例如:
vm.items.length = newLength
- 對於第一種情況,可以使用:
Vue.set(example1.items, indexOfItem, newValue)
;而對於第二種情況,可以使用vm.items.splice(newLength)
。
- 對於
Vue.set
的實現,當target
是數組的時候,也是通過target.splice(key, 1, val)
來添加的,那麼這裏的splice
到底有什麼辦法能讓添加的對象變成響應式的呢。其實之前我們也分析過,在通過observe
方法去觀察對象的時候會實例化Observer
,在它的構造函數中是專門對數組做了處理,它的定義在src/core/observer/index.js
中,如下所示:
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// ...
}
}
}
- 這裏我們只需要關注
value
是Array
的情況,首先獲取augment
,這裏的hasProto
實際上就是判斷對象中是否存在__proto__
,如果存在則augment
指向protoAugment
, 否則指向copyAugment
,來看一下這兩個函數的定義:
/**
* Augment an target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment an target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
protoAugment
方法是直接把target.__proto__
原型直接修改爲src
,而copyAugment
方法是遍歷 keys,通過def
,也就是Object.defineProperty
去定義它自身的屬性值。對於大部分現代瀏覽器都會走到protoAugment
,那麼它實際上就把value
的原型指向了arrayMethods
,arrayMethods
的定義在src/core/observer/array.js
中:
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
-
可以看到,
arrayMethods
首先繼承了Array
,然後對數組中所有能改變數組自身的方法,如push、pop
等這些方法進行重寫。重寫後的方法會先執行它們本身原有的邏輯,並對能增加數組長度的三個方法push、unshift、splice
方法做了判斷,獲取到插入的值,然後把新添加的值變成一個響應式對象,並且再調用ob.dep.notify()
手動觸發依賴通知,這就很好地解釋了之前的示例中調用vm.items.splice(newLength)
方法可以檢測到變化。 -
總結:對響應式對象又有了更全面的認識,如果在實際工作中遇到了這些特殊情況,我們就可以知道如何把它們也變成響應式的對象。其實對於對象屬性的刪除也會用同樣的問題,
Vue
同樣提供了Vue.del
的全局API
,它的實現和Vue.set
大同小異,甚至還要更簡單一些。
三、計算屬性和偵聽屬性
-
Vue
的組件對象支持了計算屬性computed
和偵聽屬性watch
兩個選項,但是不瞭解什麼時候該用computed
什麼時候該用watch
。我們接下來從源碼實現的角度來分析它們兩者有什麼區別。 -
computed
,計算屬性的初始化是發生在Vue
實例初始化階段的initState
函數中,執行了if (opts.computed) initComputed(vm, opts.computed)
,initComputed
的定義在src/core/instance/state.js
中:
const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
-
函數首先創建
vm._computedWatchers
爲一個空對象,接着對computed
對象做遍歷,拿到計算屬性的每一個userDef
,然後嘗試獲取這個userDef
對應的getter
函數,拿不到則在開發環境下報警告。接下來爲每一個getter
創建一個watcher
,這個watcher
和渲染watcher
有一點很大的不同,它是一個computed watcher
,因爲const computedWatcherOptions = { computed: true }
。computed watcher
和普通watcher
的差別我稍後會介紹。最後對判斷如果key
不是vm
的屬性,則調用defineComputed(vm, key, userDef)
,否則判斷計算屬性對於的key
是否已經被data
或者prop
所佔用,如果是的話則在開發環境報相應的警告。 -
那麼接下來需要重點關注
defineComputed
的實現,如下所示:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
- 這段邏輯很簡單,其實就是利用
Object.defineProperty
給計算屬性對應的key
值添加getter
和setter
,setter
通常是計算屬性是一個對象,並且擁有set
方法的時候纔有,否則是一個空函數。在平時的開發場景中,計算屬性有setter
的情況比較少,我們重點關注一下getter
部分,緩存的配置也先忽略,最終getter
對應的是createComputedGetter(key)
的返回值,來看一下它的定義:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
createComputedGetter
返回一個函數computedGetter
,它就是計算屬性對應的 getter。
- 整個計算屬性的初始化過程到此結束,我們知道計算屬性是一個
computed watcher
,它和普通的watcher
有什麼區別呢,爲了更加直觀,接下來來我們來通過一個例子來分析computed watcher
的實現,如下所示:
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
當初始化這個
computed watcher
實例的時候,構造函數部分邏輯稍有不同:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
可以發現
computed watcher
會並不會立刻求值,同時持有一個dep
實例。然後當我們的render
函數執行訪問到this.fullName
的時候,就觸發了計算屬性的getter
,它會拿到計算屬性對應的watcher
,然後執行watcher.depend()
,來看一下它的定義:
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意,這時候的
Dep.target
是渲染watcher
,所以this.dep.depend()
相當於渲染watcher
訂閱了這個computed watcher
的變化。然後再執行watcher.evaluate()
去求值,來看一下它的定義:
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
-
evaluate
的邏輯非常簡單,判斷this.dirty
,如果爲true
則通過this.get()
求值,然後把this.dirty
設置爲false
。在求值過程中,會執行value = this.getter.call(vm, vm)
,這實際上就是執行了計算屬性定義的getter
函數,在我們這個例子就是執行了return this.firstName + ' ' + this.lastName
。 -
這裏需要特別注意的是,由於
this.firstName
和this.lastName
都是響應式對象,這裏會觸發它們的getter
,根據我們之前的分析,它們會把自身持有的dep
添加到當前正在計算的watcher
中,這個時候Dep.target
就是這個computed watcher
。最後通過return this.value
拿到計算屬性對應的值。我們知道了計算屬性的求值過程,那麼接下來看一下它依賴的數據變化後的邏輯。 -
一旦我們對計算屬性依賴的數據做修改,則會觸發
setter
過程,通知所有訂閱它變化的watcher
更新,執行watcher.update()
方法,如下所示:
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
- 那麼對於計算屬性這樣的
computed watcher
,它實際上是有兩種模式,lazy
和active
。如果this.dep.subs.length === 0
成立,則說明沒有人去訂閱這個computed watcher
的變化,僅僅把this.dirty = true
,只有當下次再訪問這個計算屬性的時候纔會重新求值。在我們的場景下,渲染watcher
訂閱了這個computed watcher
的變化,那麼它會執行:
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
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
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
-
getAndInvoke
函數會重新計算,然後對比新舊值,如果變化了則執行回調函數,那麼這裏這個回調函數是this.dep.notify()
,在我們這個場景下就是觸發了渲染watcher
重新渲染。 -
通過以上的分析,我們知道計算屬性本質上就是一個
computed watcher
,也瞭解了它的創建過程和被訪問觸發getter
以及依賴更新的過程,其實這是最新的計算屬性的實現,之所以這麼設計是因爲Vue
想確保不僅僅是計算屬性依賴的值發生變化,而是當計算屬性最終計算的值發生變化纔會觸發渲染watcher
重新渲染,本質上是一種優化。 -
watch
,偵聽屬性的初始化也是發生在Vue
的實例初始化階段的initState
函數中,在computed
初始化之後,執行了:
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
來看一下
initWatch
的實現,它的定義在src/core/instance/state.js
中:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
這裏就是對
watch
對象做遍歷,拿到每一個handler
,因爲 Vue 是支持watch
的同一個key
對應多個handler
,所以如果handler
是一個數組,則遍歷這個數組,調用createWatcher
方法,否則直接調用createWatcher
:
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
這裏的邏輯也很簡單,首先對
hanlder
的類型做判斷,拿到它最終的回調函數,最後調用vm.$watch(keyOrFn, handler, options)
函數,$watch
是 Vue 原型上的方法,它是在執行stateMixin
的時候定義的:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
-
偵聽屬性
watch
最終會調用$watch
方法,這個方法首先判斷cb
如果是一個對象,則調用createWatcher
方法,這是因爲$watch
方法是用戶可以直接調用的,它可以傳遞一個對象,也可以傳遞函數。接着執行const watcher = new Watcher(vm, expOrFn, cb, options)
實例化了一個watcher
,這裏需要注意一點這是一個user watcher
,因爲options.user = true
。通過實例化watcher
的方式,一旦我們watch
的數據發送變化,它最終會執行watcher
的run
方法,執行回調函數cb
,並且如果我們設置了immediate
爲true
,則直接會執行回調函數cb
。最後返回了一個unwatchFn
方法,它會調用teardown
方法去移除這個watcher
。所以本質上偵聽屬性也是基於Watcher
實現的,它是一個user watcher
。其實Watcher
支持了不同的類型,下面我們看下它有哪些類型以及它們的作用。 -
Watcher options
,Watcher
的構造函數對options
做的了處理,代碼如下:
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
// ...
} else {
this.deep = this.user = this.computed = this.sync = false
}
所以
watcher
總共有 4 種類型,我們來一一分析它們,看看不同的類型執行的邏輯有哪些差別。
deep watcher
,通常,如果我們想對一下對象做深度觀測的時候,需要設置這個屬性爲true
,考慮到這種情況:
var vm = new Vue({
data() {
a: {
b: 1
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal)
}
}
}
})
vm.a.b = 2
這個時候是不會 log 任何數據的,因爲我們是 watch 了
a
對象,只觸發了a
的 getter,並沒有觸發a.b
的 getter,所以並沒有訂閱它的變化,導致我們對vm.a.b = 2
賦值的時候,雖然觸發了 setter,但沒有可通知的對象,所以也並不會觸發 watch 的回調函數了。而我們只需要對代碼做稍稍修改,就可以觀測到這個變化了,如下所示:
watch: {
a: {
deep: true,
handler(newVal) {
console.log(newVal)
}
}
}
- 這樣就創建了一個
deep watcher
了,在watcher
執行get
求值的過程中有一段邏輯:
get() {
let value = this.getter.call(vm, vm)
// ...
if (this.deep) {
traverse(value)
}
}
在對 watch 的表達式或者函數求值後,會調用
traverse
函數,它的定義在src/core/observer/traverse.js
中:
import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'
const seenObjects = new Set()
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
-
traverse
的邏輯也很簡單,它實際上就是對一個對象做深層遞歸遍歷,因爲遍歷過程中就是對一個子對象的訪問,會觸發它們的getter
過程,這樣就可以收集到依賴,也就是訂閱它們變化的watcher
,這個函數實現還有一個小的優化,遍歷過程中會把子響應式對象通過它們的dep id
記錄到seenObjects
,避免以後重複訪問。 -
那麼在執行了
traverse
後,我們再對watch
的對象內部任何一個值做修改,也會調用watcher
的回調函數了。對deep watcher
的理解非常重要,今後工作中如果大家觀測了一個複雜對象,並且會改變對象內部深層某個值的時候也希望觸發回調,一定要設置deep
爲true
,但是因爲設置了deep
後會執行traverse
函數,會有一定的性能開銷,所以一定要根據應用場景權衡是否要開啓這個配置。 -
user watcher
,通過vm.$watch
創建的watcher
是一個user watcher
,其實它的功能很簡單,在對watcher
求值以及在執行回調函數的時候,會處理一下錯誤,如下:
get() {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
},
getAndInvoke() {
// ...
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)
}
}
handleError
在 Vue 中是一個錯誤捕獲並且暴露給用戶的一個利器。
-
computed watcher
,computed watcher
幾乎就是爲計算屬性量身定製的,我們剛纔已經對它做了詳細的分析,這裏不再贅述了。 -
sync watcher
,在我們之前對setter
的分析過程知道,當響應式數據發送變化後,觸發了watcher.update()
,只是把這個watcher
推送到一個隊列中,在nextTick
後纔會真正執行watcher
的回調函數。而一旦我們設置了sync
,就可以在當前Tick
中同步執行watcher
的回調函數,如下所示:
update () {
if (this.computed) {
// ...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
只有當我們需要 watch 的值的變化到執行
watcher
的回調函數是一個同步過程的時候纔會去設置該屬性爲 true。
- 總結:對計算屬性和偵聽屬性的實現有了深入的瞭解,計算屬性本質上是
computed watcher
,而偵聽屬性本質上是user watcher
。就應用場景而言,計算屬性適合用在模板渲染中,某個值是依賴了其它的響應式對象甚至是計算屬性計算而來;而偵聽屬性適用於觀測某個值的變化去完成一段複雜的業務邏輯。同時我們又瞭解了watcher
的四個options
,通常我們會在創建user watcher
的時候配置deep
和sync
,可以根據不同的場景做相應的配置。
四、組件更新
- 之前已經講了
Vue
的組件化實現過程,不過只有Vue
組件的創建過程,並沒有涉及到組件數據發生變化,更新組件的過程。而現在對數據響應式原理的分析,瞭解到當數據發生變化的時候,會觸發渲染watcher
的回調函數,進而執行組件的更新過程,接下來我們來詳細分析這一過程,如下所示:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
- 組件的更新還是調用了
vm._update
方法,我們再回顧一下這個方法,它的定義在src/core/instance/lifecycle.js
中:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// ...
const prevVnode = vm._vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
// ...
}
- 組件更新的過程,會執行
vm.$el = vm.__patch__(prevVnode, vnode)
,它仍然會調用patch
函數,在src/core/vdom/patch.js
中定義:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// ...
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
- 這裏執行
patch
的邏輯和首次渲染是不一樣的,因爲oldVnode
不爲空,並且它和vnode
都是VNode
類型,接下來會通過sameVNode(oldVnode, vnode)
判斷它們是否是相同的VNode
來決定走不同的更新邏輯:
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
-
sameVnode
的邏輯非常簡單,如果兩個vnode
的key
不相等,則是不同的;否則繼續判斷對於同步組件,則判斷isComment
、data
、input
類型等是否相同,對於異步組件,則判斷asyncFactory
是否相同。所以根據新舊vnode
是否爲sameVnode
,會走到不同的更新邏輯,我們先來說一下不同的情況。 -
新舊節點不同,如果新舊
vnode
不同,那麼更新的邏輯非常簡單,它本質上是要替換已存在的節點,大致分爲三步,如下所示:
- 創建新節點,如下所示:
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
以當前舊節點爲參考節點,創建新的節點,並插入到 DOM 中,
createElm
的邏輯我們之前分析過。
- 更新父的佔位符節點,如下所示:
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
我們只關注主要邏輯即可,找到當前
vnode
的父的佔位符節點,先執行各個module
的destroy
的鉤子函數,如果當前佔位符是一個可掛載的節點,則執行module
的create
鉤子函數。
- 刪除舊節點,如下所示:
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
把
oldVnode
從當前 DOM 樹中刪除,如果父節點存在,則執行removeVnodes
方法:
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
let i
const listeners = cbs.remove.length + 1
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners)
}
// recursively invoke hooks on child component root node
if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm)
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm)
}
if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
i(vnode, rm)
} else {
rm()
}
} else {
removeNode(vnode.elm)
}
}
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
-
刪除節點邏輯很簡單,就是遍歷待刪除的
vnodes
做刪除,其中removeAndInvokeRemoveHook
的作用是從DOM
中移除節點並執行module
的remove
鉤子函數,並對它的子節點遞歸調用removeAndInvokeRemoveHook
函數;invokeDestroyHook
是執行module
的destory
鉤子函數以及vnode
的destory
鉤子函數,並對它的子vnode
遞歸調用invokeDestroyHook
函數;removeNode
就是調用平臺的DOM API
去把真正的DOM
節點移除。 -
在之前組件生命週期的時候提到
beforeDestroy & destroyed
這兩個生命週期鉤子函數,它們就是在執行invokeDestroyHook
過程中,執行了vnode
的destory
鉤子函數,它的定義在src/core/vdom/create-component.js
中:
const componentVNodeHooks = {
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
當組件並不是
keepAlive
的時候,會執行componentInstance.$destroy()
方法,然後就會執行beforeDestroy & destroyed
兩個鉤子函數。
- 新舊節點相同,對於新舊節點不同的情況,這種創建新節點 -> 更新佔位符節點 -> 刪除舊節點的邏輯是很容易理解的。還有一種組件
vnode
的更新情況是新舊節點相同,它會調用patchVNode
方法,它的定義在src/core/vdom/patch.js
中:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode
的作用就是把新的vnode
patch
到舊的vnode
上,這裏我們只關注關鍵的核心邏輯,我把它拆成四步驟:
- 執行
prepatch
鉤子函數,如下所示:
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
當更新的
vnode
是一個組件vnode
的時候,會執行prepatch
的方法,它的定義在src/core/vdom/create-component.js
中:
const componentVNodeHooks = {
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
}
prepatch
方法就是拿到新的vnode
的組件配置以及組件實例,去執行updateChildComponent
方法,它的定義在src/core/instance/lifecycle.js
中:
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = true
}
// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren
const hasChildren = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
parentVnode.data.scopedSlots || // has new scoped slots
vm.$scopedSlots !== emptyObject // has old scoped slots
)
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// update listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
// resolve slots + force update if has children
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = false
}
}
-
updateChildComponent
的邏輯也非常簡單,由於更新了vnode
,那麼vnode
對應的實例vm
的一系列屬性也會發生變化,包括佔位符vm.$vnode
的更新、slot
的更新,listeners
的更新,props
的更新等等。 -
執行
update
鉤子函數,如下所示:
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
回到
patchVNode
函數,在執行完新的vnode
的prepatch
鉤子函數,會執行所有module
的update
鉤子函數以及用戶自定義update
鉤子函數,對於module
的鉤子函數,之後我們會有具體的章節針對一些具體的 case 分析。
- 完成
patch
過程,如下所示:
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
- 如果
vnode
是個文本節點且新舊文本不相同,則直接替換文本內容。如果不是文本節點,則判斷它們的子節點,並分了幾種情況處理:
oldCh
與ch
都存在且不相同時,使用updateChildren
函數來更新子節點。- 如果只有
ch
存在,表示舊節點不需要了。如果舊的節點是文本節點則先將節點的文本清除,然後通過addVnodes
將ch
批量插入到新節點elm
下。 - 如果只有
oldCh
存在,表示更新的是空節點,則需要將舊的節點通過removeVnodes
全部清除。 - 當只有舊節點是文本節點的時候,則清除其節點文本內容。
- 執行
postpatch
鉤子函數,如下所示:
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
再執行完
patch
過程後,會執行postpatch
鉤子函數,它是組件自定義的鉤子函數,有則執行。那麼在整個pathVnode
過程中,最複雜的就是updateChildren
方法了。
updateChildren
,如下所示:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
updateChildren
的邏輯比較複雜,直接讀源碼比較晦澀,我們可以通過一個具體的示例來分析它。
<template>
<div id="app">
<div>
<ul>
<li v-for="item in items" :key="item.id">{{ item.val }}</li>
</ul>
</div>
<button @click="change">change</button>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
items: [
{id: 0, val: 'A'},
{id: 1, val: 'B'},
{id: 2, val: 'C'},
{id: 3, val: 'D'}
]
}
},
methods: {
change() {
this.items.reverse().push({id: 4, val: 'E'})
}
}
}
</script>
- 總結:組件更新的過程核心就是新舊
vnode diff
,對新舊節點相同以及不同的情況分別做不同的處理。新舊節點不同的更新流程是創建新節點->更新父佔位符節點->刪除舊節點;而新舊節點相同的更新流程是去獲取它們的children
,根據不同情況做不同的更新邏輯。最複雜的情況是新舊節點相同且它們都存在子節點,那麼會執行updateChildren
邏輯。
五、Props 的理解
-
Props
作爲組件的核心特性之一,也是我們平時開發Vue
項目中接觸最多的特性之一,它可以讓組件的功能變得豐富,也是父子組件通訊的一個渠道。那麼它的實現原理是怎樣的,我們來看一下。 -
規範化,在初始化
props
之前,首先會對props
做一次normalize
,它發生在mergeOptions
的時候,在src/core/util/options.js
中:
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
normalizeProps(child, vm)
// ...
}
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
合併配置,它主要就是處理我們定義組件的對象
option
,然後掛載到組件的實例this.$options
中。
- 我們接下來重點看
normalizeProps
的實現,其實這個函數的主要目的就是把我們編寫的props
轉成對象格式,因爲實際上props
除了對象格式,還允許寫成數組格式,如下所示:
-
當
props
是一個數組,每一個數組元素prop
只能是一個string
,表示prop
的key
,轉成駝峯格式,prop
的類型爲空。 -
當
props
是一個對象,對於props
中每個prop
的key
,我們會轉駝峯格式,而它的value
,如果不是一個對象,我們就把它規範成一個對象。 -
如果
props
既不是數組也不是對象,就拋出一個警告。
- 如下所示,舉個例子:
export default {
props: ['name', 'nick-name']
}
經過
normalizeProps
後,會被規範成:
options.props = {
name: { type: null },
nickName: { type: null }
}
export default {
props: {
name: String,
nickName: {
type: Boolean
}
}
}
經過
normalizeProps
後,會被規範成:
options.props = {
name: { type: String },
nickName: { type: Boolean }
}
由於對象形式的
props
可以指定每個prop
的類型和定義其它的一些屬性,推薦用對象形式定義props
。
- 初始化,
Props
的初始化主要發生在new Vue
中的initState
階段,在src/core/instance/state.js
中:
export function initState (vm: Component) {
// ....
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// ...
}
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
-
initProps
主要做三件事情:校驗、響應式和代理。 -
校驗,校驗的邏輯很簡單,遍歷
propsOptions
,執行validateProp(key, propsOptions, propsData, vm)
方法。這裏的propsOptions
就是我們定義的props
在規範後生成的options.props
對象,propsData
是從父組件傳遞的prop
數據。所謂校驗的目的就是檢查一下我們傳遞的數據是否滿足prop
的定義規範。再來看一下validateProp
方法,它定義在src/core/util/options.js
中:
export function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
if (
process.env.NODE_ENV !== 'production' &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
return value
}
validateProp
主要就做三件事情:處理Boolean
類型的數據,處理默認數據,prop
斷言,並最終返回prop
的值。
先來看
Boolean
類型數據的處理邏輯,如下所示:
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
先通過
const booleanIndex = getTypeIndex(Boolean, prop.type)
來判斷prop
的定義是否是Boolean
類型的,如下所示:
function getType (fn) {
const match = fn && fn.toString().match(/^\s*function (\w+)/)
return match ? match[1] : ''
}
function isSameType (a, b) {
return getType(a) === getType(b)
}
function getTypeIndex (type, expectedTypes): number {
if (!Array.isArray(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1
}
for (let i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i
}
}
return -1
}
getTypeIndex
函數就是找到type
和expectedTypes
匹配的索引並返回。
prop
類型定義的時候可以是某個原生構造函數,也可以是原生構造函數的數組,比如:
export default {
props: {
name: String,
value: [String, Boolean]
}
}
-
如果
expectedTypes
是單個構造函數,就執行isSameType
去判斷是否是同一個類型;如果是數組,那麼就遍歷這個數組,找到第一個同類型的,返回它的索引。 -
回到
validateProp
函數,通過const booleanIndex = getTypeIndex(Boolean, prop.type)
得到booleanIndex
,如果prop.type
是一個Boolean
類型,則通過absent && !hasOwn(prop, 'default')
來判斷如果父組件沒有傳遞這個prop
數據並且沒有設置default
的情況,則value
爲 false。 -
接着判斷
value === '' || value === hyphenate(key)
的情況,如果滿足則先通過const stringIndex = getTypeIndex(String, prop.type)
獲取匹配String
類型的索引,然後判斷stringIndex < 0 || booleanIndex < stringIndex
的值來決定value
的值是否爲true
。這塊邏輯稍微有點繞,我們舉兩個例子來說明: -
例如你定義一個組件
Student
:
export default {
name: String,
nickName: [Boolean, String]
}
然後在父組件中引入這個組件:
<template>
<div>
<student name="Kate" nick-name></student>
</div>
</template>
或者是:
<template>
<div>
<student name="Kate" nick-name="nick-name"></student>
</div>
</template>
第一種情況沒有寫屬性的值,滿足
value === ''
,第二種滿足value === hyphenate(key)
的情況,另外nickName
這個prop
的類型是Boolean
或者是String
,並且滿足booleanIndex < stringIndex
,所以對nickName
這個prop
的value
爲true
。接下來看一下默認數據處理邏輯:
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
- 當
value
的值爲undefined
的時候,說明父組件根本就沒有傳這個prop
,那麼我們就需要通過getPropDefaultValue(vm, prop, key)
獲取這個prop
的默認值。我們這裏只關注getPropDefaultValue
的實現,toggleObserving
和observe
的作用我們之後會說,如下所示:
function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
// no default, return undefined
if (!hasOwn(prop, 'default')) {
return undefined
}
const def = prop.default
// warn against non-factory defaults for Object & Array
if (process.env.NODE_ENV !== 'production' && isObject(def)) {
warn(
'Invalid default value for prop "' + key + '": ' +
'Props with type Object/Array must use a factory function ' +
'to return the default value.',
vm
)
}
// the raw prop value was also undefined from previous render,
// return previous default value to avoid unnecessary watcher trigger
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
// call factory function for non-Function types
// a value is Function if its prototype is function even across different execution context
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
}
-
檢測如果
prop
沒有定義default
屬性,那麼返回undefined
,通過這塊邏輯我們知道除了Boolean
類型的數據,其餘沒有設置default
屬性的prop
默認值都是undefined
。 -
接着是開發環境下對
prop
的默認值是否爲對象或者數組類型的判斷,如果是的話會報警告,因爲對象和數組類型的prop
,他們的默認值必須要返回一個工廠函數。 -
接下來的判斷是如果上一次組件渲染父組件傳遞的
prop
的值是undefined
,則直接返回 上一次的默認值vm._props[key]
,這樣可以避免觸發不必要的watcher
的更新。 -
最後就是判斷
def
如果是工廠函數且prop
的類型不是Function
的時候,返回工廠函數的返回值,否則直接返回def
。 -
至此,我們講完了
validateProp
函數的Boolean
類型數據的處理邏輯和默認數據處理邏輯,最後來看一下prop
斷言邏輯,如下所示:
if (
process.env.NODE_ENV !== 'production' &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
在開發環境且非
weex
的某種環境下,執行assertProp
做屬性斷言,如下所示:
function assertProp (
prop: PropOptions,
name: string,
value: any,
vm: ?Component,
absent: boolean
) {
if (prop.required && absent) {
warn(
'Missing required prop: "' + name + '"',
vm
)
return
}
if (value == null && !prop.required) {
return
}
let type = prop.type
let valid = !type || type === true
const expectedTypes = []
if (type) {
if (!Array.isArray(type)) {
type = [type]
}
for (let i = 0; i < type.length && !valid; i++) {
const assertedType = assertType(value, type[i])
expectedTypes.push(assertedType.expectedType || '')
valid = assertedType.valid
}
}
if (!valid) {
warn(
getInvalidTypeMessage(name, value, expectedTypes),
vm
)
return
}
const validator = prop.validator
if (validator) {
if (!validator(value)) {
warn(
'Invalid prop: custom validator check failed for prop "' + name + '".',
vm
)
}
}
}
assertProp
函數的目的是斷言這個prop
是否合法,如下所示:
-
首先判斷如果
prop
定義了required
屬性但父組件沒有傳遞這個prop
數據的話會報一個警告。 -
接着判斷如果
value
爲空且prop
沒有定義required
屬性則直接返回。 -
然後再去對
prop
的類型做校驗,先是拿到prop
中定義的類型type
,並嘗試把它轉成一個類型數組,然後依次遍歷這個數組,執行assertType(value, type[i])
去獲取斷言的結果,直到遍歷完成或者是valid
爲true
的時候跳出循環:
const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
function assertType (value: any, type: Function): {
valid: boolean;
expectedType: string;
} {
let valid
const expectedType = getType(type)
if (simpleCheckRE.test(expectedType)) {
const t = typeof value
valid = t === expectedType.toLowerCase()
// for primitive wrapper objects
if (!valid && t === 'object') {
valid = value instanceof type
}
} else if (expectedType === 'Object') {
valid = isPlainObject(value)
} else if (expectedType === 'Array') {
valid = Array.isArray(value)
} else {
valid = value instanceof type
}
return {
valid,
expectedType
}
}
-
assertType
的邏輯很簡單,先通過getType(type)
獲取prop
期望的類型expectedType
,然後再去根據幾種不同的情況對比prop
的值value
是否和expectedType
匹配,最後返回匹配的結果。 -
如果循環結束後
valid
仍然爲false
,那麼說明prop
的值value
與prop
定義的類型都不匹配,那麼就會輸出一段通過getInvalidTypeMessage(name, value, expectedTypes)
生成的警告信息,就不細說了。 -
最後判斷當
prop
自己定義了validator
自定義校驗器,則執行validator
校驗器方法,如果校驗不通過則輸出警告信息。
-
響應式,回到
initProps
方法,當我們通過const value = validateProp(key, propsOptions, propsData, vm)
對prop
做驗證並且獲取到prop
的值後,接下來需要通過defineReactive
把prop
變成響應式。 -
defineReactive
我們之前已經介紹過,這裏要注意的是,在開發環境中我們會校驗prop
的key
是否是HTML
的保留屬性,並且在defineReactive
的時候會添加一個自定義setter
,當我們直接對prop
賦值的時候會輸出警告:
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
}
關於
prop
的響應式有一點不同的是當vm
是非根實例的時候,會先執行toggleObserving(false)
,它的目的是爲了響應式的優化。
- 代理,在經過響應式處理後,我們會把
prop
的值添加到vm._props
中,比如key
爲name
的prop
,它的值保存在vm._props.name
中,但是我們在組件中可以通過this.name
訪問到這個prop
,這就是代理做的事情,如下所示:
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
通過
proxy
函數實現了上述需求。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
- 當訪問
this.name
的時候就相當於訪問this._props.name
。其實對於非根實例的子組件而言,prop
的代理髮生在Vue.extend
階段,在src/core/global-api/extend.js
中:
Vue.extend = function (extendOptions: Object): Function {
// ...
const Sub = function VueComponent (options) {
this._init(options)
}
// ...
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// ...
return Sub
}
function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
這麼做的好處是不用爲每個組件實例都做一層
proxy
,是一種優化手段。
-
Props 更新,當父組件傳遞給子組件的
props
值變化,子組件對應的值也會改變,同時會觸發子組件的重新渲染。那麼接下來我們就從源碼角度來分析這兩個過程。 -
子組件
props
更新,如下所示:
-
首先,
prop
數據的值變化在父組件,我們知道在父組件的render
過程中會訪問到這個prop
數據,所以當prop
數據變化一定會觸發父組件的重新渲染,那麼重新渲染是如何更新子組件對應的prop
的值呢? -
在父組件重新渲染的最後,會執行
patch
過程,進而執行patchVnode
函數,patchVnode
通常是一個遞歸過程,當它遇到組件vnode
的時候,會執行組件更新過程的prepatch
鉤子函數,在src/core/vdom/patch.js
中:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// ...
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// ...
}
prepatch
函數定義在src/core/vdom/create-component.js
中:
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
- 內部會調用
updateChildComponent
方法來更新props
,注意第二個參數就是父組件的propData
,那麼爲什麼vnode.componentOptions.propsData
就是父組件傳遞給子組件的prop
數據呢(這個也同樣解釋了第一次渲染的propsData
來源)?原來在組件的render
過程中,對於組件節點會通過createComponent
方法來創建組件vnode
:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// ...
return vnode
}
- 在創建組件
vnode
的過程中,首先從data
中提取出propData
,然後在new VNode
的時候,作爲第七個參數VNodeComponentOptions
中的一個屬性傳入,所以我們可以通過vnode.componentOptions.propsData
拿到prop
數據。接着看updateChildComponent
函數,它的定義在src/core/instance/lifecycle.js
中:
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// ...
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// ...
}
-
我們重點來看更新
props
的相關邏輯,這裏的propsData
是父組件傳遞的props
數據,vm
是子組件的實例。vm._props
指向的就是子組件的props
值,propKeys
就是在之前initProps
過程中,緩存的子組件中定義的所有prop
的key
。主要邏輯就是遍歷propKeys
,然後執行props[key] = validateProp(key, propOptions, propsData, vm)
重新驗證和計算新的prop
數據,更新vm._props
,也就是子組件的props
,這個就是子組件props
的更新過程。 -
子組件重新渲染,其實子組件的重新渲染有兩種情況,一個是
prop
值被修改,另一個是對象類型的prop
內部屬性的變化。
-
先來看一下
prop
值被修改的情況,當執行props[key] = validateProp(key, propOptions, propsData, vm)
更新子組件prop
的時候,會觸發prop
的setter
過程,只要在渲染子組件的時候訪問過這個prop
值,那麼根據響應式原理,就會觸發子組件的重新渲染。 -
再來看一下當對象類型的
prop
的內部屬性發生變化的時候,這個時候其實並沒有觸發子組件prop
的更新。但是在子組件的渲染過程中,訪問過這個對象prop
,所以這個對象prop
在觸發getter
的時候會把子組件的render watcher
收集到依賴中,然後當我們在父組件更新這個對象prop
的某個屬性的時候,會觸發setter
過程,也就會通知子組件render watcher
的update
,進而觸發子組件的重新渲染。 -
以上就是當父組件
props
更新,觸發子組件重新渲染的兩種情況。
toggleObserving
,最後toggleObserving
,它的定義在src/core/observer/index.js
中:
export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
shouldObserve = value
}
它在當前模塊中定義了
shouldObserve
變量,用來控制在observe
的過程中是否需要把當前值變成一個Observer
對象。那麼爲什麼在props
的初始化和更新過程中,多次執行toggleObserving(false)
呢,接下來我們就來分析這幾種情況。
- 在
initProps
的過程中,如下所示:
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
// ...
const value = validateProp(key, propsOptions, propsData, vm)
defineReactive(props, key, value)
// ...
}
toggleObserving(true)
對於非根實例的情況,我們會執行
toggleObserving(false)
,然後對於每一個prop
值,去執行defineReactive(props, key, value)
去把它變成響應式。回顧一下defineReactive
的定義:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
},
set: function reactiveSetter (newVal) {
// ...
}
})
}
-
通常對於值
val
會執行observe
函數,然後遇到val
是對象或者數組的情況會遞歸執行defineReactive
把它們的子屬性都變成響應式的,但是由於shouldObserve
的值變成了false
,這個遞歸過程被省略了。爲什麼會這樣呢? -
因爲正如我們前面分析的,對於對象的
prop
值,子組件的prop
值始終指向父組件的prop
值,只要父組件的prop
值變化,就會觸發子組件的重新渲染,所以這個observe
過程是可以省略的。最後再執行toggleObserving(true)
恢復shouldObserve
爲true
。在validateProp
的過程中:
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
- 這種是父組件沒有傳遞
prop
值對默認值的處理邏輯,因爲這個值是一個拷貝,所以我們需要toggleObserving(true)
,然後執行observe(value)
把值變成響應式。在updateChildComponent
過程中:
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
其實和
initProps
的邏輯一樣,不需要對引用類型props
遞歸做響應式處理,所以也需要toggleObserving(false)
。
- 總結:瞭解了
props
的規範化、初始化、更新等過程的實現原理;也瞭解了Vue
內部對props
如何做響應式的優化;同時還了解到props
的變化是如何觸發子組件的更新。瞭解這些對我們平時對props
的應用,遇到問題時的定位追蹤會有很大的幫助。