Vue.js 實(shí)現(xiàn)響應(yīng)式
data
、props
、computed
的核心是利用了 ES5 的 Object.defineProperty洒敏,給對(duì)象的屬性添加getter
和setter
.
Object.defineProperty
Object.defineProperty
方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性疙驾, 并返回這個(gè)對(duì)象,先來(lái)看一下它的語(yǔ)法:
Object.defineProperty(obj, prop, descriptor)
obj
是要在其上定義屬性的對(duì)象函荣;prop
是要定義或修改的屬性的名稱;descriptor
是將被定義或修改的屬性描述符扳肛。
比較核心的是 descriptor
傻挂,它有很多可選鍵值挖息,具體的可以去參閱它的文檔。這里我們最關(guān)心的是 get
和 set
殖蚕,get
是一個(gè)給屬性提供的 getter
方法沉迹,當(dāng)我們?cè)L問(wèn)了該屬性的時(shí)候會(huì)觸發(fā) getter
方法;set
是一個(gè)給屬性提供的 setter
方法蛤育,當(dāng)我們對(duì)該屬性做修改的時(shí)候會(huì)觸發(fā) setter
方法葫松。
一旦對(duì)象擁有了 getter
和 setter
腋么,我們可以簡(jiǎn)單地把這個(gè)對(duì)象稱為響應(yīng)式對(duì)象。那么 Vue.js
把哪些對(duì)象變成了響應(yīng)式對(duì)象了呢圣勒,接下來(lái)我們從源碼層面分析圣贸。
在src\core\instance\init.js
文件中的initMixin
函數(shù)中有一個(gè)initState(vm)
方法扛稽,而initState(vm)
在src\core\instance\state.js
定義的:
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)
}
}
initState
方法主要是對(duì) props
、methods
橙困、data
耕餐、computed
和 wathcer 等屬性做了初始化操作。這里我們重點(diǎn)分析 props
和 data
夏跷,對(duì)于其它屬性的初始化我們之后再詳細(xì)分析槽华。
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {} // propsData 既 props 參數(shù)趟妥?披摄?
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') {
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 (vm.$parent && !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)
}
}
observerState.shouldConvert = true
}
props
的初始化主要過(guò)程疚膊,就是遍歷定義的 props
配置寓盗。遍歷的過(guò)程主要做兩件事情:一個(gè)是調(diào)用 defineReactive
方法把每個(gè) prop
對(duì)應(yīng)的值變成響應(yīng)式,可以通過(guò) vm._props.xxx
訪問(wèn)到定義 props 中對(duì)應(yīng)的屬性基显。對(duì)于 defineReactive
方法善炫,我們稍后會(huì)介紹销部;另一個(gè)是通過(guò) proxy
把 vm._props.xxx
的訪問(wèn)代理到 vm.xxx
上制跟,我們稍后也會(huì)介紹雨膨。
下面我們?cè)賮?lái)看一下data
的init
過(guò)程:
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--) { // 如果data中的屬性在methods、props定義了撒妈,則throw一個(gè)警告
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
的初始化主要過(guò)程也是做兩件事狰右,一個(gè)是對(duì)定義 data
函數(shù)返回對(duì)象的遍歷,通過(guò) proxy
把每一個(gè)值 vm._data.xxx
都代理到 vm.xxx
上嫁佳;另一個(gè)是調(diào)用 observe 方法觀測(cè)整個(gè) data
的變化蒿往,把 data 也變成響應(yīng)式湿弦,可以通過(guò) vm._data.xxx
訪問(wèn)到定義 data
返回函數(shù)中對(duì)應(yīng)的屬性颊埃,observe
我們稍后會(huì)介紹。
可以看到娃惯,無(wú)論是 props
或是 data
的初始化都是把它們變成響應(yīng)式對(duì)象肥败,這個(gè)過(guò)程我們接觸到幾個(gè)函數(shù)馒稍,接下來(lái)我們來(lái)詳細(xì)分析它們。
proxy
首先介紹一下代理证膨,代理的作用是把 props
和 data
上的屬性代理到 vm 實(shí)例上鼓黔,這也就是為什么比如我們定義了如下 props
澳化,卻可以通過(guò) vm
實(shí)例訪問(wèn)到它。
let comP = {
props: {
msg: 'hello'
},
methods: {
say() {
console.log(this.msg)
}
}
}
// 我們可以在 say 函數(shù)中通過(guò) this.msg 訪問(wèn)到我們定義在 props 中的 msg井濒,這個(gè)過(guò)程發(fā)生在 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)
}
proxy
方法的實(shí)現(xiàn)很簡(jiǎn)單瑞你,通過(guò) Object.defineProperty
把 target[sourceKey][key]
的讀寫變成了對(duì) target[key] 的讀寫。所以對(duì)于 props
而言春感,對(duì) vm._props.xxx 的讀寫變成了 vm.xxx 的讀寫甥厦,而對(duì)于 vm._props.xxx
我們可以訪問(wèn)到定義在 props 中的屬性寇钉,所以我們就可以通過(guò) vm.xxx
訪問(wèn)到定義在 props中的 xxx 屬性了。同理谦秧,對(duì)于 data
而言疚鲤,對(duì) vm._data.xxxx
的讀寫變成了對(duì) vm.xxxx
的讀寫缘挑,而對(duì)于 vm._data.xxxx 我們可以訪問(wèn)到定義在 data
函數(shù)返回對(duì)象中的屬性语淘,所以我們就可以通過(guò) vm.xxxx
訪問(wèn)到定義在 data
函數(shù)返回對(duì)象中的 xxxx 屬性了。
observe
observe
的功能就是用來(lái)監(jiān)測(cè)數(shù)據(jù)的變化姑蓝,它的定義在 src/core/observer/index.js
中:
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*嘗試為值創(chuàng)建觀察者實(shí)例纺荧,
*如果觀察成功颅筋,返回新的觀察者,
*如果已經(jīng)添加過(guò)則直接返回占贫。
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 不是數(shù)組或?qū)ο蠡蛘呤莢Node直接返回
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 如果value(data,props,computed等)有定義__ob__屬性(Observer中的def(value, '__ob__', this))則直接返回 value.__ob__
ob = value.__ob__
} else if (
observerState.shouldConvert && // src\core\instance\state.js有用到
!isServerRendering() && // isServerRendering 非服務(wù)渲染
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && // Object.isExtensible() 方法判斷一個(gè)對(duì)象是否是可擴(kuò)展的(是否可以在它上面添加新的屬性)
!value._isVue // 非vue的實(shí)例
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe
方法的作用就是給非 VNode
的對(duì)象類型數(shù)據(jù)添加一個(gè) Observer
蜻拨,如果已經(jīng)添加過(guò)則直接返回桩引,否則在滿足一定條件下去實(shí)例化一個(gè) Observer
對(duì)象實(shí)例收夸。接下來(lái)我們來(lái)看一下 Observer
的作用卧惜。
Observer
Observer
是一個(gè)類,它的作用是給對(duì)象的屬性添加 getter
和 setter
设凹,用于依賴收集和派發(fā)更新:
/**
* Observer class that are attached to each observed 附加到每個(gè)觀察對(duì)象的觀察者類
* object. Once attached, the observer converts target 一旦連接闪朱,觀察者轉(zhuǎn)換目標(biāo)
* object's property keys into getter/setters that getter/setter
* collect dependencies and dispatches updates. 收集依賴項(xiàng)并觸發(fā)更新
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// def函數(shù) 第四個(gè)參數(shù)不傳钻洒,則屬性不可枚舉素标,所以在 walk 函數(shù)中不會(huì)執(zhí)行 defineReactive ,也就不會(huì)把 __ob__ 添加到 value中
def(value, '__ob__', this) // 給對(duì)象添加 __ob__屬性
if (Array.isArray(value)) {
const augment = hasProto // hasProto 是否有 __proto__ in object
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys) // 深入了解寓免?
this.observeArray(value) // 是數(shù)組的話 observerArray
} else { // else observe 對(duì)象
this.walk(value)
}
}
/**
* Walk through each property 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], obj[keys[i]]) // object + key + value
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Observer
的構(gòu)造函數(shù)邏輯很簡(jiǎn)單再榄,首先實(shí)例化 Dep
對(duì)象困鸥,這塊稍后會(huì)介紹剑按,接著通過(guò)執(zhí)行 def
函數(shù)把自身實(shí)例添加到數(shù)據(jù)對(duì)象 value
的 __ob__
屬性上艺蝴,def
的定義在 src/core/util/lang.js
中:
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable, // 是否可枚舉
writable: true,
configurable: true
})
}
def
函數(shù)是一個(gè)非常簡(jiǎn)單的Object.defineProperty
的封裝猜敢,這就是為什么我們?cè)陂_(kāi)發(fā)中輸出 data 上對(duì)象類型的數(shù)據(jù)盒延,會(huì)發(fā)現(xiàn)該對(duì)象多了一個(gè) __ob__
的屬性添寺。
回到 Observer
的構(gòu)造函數(shù)计露,接下來(lái)會(huì)對(duì) value
做判斷憎乙,對(duì)于數(shù)組會(huì)調(diào)用 observeArray
方法,否則對(duì)純對(duì)象調(diào)用 walk
方法该押〔侠瘢可以看到 observeArray
是遍歷數(shù)組再次調(diào)用 observe 方法椭蹄,而 walk
方法是遍歷對(duì)象的 key 調(diào)用 defineReactive
方法绳矩,那么我們來(lái)看一下這個(gè)方法是做什么的。
defineReactive
defineReactive
的功能就是定義一個(gè)響應(yīng)式對(duì)象割以,給對(duì)象動(dòng)態(tài)添加 getter
和 setter
应媚,它的定義在 src/core/observer/index.js
中:
/**
* Define a reactive property on an Object.
*在對(duì)象上定義響應(yīng)屬性
*/
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) { // 當(dāng)且僅當(dāng)指定對(duì)象的屬性描述可以被改變或者屬性可被刪除時(shí)中姜,為true丢胚。
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val) // shallow 淺的 observe(val)為每個(gè)值創(chuàng)建一個(gè)observe
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
},
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()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
defineReactive
函數(shù)最開(kāi)始初始化 Dep
對(duì)象的實(shí)例携龟,接著拿到 obj
的屬性描述符峡蟋,然后對(duì)子對(duì)象遞歸調(diào)用 observe
方法华望,這樣就保證了無(wú)論 obj
的結(jié)構(gòu)多復(fù)雜仅乓,它的所有子屬性也能變成響應(yīng)式的對(duì)象方灾,這樣我們?cè)L問(wèn)或修改 obj
中一個(gè)嵌套較深的屬性碌更,也能觸發(fā) getter
和 setter
痛单。最后利用 Object.defineProperty
去給 obj
的屬性 key
添加 getter
和 setter
。而關(guān)于 getter
和 setter
的具體實(shí)現(xiàn)鸟妙,我們會(huì)在之后介紹重父。
總結(jié)
這一節(jié)我們介紹了響應(yīng)式對(duì)象忽匈,核心就是利用Object.defineProperty
給數(shù)據(jù)添加了getter
和setter
丹允,目的就是為了在我們訪問(wèn)數(shù)據(jù)以及寫數(shù)據(jù)的時(shí)候能自動(dòng)執(zhí)行一些邏輯:getter
做的事情是依賴收集,setter
做的事情是派發(fā)更新折柠,那么在接下來(lái)的章節(jié)我們會(huì)重點(diǎn)對(duì)這兩個(gè)過(guò)程分析扇售。