眾所周知,Vue是以資料驅動檢視展示的,即Vue會監聽資料的變化,從而自動重新渲染頁面的結構。
Vue主要透過三步走來實現這個功能:
第一步是對資料進行響應式改造,即對資料的讀寫操作進行劫持;
第二步是對模板依賴的資料進行收集;
第三步是在資料發生變化時,觸發元件更新。
資料響應式改造
0. defineReactive
對資料進行響應式改造的核心程式碼
// core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
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.target`不為空時,通常指向一個`watcher`例項
dep.depend() // 屬性被收集到當前`watcher`例項的依賴陣列中
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
透過Object.defineProperty
修改物件屬性的屬性描述符descriptor,來實現劫持物件屬性的讀寫操作。
前置知識,物件的屬性分為data型和accessor型。
data型的屬性描述符包含value和writable;accessor型的屬性描述符包含getter和setter函式(兩者至少存在一個)。
由上述程式碼可以看出,所有屬性被處理成了accessor型屬性,即透過getter和setter來完成讀寫,比如當我們讀取person
物件上的屬性name
,實際得到的是name
的屬性訪問符中的getter函式執行後的返回值。
上述的響應式改造中,每個屬性會對應一個dep例項:const dep = new Dep()
,假如屬性值val是物件或陣列,會被列入觀察物件,他的屬性會被遞迴進行響應式改造let childOb = !shallow && observe(val)
。
get函式被用於收集依賴:
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
},
該函式在物件屬性被訪問時會執行,如果Dep.target
不為空,即當下有一個監聽器watcher
在收集依賴,就進行依賴的收集,dep例項會被收集到該watcher
的依賴陣列newDeps中,同時dep也會將此watcher
記錄到自己的subs訂閱陣列中,記錄有誰訂閱了自己的變更。
如果childOb
不為空(即屬性值val為陣列或物件,且可擴充套件),就對val的__ob__
屬性也進行收集操作。
如果value是陣列,對陣列中的物件元素也進行依賴收集。
就是一層層的遞迴收集。
set函式被用於通知變更:
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
如果屬性的新值是屬性或物件,就更新childOb
。
完成屬性賦值操作後,呼叫dep.notify()
,通知所有訂閱了自己的watcher
例項執行update
操作,即下面程式碼中的for迴圈操作。
// core/observer/dep.js
export default class Dep {
// ...
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)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
如果是同步this.sync
的watcher會立即被執行,否則會插入到watcher佇列queueWatcher(this)
排隊等待執行:
// core/observer/watcher.js
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
1. initInjections
// core/instance/inject.js
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
透過執行resolveInject
解析inject中的資料,解析結果賦值給result。result包含inject中所有的key,如果上級元件中沒有對應inject資料的provide,就賦預設值,簡單來說大致就是result[key] = inject[key] || default
。
再呼叫defineReactive(vm, key, result[key])
將這些key加到vm
例項上,即inject中的資料也會進行響應式處理。
假設存在一個inject:["person"]
,如果person
的值是個物件,它是一個被觀察物件,當前子元件的watcher
會對該物件的__ob__
屬性依賴收集,在上級元件中更改了原始person的某個屬性,就會觸發子元件的更新。
2. initProps
// core/instance/state.js
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)
}
propsOptions
,接收的是vm.$options.props
,是宣告接收的props的配置,;vm.$options.propsData
是實際接收到的props資料。
呼叫defineReactive(props, key, result[key])
將propsOptions上的key加到props
物件上,即vm._props
上,進行響應式處理,如果是在vm
上不存在的key,透過proxy(vm, '_props', key)
操作,使得可以透過vm
直接訪問到_props
的屬性,而不需要透過_props
物件來訪問。
3. initData
// core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
data選項會被掛在vm._data
上,從上述程式碼中可以看出,data必須是一個物件,或者返回值為物件的函式。
透過proxy(vm, '_data', key)
操作,vm可以直接訪問到_data
的屬性,而不需要透過_data
物件來訪問。
最後透過observe(data, true /* asRootData */)
來對資料做響應式改造,可以看到這個observe方法多傳了一個引數值為true
,標記當前處理的資料是$options.data
物件。
observe
方法實際是建立一個新的ob例項,資料的__ob__
屬性相信在控制檯列印過vue中資料的同學都不陌生,都是指向ob例項。
// core/observe/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
// core/observe/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
從上面Observer
的建構函式中可以看出,建立ob
例項後,這個例項就掛載資料的__ob__
屬性上了,因為在iniDats
時傳遞給建構函式的引數是個物件,所以會呼叫walk方法,繼續看walk方法的定義,可以看出,是把這個物件的屬性逐個取出,呼叫defineReactive(obj, keys[i])
進行響應式改造。
4. initComputed
// core/instance/state.js
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)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
獲得每個計算屬性對應watcher的初始值
從上述程式碼中可以看出,會遍歷computed所有的屬性,每個屬性對應配置一個watcher例項,watcher例項在建立時,會呼叫每個computed對應的getter獲取一遍初始值,放在watcher例項的value
屬性上
// core/observer/watcher.js
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
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
}
// ...
}
// core/observer/dep.js
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
可以看到,執行watcher例項的get()
方法時,會進行一個pushTarget(this)
的操作,此操作修改了Dep.target
,使它指向了當前的watcher例項,如果某個computed屬性依賴了data中的某個屬性,需要讀取data中的某個屬性值,就會觸發該data屬性的getter函式,使得該data屬性被收集到當前watcher例項的依賴陣列中。
完成computed屬性的取值後,執行popTarget()
,即下面的程式碼:
// core/observer/dep.js
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
會使Dep.target
指回上一個watcher例項。
最後清理依賴this.cleanupDeps()
,將不再關聯的依賴dep其訂閱陣列中對應的watcher移除,將newDeps
賦值給deps
並清空newDeps
,代表該watcher
例項一次依賴收集完畢。
計算屬性被讀取時,其對應watcher依賴的資料會被當前watcher收集為自身的依賴
如果computed某個屬性的識別符號不在vm
例項上,就繼續執行defineComputed(vm, key, userDef)
,會將給vm例項新增一個名為key
的屬性,該屬性的getter函式由下述程式碼定義:
// core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
即在這個計算屬性被讀取時,會拿到它所對應的watcher例項,如果當前Dep.target
不為null時,watcher會執行例項方法depend()
。
export default class Watcher {
// ...
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// ...
}
可以看到此watcher例項的依賴deps會被一一取出,執行dep例項的depend方法:
// core/observerdep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
即此watcher例項的依賴都會被收集到當前Dep.target
指向的watcher例項的依賴陣列中。
5. initWatch
// 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)
}
}
}
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)
}
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) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
initWatch的內容比較簡單,就是透過呼叫createWatcher(vm, key, handler)
,一一對應生成watcher
例項,並且給watcher
例項標記options.user = true
,代表這個watcher
是使用者配置的。
每個watch通常是有一個對應的表示式(通常是vm的data資料)和一個對應的回撥函式,使用場景通常是當vm中的某些資料發生改變時,使用者需要做一些自定義的操作來做處理。
與computed中生成對應watcher例項類似,watcher例項在建立時,每個watch對應的表示式就會被求值一遍,即vm例項上的某些資料屬性被讀取,這些屬性對應的dep會被收集到該watcher
例項的依賴陣列中,求得的值會放在watcher
例項的value
屬性上,如果某個watch配置了immediate
,就立即執行一遍watch對應的回撥函式,入參為watcher
的value
屬性值。
可以看到在執行回撥函式前,執行了一個pushTarget()
,此時Dep.target會指向空,所以在回撥函式執行過程中,如果vm的某些資料屬性被訪問,這些屬性不會被收集依賴,因為屬性的getter
函式中在屬性被收集依賴前有個對Dep.target
的判空檢查。
6. 一些說明
__ob__
是給物件加的屬性,指向observer例項,ob和物件是一對一,代表這個物件被觀察,該物件的屬性的讀寫操作會被做響應式處理,即被劫持。
dep
是給屬性配置的用於依賴收集的,通常物件的某個屬性與dep是一對一,可以被多個watcher收集,即多個watcher例項在監聽這個屬性的變化;__ob__
是物件的特殊屬性,它也有自己的dep,可以被watcher
收集。
一個watcher例項只會關聯一個vm,一個vm例項可以關聯多個watcher
,watcher例項會放在vm._watchers
陣列中;渲染watcher
還會放在vm._watcher
上,渲染watcher
從字面上理解就是與元件渲染有關的watcher
。