Vue 實(shí)例在建立的時(shí)候會(huì)運(yùn)行一系列的初始化操作仗扬,而在這些初始化操作里面,和數(shù)據(jù)綁定關(guān)聯(lián)最大的是 initState蕾额。這個(gè)里面要說(shuō)的也是比較多早芭,有可能這次的文章里面寫(xiě)不全,先寫(xiě)這看吧诅蝶。
首先看 initState
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
這里面主要是對(duì) props, methods, data, computed 和 watch 進(jìn)行初始化(如果還不知道這幾個(gè)屬性都是什么退个,建議先去看一下官方文檔并且寫(xiě)幾個(gè)小例子)。這些屬性都是要在 Dom 渲染時(shí)獲取的调炬,自然也大都需要進(jìn)行數(shù)據(jù)綁定语盈。
initProps
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
observerState.shouldConvert = isRoot
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
......
} 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)
}
}
observerState.shouldConvert = true
}
省略的地方是開(kāi)發(fā)環(huán)境中為了方便調(diào)試寫(xiě)的代碼,Vue 源碼中有相當(dāng)多的地方是這樣寫(xiě)的缰泡。
整體邏輯就是:
- 把所有 prop 的 key 另存在 options 的 _propKeys 中黎烈。
- 對(duì)于每一個(gè) prop,將其 key 添加到 _propKeys 中匀谣,獲取其 value照棋,并執(zhí)行 defineReactive 函數(shù)。(不了解的可以看上一節(jié))
- 對(duì)于每一個(gè) prop, 調(diào)用 proxy 函數(shù)在 Vue 對(duì)象上建立一個(gè)該值的引用武翎。
在獲取 prop 的 value 的時(shí)候調(diào)用了 validateProp 進(jìn)行驗(yàn)證并取驗(yàn)證后的返回值烈炭。
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]
// handle boolean props
if (isType(Boolean, prop.type)) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
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 prevShouldConvert = observerState.shouldConvert
observerState.shouldConvert = true
observe(value)
observerState.shouldConvert = prevShouldConvert
}
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
}
注意,prop 驗(yàn)證只有在開(kāi)發(fā)環(huán)境中才會(huì)進(jìn)行宝恶,并且并不會(huì)影響渲染符隙,只會(huì)發(fā)出警告。
這里的工作主要是在 prop 沒(méi)有傳值時(shí)獲取 prop 的默認(rèn)值(默認(rèn)值是自己設(shè)置的)垫毙,并對(duì)該值執(zhí)行 observe霹疫。對(duì)于布爾類型,如果沒(méi)有默認(rèn)值則認(rèn)為默認(rèn)值是 false综芥。
如果是開(kāi)發(fā)環(huán)境丽蝎,則會(huì)進(jìn)行類型驗(yàn)證,這個(gè)驗(yàn)證是典型的根據(jù)構(gòu)造函數(shù)名進(jìn)行類型驗(yàn)證的膀藐,這個(gè)函數(shù)名獲取到以后會(huì)進(jìn)行字符串的比對(duì)屠阻,最近也正想自己寫(xiě)一個(gè)比較完善的類型驗(yàn)證組件,所以在這篇文章里就不詳述了额各,免得跑題国觉。
這里多次對(duì) observerState.shouldConvert 進(jìn)行賦值,這個(gè)值的 true or false 直接決定了 Observer 是否會(huì)建立虾啦。
至于這個(gè) propsData 是什么時(shí)候取得的呢麻诀,當(dāng)然是在模板編譯的時(shí)候取得的痕寓。關(guān)于 prop 還有很多需要說(shuō)的,有可能還要另外寫(xiě)一篇文章來(lái)說(shuō)明蝇闭。
initMethod
對(duì) method 的初始化相對(duì)其他來(lái)說(shuō)還是比較簡(jiǎn)單的
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (methods[key] == null) {
warn(
`Method "${key}" has an undefined value in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
}
}
主要是在開(kāi)發(fā)環(huán)境中檢測(cè):
- 方法名是否為空
- 方法名是否和一個(gè) prop 沖突
- 方法名是否和已有的 Vue 實(shí)例方法沖突
另外會(huì)用 bind 將該方法的作用域綁定到 Vue 實(shí)例對(duì)象上呻率,且創(chuàng)建一個(gè)在 Vue 實(shí)例對(duì)象上的引用(這點(diǎn)很重要)
export function bind (fn: Function, ctx: Object): Function {
function boundFn (a) {
const l: number = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
// record original fn length
boundFn._length = fn.length
return boundFn
}
這個(gè) bind 是用apply 和 call 重寫(xiě)的 bind,據(jù)說(shuō)是會(huì)比原生的 bind 要快丁眼,但是實(shí)在才學(xué)尚淺,不明白為什么昭殉。
initData
如果對(duì)上篇文章說(shuō)到的內(nèi)容比較熟悉的話苞七,這里應(yīng)該就沒(méi)什么難度了。
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 */)
}
首先會(huì)獲取 data挪丢,如果 data 是函數(shù)的話蹂风,則調(diào)用 getData 獲取函數(shù)的返回值。
這里面還是在檢測(cè)一些重名的問(wèn)題乾蓬,就不想細(xì)說(shuō)了惠啄。
這里最重要的是對(duì) data 運(yùn)行 observe 函數(shù)建立起 Observer 和鉤子函數(shù)
initComputed
這里就比較麻煩了,由于計(jì)算屬性并不是值任内,而是函數(shù)撵渡,并且返回值還會(huì)和一些值有關(guān),同時(shí)還要涉及到緩存的問(wèn)題死嗦,就需要一些特殊的方法進(jìn)行處理了趋距,為了避免文章太長(zhǎng),就放在下一篇說(shuō)越除。
initWatch
說(shuō)到這里就一定要補(bǔ)充一下之前沒(méi)有說(shuō)到的關(guān)于 Watcher 的問(wèn)題了节腐,先看代碼,一步步往下說(shuō)摘盆。
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)
}
}
}
首先是對(duì)于每一個(gè) watch 屬性運(yùn)行 createWatcher(想想也應(yīng)該知道是建立一個(gè) Watcher 對(duì)象)
function createWatcher (
vm: Component,
keyOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(keyOrFn, handler, options)
}
這里主要進(jìn)行了兩步預(yù)處理翼雀,代碼上很好理解,主要做一些解釋:
第一步孩擂,可以理解為用戶設(shè)置的 watch 有可能是一個(gè) options 對(duì)象狼渊,如果是這樣的話則取 options 中的 handler 作為回調(diào)函數(shù)。(并且將options 傳入下一步的 vm.$watch)
第二步类垦,watch 有可能是之前定義過(guò)的 method囤锉,則獲取該方法為 handler。
下面就要看 $watch 方法了护锤,這個(gè)方法是在 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()
}
}
這里的邏輯是官地,如果 cb(就是前面的 handler)是對(duì)象的話則再運(yùn)行一遍 createWatcher 進(jìn)行處理,然后建立一個(gè) Watcher 對(duì)象進(jìn)行監(jiān)聽(tīng)烙懦,如果 options 中的 immediate 為 true 則立即執(zhí)行該回調(diào)函數(shù)驱入,最后返回一個(gè)函數(shù)用來(lái)停止監(jiān)聽(tīng)。
接下來(lái)就要看看這個(gè)回調(diào)函數(shù)是什么時(shí)候運(yùn)行的了
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
// 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)
}
}
}
}
再次看到 Watcher 的 run 方法,這里面判斷 user 如果為 true 則運(yùn)行 cb 函數(shù)亏较,這個(gè)函數(shù)就是之前傳入的 handler 回調(diào)函數(shù)莺褒, user 則在 vm.$watch 中賦值為true,其他地方建立的 Watcher 則基本都為 false雪情,其他的幾個(gè)如 lasy 等參數(shù)也是通過(guò) options 傳入的遵岩,這里就不詳細(xì)說(shuō)了,具體可以自己看一下代碼或者官方API文檔巡通。
結(jié)語(yǔ)
到這一步為止(先不算計(jì)算屬性的初始化)尘执,數(shù)據(jù)綁定的邏輯基本分析完了,這篇文章看完以后重點(diǎn)還是要看看 Watcher 對(duì)象的設(shè)計(jì)宴凉,可以說(shuō)這個(gè)監(jiān)視器設(shè)計(jì)的相當(dāng)巧妙誊锭,廢話不多說(shuō)了,希望大家有什么見(jiàn)解或者分析有誤的可以提出來(lái)弥锄。