Vue2實現響應式的核心是利用了ES5的Object.defineProperty,這也是Vue不能兼容IE8及如下瀏覽器的緣由javascript
會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象java
能夠在MDN看看關於它的使用介紹react
在Vue中主要使用到的是descriptor中的get和set,get 是一個給屬性提供的 getter 方法,當訪問了該屬性的時候會觸發getter方法。set 是一個給屬性提供的 setter 方法,當對該屬性作修改的時候會觸發setter方法數組
建議去下載一份Vue2代碼,這樣對代碼結構以及思路會比較清晰。想了解的函數方法也能夠及時查到。接下來提到的實現函數代碼篇幅不會太多,僅做爲引導做用瀏覽器
在Vue2中,只要是在data屬性裏聲明的字段,都會變成響應式屬性,在代碼中修改值,會對應的更新到dom上(也涉及到數據驅動知識)或對該值做出響應dom
let vm = new Vue({
data() {
return {
one: 'one'
}
},
watch: {
one: function() {
console.log('one changed')
}
},
computed: {
two: function() {
return this.one + 1
}
}
})
vm.one = '1'
// console.log: one changed
vm.two: '11'
// 此後系統會對這個修改做出一系列的響應如更新dom,更新有關數據等等
複製代碼
在Vue的初始化_init函數執行時,其中會執行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)
}
}
複製代碼
它主要是對props、methods、data、computed和 wathcer等屬性作了初始化操做。這裏咱們重點分析props 和 data:oop
// src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
if (process.env.NODE_ENV !== 'production') {
...
} else {
defineReactive(props, key, value)
}
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
複製代碼
props的初始化主要過程是遍歷定義的props配置。遍歷的過程主要作兩件事情:ui
// src/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 = {}
...
}
// 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') {
// 判斷在methods中是否聲明過了
if (methods && hasOwn(methods, key)) {
...
}
}
// 判斷在props中是否聲明過了
if (props && hasOwn(props, key)) {
...
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// 關鍵代碼!!
observe(data, true /* asRootData */)
}
複製代碼
data 的初始化主要過程也是作兩件事:this
能夠看到,不管是props仍是data的初始化都是把它們變成響應式對象,其中主要函數有defineReactive,proxy,observe
簡單理解是:Vue在建立實例的時候,會拿到options中的data和props等字段,而後對他們進行響應式改造
進行響應式聲明的入口只有data、props等幾個屬性,而且字段須要是顯示聲明的。因此在其餘地方的字段聲明就不會有響應式的效果,好比在執行代碼中新加的屬性,數組的部分操做(這個比較特殊)
若是在平時開發中遇到數據怎麼怎麼改都不會在頁面上動的,能夠先檢查一下是否觸及到上面的問題
let vm = new Vue({
data () {
return {
one: 1,
two: 2,
arr: []
}
},
created: {
this.three = 3
}
})
vm.one = 'one' // 會有對應的響應
vm.two = 'two' // 會有對應的響應
vm.three = 'three' // 不會有對應的響應!
vm.arr[0] = 'new one' // 不會有對應的響應!
複製代碼
至於爲何,繼續看下去便能知一二
proxy的做用是把對象上的屬性代理到vm實例上
// src/core/instance/state.js
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)
}
複製代碼
這也就是爲何咱們定義了以下props、data,經過vm實例就能訪問到它
let comP = {
props: {
msg: 'hello'
},
data() {
return {
str: 'hi'
}
}
methods: {
say() {
console.log(this.msg, this.str)
}
}
}
複製代碼
proxy方法的實現很簡單,經過Object.defineProperty把target[sourceKey][key]的讀寫變成了對target[key]的讀寫,對 props而言,vm._props.xxx的讀寫變成了vm.xxx的讀寫,因此咱們就能夠經過vm.xxx訪問到定義在props中的xxx 屬性了。同理data也如此
observe是用來給值綁上監測數據的變化的功能
// src/core/observer/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
}
複製代碼
observe 方法的做用就是給非VNode的對象類型數據添加一個Observer,若是已經添加過則直接返回,不然在知足必定條件下去實例化一個Observer對象實例
Observer是一個類,它的做用是給對象的屬性添加getter和setter,用於依賴收集和派發更新
export class Observer {
value: any;
dep: Dep;
vmCount: number;
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 {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製代碼
Observer的構造函數邏輯很簡單,首先實例化Dep對象(後面講),接着經過執行def函數把自身實例添加到數據對象value的 __ob__屬性上,def的定義在 src/core/util/lang.js中
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
複製代碼
def函數是一個很是簡單的Object.defineProperty的封裝,這就是爲何我在開發中輸出data 上對象類型的數據,會發現該對象多了一個__ob__的屬性
回到Observer的構造函數,接下來會對value作判斷,對於數組會調用observeArray方法,不然對純對象調用walk方法(重要)。能夠看到 observeArray是遍歷數組再次調用observe方法,而walk方法是遍歷對象的key調用defineReactive方法
defineReactive的功能是定義一個響應式對象,給對象動態添加getter和setter
// src/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
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 關鍵代碼!若是是value是對象,進入下一層響應式綁定
let childOb = !shallow && observe(val)
// 這裏這裏這裏是關鍵!對key進行綁定,而不是對value
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
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
複製代碼
defineReactive函數最開始初始化Dep對象的實例,接着拿到obj的屬性描述符,而後會對子對象遞歸調用observe 方法(observe函數會判斷傳入值的類型而後作對應操做),這樣就保證了不管obj的結構多複雜,它的全部子屬性也能變成響應式的對象,這樣咱們訪問或修改 obj 中一個嵌套較深的屬性,也能觸發getter和setter。最後利用 Object.defineProperty去給obj的屬性key添加getter和 setter
而關於getter和setter的具體實現,篇幅太多,下篇介紹
響應式的聲明大概就是這樣:拿到配置中的data、props等屬性,對裏面的字段都進行響應式改造:
是否是其實沒什麼捏