vue響應式原理學習(二)— Observer的實現

以前個人一篇文章vue響應式原理學習(一)講述了vue數據響應式原理的一些簡單知識。 衆所周知,Vuedata屬性,是默認深度監聽的,此次咱們再深度分析下,Observer觀察者的源碼實現。javascript

先寫個深拷貝熱熱身

既然data屬性是被深度監聽,那咱們就首先本身實現一個簡單的深拷貝,理解下思路。html

深拷貝的原理有點像遞歸, 其實就是遇到引用類型,調用自身函數再次解析。vue

function deepCopy(source) {
    // 類型校驗,若是不是引用類型 或 全等於null,直接返回
    if (source === null || typeof source !== 'object') {
        return source;
    }

    let isArray = Array.isArray(source),
        result = isArray ? [] : {};
        
    // 遍歷屬性
    if (isArray) {
        for(let i = 0, len = source.length; i < len; i++) {
            let val = source[i];
            // typeof [] === 'object', typeof {} === 'object'
            // 考慮到 typeof null === 'object' 的狀況, 因此要加個判斷
            if (val && typeof val === 'object') {
                result[i] = deepCopy(val);
            } else {
                result[i] = val;
            }
        }
        // 簡寫 
        // result = source.map(item => {
        // return (item && typeof item === 'object') ? deepCopy(item) : item
        // });
    } else {
        const keys = Object.keys(source);
        for(let i = 0, len = keys.length; i < len; i++) {
            let key = keys[i],
                val = source[key];
            if (val && typeof val === 'object') {
                result[key] = deepCopy(val);
            } else {
                result[key] = val;
            }
        }
        // 簡寫
        // keys.forEach((key) => {
        // let val = source[key]; 
        // result[key] = (val && typeof val === 'object') ? deepCopy(val) : val; 
        // });
    }
    
    return result;
}
複製代碼

爲何是簡單的深拷貝,由於沒考慮 RegExp, Date, 原型鏈,DOM/BOM對象等等。要寫好一個深拷貝,不簡單。java

有的同窗可能會問,爲何不直接一個 for in 解決。以下:react

function deepCopy(source) {
    let result = Array.isArray(source) ? [] : {};
    
    // 遍歷對象
    for(let key in source) {
        let val = source[key];
        result[key] = (val && typeof val === 'object') ? deepCopy(val) : val;
    }

    return result;
}
複製代碼

其實 for in有一個痛點就是原型鏈上的非內置方法也會被遍歷。例如開發者本身在對象的 prototype上擴展的方法。api

又有的同窗可能會說,加 hasOwnProperty 解決呀。若是是 Object 類型,確實能夠解決,但如何是 Array 的話,就獲取不到數組的索引啦。數組

說到 for in,再加個注意項,就是 for in 也是能夠 continue 的,而數組的 forEach 方法不能夠。由於 forEach的內部實現是在一個for循環中依次執行你傳入的函數。函數

分析 Vue 的 Observer

這裏我主要是爲代碼添加註釋,建議看官們最好打開源碼來看。post

代碼來源:Vue項目下的 src/core/observer/index.js學習

Vue 將 Observer 封裝成了一個 class

Observer
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
        // 每觀察一個對象,就在對象上添加 __ob__ 屬性,值爲當前 Observer 實例
        // 固然,前提是 value 自己是一個數組或對象,而非基礎數據類型,如數字,字符串等。
        def(value, '__ob__', this)   
        
        // 若是是數組
        if (Array.isArray(value)) {
            // 這兩行代碼後面再講解
            // 這裏代碼的做用是 爲數組的操做函數賦能
            // 也就是,當咱們使用 push pop splice 等數組的api時,也能夠觸發數據響應,更新視圖。
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arrayKeys)
            
            // 遍歷數組並觀察
            this.observeArray(value)
        } else {
            // 遍歷對象並觀察
            // 這裏會有存在 value 不是 Object 的狀況,
            // 不過沒事,Object.keys的參數爲數字,字符串時 會 返回一個空數組。
            this.walk(value)
        }
    }

    // 遍歷對象並觀察
    walk(obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            // 觀察對象,defineReactive 函數內部調用了 observe 方法, 
            // observe 內部 調用了 Observer 構造函數
            defineReactive(obj, keys[i])
        }
    }

    // 遍歷數組並觀察
    observeArray(items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            // 觀察對象,observe 內部 調用了 Observer 構造函數
            observe(items[i])
        }
    }
}

function protoAugment(target, src: Object, keys: any) {
    target.__proto__ = src
}

function copyAugment(target: Object, src: Object, keys: Array<string>) {
    for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}
複製代碼

上面的代碼中,細心的同窗可能對observedefdefineReactive這些函數不明因此,接下來講說這幾個函數

observe 函數

用來調用 Observer構造函數

export function observe(value: any, asRootData: ?boolean): Observer | void {
    // 若是不是對象,或者是VNode實例,直接返回。
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    // 定義一個 變量,用來存儲 Observer 實例
    let ob: Observer | void
    // 若是對象已經被觀察過,Vue會自動給對象加上一個 __ob__ 屬性,避免重複觀察
    // 若是對象上已經有 __ob__屬性,表示已經被觀察過,就直接返回 __ob__
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
    } else if (
        shouldObserve &&       // 是否應該觀察
        !isServerRendering() &&  // 非服務端渲染
        (Array.isArray(value) || isPlainObject(value)) &&     // 是數組或者Object對象
        Object.isExtensible(value) &&     // 對象是否可擴展,也就是是否可向對象添加新屬性
        !value._isVue // 非 Vue 實例
    ) {
        ob = new Observer(value) 
    }
    if (asRootData && ob) {  // 暫時還不清楚,不過咱們能夠先忽略它
        ob.vmCount++
    }  
    return ob  // 返回 Observer 實例
}
複製代碼

能夠發現 observe 函數,只是 返回 一個 Observer 實例,只是多了些許判斷。爲了方便理解,咱們徹底能夠把代碼縮減:

// 這就清晰多了
function observe(value) {
    let ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.___ob___
    } else {
        ob = new Observer(value) 
    }
    return ob;
}
複製代碼
def 函數

其實就是 Object.defineProperty 的封裝

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
    Object.defineProperty(obj, key, {
        value: val,
        // 默認不可枚舉,也就意味着正常狀況,Vue幫咱們在對象上添加的 __ob__屬性,是遍歷不到的
        enumerable: !!enumerable,  
        writable: true,
        configurable: true
    })
}
複製代碼
defineReactive 函數

defineReactive函數的功能較多,主要是用來 初始化時收集依賴改變屬性時觸發依賴

export function defineReactive( obj: Object, // 被觀察對象 key: string, // 對象的屬性 val: any, // 用戶給屬性賦值 customSetter?: ?Function, // 用戶額外自定義的 set shallow?: boolean // 是否深度觀察 ) {
    // 用於收集依賴
    const dep = new Dep()

    // 若是不可修改,直接返回
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }
    
    
    // 若是用戶本身 未在對象上定義get 或 已在對象上定義set,且用戶沒有傳入 val 參數
    // 則先計算對象的初始值,賦值給 val 參數
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }

    // !shallow 表示 深度觀察,shallow 不爲 true 的狀況下,表示默認深度觀察
    // 若是是深度觀察,執行 observe 方法觀察對象
    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

            // 判斷值是否改變
            // (newVal !== newVal && value !== value) 用來判斷 NaN !== NaN 的狀況
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            
            // 非生產環境,觸發用戶額外自定義的 setter
            if (process.env.NODE_ENV !== 'production' && customSetter) {
                customSetter()
            }
            
            // 觸發對象原有的 setter,若是沒有的話,用新值(newVal)覆蓋舊值(val)
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }

            // 若是是深度觀察,屬性被更改後,從新觀察
            childOb = !shallow && observe(newVal)
            
            // 觸發依賴。收集依賴和觸發依賴是個比較大的流程,往後再說
            dep.notify()
        }
    })
}
複製代碼
入口在哪

說了這麼多,那Vue觀察對象的初始化入口在哪裏呢,固然是在初始化Vue實例的地方了,也就是 new Vue 的時候。

代碼來源:Vue項目下src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)    // 這個方法 定義在 initMixin 函數內
}

// 就是這裏,initMixin 函數會在 Vue 的 prototype 上擴展一個 _init 方法
// 咱們 new Vue 的時候就是執行的 this._init(options) 方法
initMixin(Vue)  

stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
複製代碼

initMixin 函數在 Vue.prototype 上擴展一個 _init 方法,_init方法會有一個initState函數進行數據初始化

initState(vm)   // vm 爲當前 Vue 實例,Vue 會將咱們傳入的 data 屬性賦值給 vm._data 
複製代碼

initState 函數會在內部執行一段代碼,觀察 vm實例上的data屬性

代碼來源:Vue項目下 src/core/instance/state.js。無用的代碼我先註釋掉了,只保留初始化 data 的代碼。

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)
    
    // 若是傳入了 data 屬性
    // 這裏的 data 就是咱們 new Vue 時傳入的 data 屬性
    if (opts.data) {    
        // initData 內部會將 咱們傳入的 data屬性 規範化。
        // 若是傳入的 data 不是函數,則直接 observe(data)
        // 若是傳入的 data 是函數,會先執行函數,將 返回值 賦值給 data,覆蓋原有的值,再observe(data)。
        // 這也就是爲何咱們寫組件時 data 能夠傳入一個函數
        initData(vm)    
    } else {
        // 若是沒傳入 data 屬性,觀察一個空對象
        observe(vm._data = {}, true /* asRootData */)
    }
    
    // if (opts.computed) initComputed(vm, opts.computed)
    // if (opts.watch && opts.watch !== nativeWatch) {
    // initWatch(vm, opts.watch)
    // }
}
複製代碼
總結

咱們 new Vue 的時候 Vue 對咱們傳入的 data 屬性到底作了什麼操做?

  1. 若是咱們傳入的 data 是一個函數,會先執行函數獲得返回值。並賦值覆蓋 data。若是傳入的是對象,則不作操做。
  2. 執行 observe(data)
    • observe 內部會執行 new Observer(data)
    • new Observer(data) 會在 data對象 上擴展一個不可枚舉的屬性 __ob__,這個屬性有大做用。
    • 若是 data 是個數組
      • 執行 observeArray(data)。這個方法會遍歷data對象,並對每個數組項執行observe以後的流程參考第2步
    • 若是 data 是對象
      • 執行 walk(data)。這個方法會遍歷data對象,並對每個屬性執行 defineReactive
      • defineReactive 內部會對傳入的對象屬性執行 observe以後的流程參考第2步

篇幅和精力有限,關於 protoAugmentcopyAugment的做用,defineReactive 內如何收集依賴與觸發依賴的實現,往後再說。

文章內容若是有錯誤之處,還請指出。

參考:

JavaScript 如何完整實現深度Clone對象

Vue 技術內幕

相關文章
相關標籤/搜索