Object.defineProperty 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象,先來看一下它的語法:html
Object.defineProperty(obj, prop, descriptor)
get 是一個給屬性提供的 getter 方法,當咱們訪問了該屬性的時候會觸發 getter 方法;set 是一個給屬性提供的 setter 方法,當咱們對該屬性作修改的時候會觸發 setter 方法。vue
在 Vue 的初始化階段,_init 方法執行的時候,會執行 initState(vm) 方法,它的定義在 src/core/instance/state.js 中。react
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options // 初始化props if (opts.props) initProps(vm, opts.props) // 初始化方法 if (opts.methods) initMethods(vm, opts.methods) // 初始化data沒有跟數據的話就初始一個 if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // 初始computed if (opts.computed) initComputed(vm, opts.computed) // 出書watach if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
initState 方法主要是對 props、methods、data、computed 和 wathcer 等屬性作了初始化操做。這裏咱們重點分析 props 和 data,對於其它屬性的初始化咱們以後再詳細分析。數組
if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) }
首先判斷 opts.data 是否存在,即 data 選項是否存在,若是存在則調用 initData(vm) 函數初始化 data 選項,不然經過 observe 函數觀測一個空的對象,而且 vm._data 引用了該空對象。其中 observe 函數是將 data 轉換成響應式數據的核心入口,另外實例對象上的 _data 屬性咱們在前面的章節中講解 $data 屬性的時候講到過,$data 屬性是一個訪問器屬性,其代理的值就是 _data。閉包
// 傳入兩個參數vue實例和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 (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) } } toggleObserving(true) }
props 的初始化主要過程,就是遍歷定義的 props 配置。遍歷的過程主要作兩件事情:一個是調用 defineReactive 方法把每一個 prop 對應的值變成響應式,能夠經過 vm._props.xxx 訪問到定義 props 中對應的屬性。對於 defineReactive 方法,咱們稍後會介紹;另外一個是經過 proxy 把 vm._props.xxx 的訪問代理到 vm.xxx 上,咱們稍後也會介紹ide
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 的初始化主要過程也是作兩件事,一個是對定義 data 函數返回對象的遍歷,經過 proxy 把每個值 vm._data.xxx 都代理到 vm.xxx 上;另外一個是調用 observe 方法觀測整個 data 的變化,把 data 也變成響應式,能夠經過 vm._data.xxx 訪問到定義 data 返回函數中對應的屬性,observe 咱們稍後會介紹。函數
能夠看到,不管是 props 或是 data 的初始化都是把它們變成響應式對象oop
let data = vm.$options.data // 首先定義 data 變量,它是 vm.$options.data 的引用 data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
vm.$options.data 其實最終被處理成了一個函數,且該函數的執行結果纔是真正的數據。在上面的代碼中咱們發現其中依然存在一個使用 typeof 語句判斷 data 數據類型的操做,咱們知道通過 mergeOptions 函數處理後 data 選項必然是一個函數,那麼這裏的判斷還有必要嗎?答案是有,這是由於 beforeCreate 生命週期鉤子函數是在 mergeOptions 函數以後 initData 以前被調用的,若是在 beforeCreate 生命週期鉤子函數中修改了 vm.$options.data 的值,那麼在 initData 函數中對於 vm.$options.data 類型的判斷就是必要的了。性能
若是 vm.$options.data 的類型爲函數,則調用 getData 函數獲取真正的數據,getData 函數就定義在 initData 函數的下面優化
// data 選項是一個函數, 參數是 Vue 實例對象 // getData 函數的做用其實就是經過調用 data 函數獲取真正的數據對象並返回 export function getData (data: Function , vm: Component): any { // #7573 disable dep collection when invoking data getters pushTarget() try { return data.call(vm, vm) } catch (e) { handleError(e, vm, `data()`) return {} } finally { popTarget() } }
data.call(vm, vm),並且咱們注意到 data.call(vm, vm) 被包裹在 try...catch 語句塊中,這是爲了捕獲 data 函數中可能出現的錯誤。同時若是有錯誤發生那麼則返回一個空對象做爲數據對象:return {}
另外咱們注意到在 getData 函數的開頭調用了 pushTarget() 函數,而且在 finally 語句塊中調用了 popTarget(),這麼作的目的是什麼呢?這麼作是爲了防止使用 props 數據初始化 data 數據時收集冗餘的依賴,等到咱們分析 Vue 是如何收集依賴的時候會回頭來講明。總之 getData 函數的做用就是:「經過調用 data 選項從而獲取數據對象」
咱們再回到 initData 函數中:
data = vm._data = getData(data, vm)
當經過 getData 拿到最終的數據對象後,將該對象賦值給 vm._data 屬性,同時重寫了 data 變量,此時 data 變量已經不是函數了,而是最終的數據對象
緊接着是一個 if 語句塊:
// isPlainObject 函數判斷變量 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 ) } 觸發代碼: data 函數返回了一個字符串而不是對象,因此咱們須要判斷一下 data 函數返回值的類型。 new Vue({ data () { return '我就是不返回對象' } })
接下來:
// proxy data on instance const keys = Object.keys(data) // 使用 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) } }
這段代碼的意思是非生產環境下若是發如今 methods 對象上定義了一樣的 key,也就是說 data 數據的 key 與 methods 對象中定義的函數名稱相同,那麼會打印一個警告,提示開發者:你定義在 methods 對象中的函數名稱已經被做爲 data 對象中某個數據字段的 key 了,你應該換一個函數名字.
爲何要這麼作呢:
const ins = new Vue({ data: { a: 1 }, methods: { b () {} } }) ins.a // 1 ins.b // function
在這個例子中不管是定義在 data 中的數據對象,仍是定義在 methods 對象中的函數,均可以經過實例對象代理訪問。因此當 data 數據對象中的 key 與 methods 對象中的 key 衝突時,豈不就會產生覆蓋掉的現象,因此爲了不覆蓋 Vue 是不容許在 methods 中定義與 data 字段的 key 重名的函數的。而這個工做就是在 while 循環中第一個語句塊中的代碼去完成的.
第二個 if 語句塊:
// 檢測props裏面是否有很data同名的 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 ) // 判判定義在 data 中的 key 是不是保留鍵 } else if (!isReserved(key)) { proxy(vm, `_data`, key) }
另外這裏有一個優先級的關係:==props優先級 > data優先級 > methods優先級==
!isReserved(key),該條件的意思是判判定義在 data 中的 key 是不是保留鍵
isReserved 函數經過判斷一個字符串的第一個字符是否是 $ 或 _ 來決定其是不是保留的,Vue 是不會代理那些鍵名以 $ 或 _ 開頭的字段的,由於 Vue 自身的屬性和方法都是以 $ 或 _ 開頭的,因此這麼作是爲了不與 Vue 自身的屬性和方法相沖突。
若是 key 既不是以 $ 開頭,又不是以 _ 開頭,那麼將執行 proxy 函數,實現實例對象的代理訪問:
下代理,代理的做用是把 props 和 data 上的屬性代理到 vm 實例上,這也就是爲何好比咱們定義了以下 props,卻能夠經過 vm 實例訪問到它。
let comP = { props: { msg: 'hello' }, methods: { say() { console.log(this.msg) } } }
say 函數中經過 this.msg 訪問到咱們定義在 props 中的 msg,這個過程發生在 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) }
例子:
class Vue { constructor(data) { this.data = data; this.initData(); } initData(){ this.proxy(this, `data`, 'a'); } proxy(target, sourceKey, key){ const sharedPropertyDefinition = { enumerable: true, configurable: true, get: ()=>{}, set: ()=>{} } sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) } } const vue = new Vue({ a: '1' }) console.log(vue.data.a, vue.a)
proxy 方法的實現很簡單,經過 Object.defineProperty 把 target[sourceKey][key] 的讀寫變成了對 target[key] 的讀寫。因此對於 props 而言,對 vm._props.xxx 的讀寫變成了 vm.xxx 的讀寫,而對於 vm._props.xxx 咱們能夠訪問到定義在 props 中的屬性,因此咱們就能夠經過 vm.xxx 訪問到定義在 props 中的 xxx 屬性了。同理,對於 data 而言,對 vm._data.xxxx 的讀寫變成了對 vm.xxxx 的讀寫,而對於 vm._data.xxxx 咱們能夠訪問到定義在 data 函數返回對象中的屬性,因此咱們就能夠經過 vm.xxxx 訪問到定義在 data 函數返回對象中的 xxxx 屬性了
最後通過一系列的處理,initData 函數來到了最後一句代碼:
// observe data observe(data, true /* asRootData */)
調用 observe 函數將 data 數據對象轉換成響應式的,能夠說這句代碼纔是響應系統的開始,不過在講解 observe 函數以前咱們有必要總結一下 initData 函數所作的事情,經過前面的分析可知 initData 函數主要完成以下工做:
observe 的功能就是用來監測數據的變化,它的定義在 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. */ // 第一個參數是要觀測的數據,第二個參數是一個布爾值,表明將要被觀測的數據是不是根級數據 export function observe (value: any, asRootData: ?boolean): Observer | void { // 觀測的數據不是一個對象或者是 VNode 實例,則直接 return if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 若是有__ob__的就直接返回,優化性能 //if 分支的判斷條件,首先使用 hasOwn 函數檢測數據對象 value 自身是否含有 __ob__ //屬性,而且 __ob__ 屬性應該是 Observer 的實例。若是爲真則直接將數據對象自身的 __ob__ //屬性的值做爲 ob 的值:ob = value.__ob__。那麼 __ob__ //是什麼呢?其實當一個數據對象被觀測以後將會在該對象上定義 __ob__ 屬性,因此 if //分支的做用是用來避免重複觀測一個數據對象 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } // 若是數據對象上沒有定義 __ob__ 屬性,那麼說明該對象沒有被觀測過 else if ( shouldObserve && // 二、來判斷是不是服務端渲染 !isServerRendering() && // 三、只有當數據對象是數組或純對象 (Array.isArray(value) || isPlainObject(value)) && // 四、要被觀測的數據對象必須是可擴展的 // 不可擴展:Object.preventExtensions()、Object.freeze() 以及 Object.seal() Object.isExtensible(value) && // 五、Vue 實例對象擁有 _isVue 屬性,因此這個條件用來避免 Vue 實例對象被觀測 !value._isVue ) { // 執行 ob = new Observer(value) 對數據對象進行觀測 ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
一、shouldObserve
shouldObserve 變量也定義在 core/observer/index.js 文件內,以下:
/** * In some cases we may want to disable observation inside a component's * update computation. */ export let shouldObserve: boolean = true export function toggleObserving (value: boolean) { shouldObserve = value }
該變量的初始值爲 true,在 shouldObserve 變量的下面定義了 toggleObserving 函數,該函數接收一個布爾值參數,用來切換 shouldObserve 變量的真假值,咱們能夠把 shouldObserve 想象成一個開關,爲 true 時說明打開了開關,此時能夠對數據進行觀測,爲 false 時能夠理解爲關閉了開關,此時數據對象將不會被觀測
知足上面的5個條件:接下來咱們來看一下 Observer 的做用。
/** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ 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 // 爲每個value設置一個__ob__ def(value, '__ob__', this) // 該判斷用來區分數據對象究竟是數組仍是一個純對象 if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. * 遍歷obj的key,去添加get和set變成響應式對象 */ 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]) } } }
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { // 省略... } walk (obj: Object) { // 省略... } observeArray (items: Array<any>) { // 省略... } }
Observer 類的實例對象將擁有三個實例屬性,分別是 value、dep 和 vmCount 以及兩個實例方法 walk 和 observeArray。Observer 類的構造函數接收一個參數,即數據對象。
constructor (value: any) { this.value = value this.dep = new Dep() // 這個「筐」並不屬於某一個字段,後面咱們會發現,這個筐是屬於某一個對象或數組的 this.vmCount = 0 def(value, '__ob__', this) // 初始化完成三個實例屬性以後,使用 def 函數,爲數據對象定義了一個 __ob__ 屬性,這個屬性的值就是當前 Observer 實例對象 if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } }
/** * Define a property. def 函數其實就是 Object.defineProperty 函數的簡單封裝 */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable,// 那是false,定義不可枚舉的屬性 在walk中循環的時候,不會取到那個屬性 writable: true, configurable: true }) }
例子:
const data = { a: 1 } 那麼通過 def 函數處理以後,data 對象應該變成以下這個樣子: const data = { a: 1, // __ob__ 是不可枚舉的屬性 __ob__: { value: data, // value 屬性指向 data 數據對象自己,這是一個循環引用 dep: dep實例對象, // new Dep() vmCount: 0 } }
回到 Observer 的構造函數,接下來會對 value 作判斷,對於數組會調用 observeArray 方法,不然對純對象調用 walk 方法。能夠看到 observeArray 是遍歷數組再次調用 observe 方法,而 walk 方法是遍歷對象的 key 調用 defineReactive 方法,那麼咱們來看一下這個方法是作什麼的。
defineReactive 的功能就是定義一個響應式對象,給對象動態添加 getter 和 setter,它的定義在 src/core/observer/index.js 中:
==defineReactive 函數的核心就是 將數據對象的數據屬性轉換爲訪問器屬性==
/** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 這個 dep 常量所引用的 Dep 實例對象才與咱們前面講過的「筐」的做用相同 // 即 每個數據字段都經過閉包引用着屬於本身的 dep 常量 // 每次調用 defineReactive 定義訪問器屬性時,該屬性的 setter/getter 都閉包引用了一個屬於本身的「筐 const dep = new Dep() // 不可配置的直接返回 // 獲取該字段可能已有的屬性描述對象 const property = Object.getOwnPropertyDescriptor(obj, key) // 判斷該字段是不是可配置的 // 一個不可配置的屬性是不能使用也不必使用 Object.defineProperty 改變其屬性定義的。 if (property && property.configurable === false) { return } // 保存了來自 property 對象的 get 和 set // 避免原有的 set 和 get 方法被覆蓋 const getter = property && property.get const setter = property && property.set // 下面會特殊說明 if ((!getter || setter) && arguments.length === 2) { // 獲取到了對象屬性的值 val,可是 val 自己有可能也是一個對象 val = obj[key] } // 若是是對象繼續調用 observe(val) 函數觀測該對象從而深度觀測數據對象 // walk 函數中調用 defineReactive 函數時沒有傳遞 shallow 參數,因此該參數是 undefined // 默認就是深度觀測 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 }, // 經過觀察者 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() } }) }
假設咱們有以下數據對象:
const data = { a: { b: 1 } } observe(data)
數據對象 data 擁有一個叫作 a 的屬性,且屬性 a 的值是另一個對象,該對象擁有一個叫作 b 的屬性。那麼通過 observe 處理以後, data 和 data.a 這兩個對象都被定義了 ob 屬性,而且訪問器屬性 a 和 b 的 setter/getter 都經過閉包引用着屬於本身的 Dep 實例對象和 childOb 對象:
const data = { // 屬性 a 經過 setter/getter 經過閉包引用着 dep 和 childOb a: { // 屬性 b 經過 setter/getter 經過閉包引用着 dep 和 childOb b: 1 __ob__: {a, dep, vmCount} } __ob__: {data, dep, vmCount} }
defineReactive 函數最開始初始化 Dep 對象的實例,接着拿到 obj 的屬性描述符,而後對子對象遞歸調用 observe 方法,這樣就保證了不管 obj 的結構多複雜,它的全部子屬性也能變成響應式的對象,這樣咱們訪問或修改 obj 中一個嵌套較深的屬性,也能觸發 getter 和 setter。最後利用 Object.defineProperty 去給 obj 的屬性 key 添加 getter 和 setter。
核心就是利用 Object.defineProperty 給數據添加了 getter 和 setter,目的就是爲了在咱們訪問數據以及寫數據的時候能自動執行一些邏輯:getter 作的事情是依賴收集,setter 作的事情是派發更新