Vue 數據響應式源碼分析筆記

1、前言

Vue2實現響應式的核心是利用了ES5的Object.defineProperty,這也是Vue不能兼容IE8及如下瀏覽器的緣由javascript

Object.defineProperty

會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象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,更新有關數據等等
複製代碼

2、初始化

在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

3、props初始化

// 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

  • 調用defineReactive方法把每一個prop對應的值變成響應式,能夠經過 vm._props.xxx 訪問到定義props中對應的屬性(defineReactive方法稍後介紹
  • 另外一個是經過proxy把vm._props.xxx的訪問代理到vm.xxx上

4、data初始化

// 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

  • 對定義data函數返回對象的遍歷,經過proxy把每個值vm._data.xxx都代理到vm.xxx上
  • 另外一個是調用observe方法觀測整個data的變化,把data也變成響應式,能夠經過vm._data.xxx訪問到定義data 返回函數中對應的屬性(observe稍後會介紹)

能夠看到,不管是props仍是data的初始化都是把它們變成響應式對象,其中主要函數有defineReactive,proxy,observe

5、此處有個小結

簡單理解是: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' // 不會有對應的響應!
複製代碼

至於爲何,繼續看下去便能知一二

6、proxy

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也如此

7、observe

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對象實例

8、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方法

9、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的具體實現,篇幅太多,下篇介紹

10、總結

響應式的聲明大概就是這樣:拿到配置中的data、props等屬性,對裏面的字段都進行響應式改造

  • 遇到值爲object, arr的,對key進行響應式改造,而後遍歷拿該值的內部字段繼續進行響應式改造
  • 遇到值爲基本類型的,對key進行響應式改造
  • 具體實現上面有說到,固然本身看一下源碼最好啦

是否是其實沒什麼捏

相關文章
相關標籤/搜索