vue2.x響應式原理總結


vue作為前端使用廣泛的三大框架(react、vue、Angular)之一,vue2.x的雙向數據綁定是基於Object.defineProperty實現。

 vue2.x雙向數據綁定解析

vue2.x是利用Object.defineProperty劫持對象或對象的屬性的訪問器,在屬性值發生變化時獲取屬性值變化, 從而進行后續操作。

1、Object.defineProperty在js中的描述:

Object.defineProperty(obj, prop, descriptor) 直接在一個對象上定義一個屬性,或者修改一個對象的現有 屬性,並返回這個對象。

參數:obj 要在其上定義屬性的對象;prop 要定義或修改的屬性的名稱;descriptor 將被定義或修改的屬性描述符。

返回值: 傳遞給函數的對象obj

// 定義一個對象
const data={name:'peak',age:10}

// 遍歷對象 實現對對象的屬性進行劫持
Object.keys(data).forEach((key) => {
   Object.defineProperty(data, key, {
     // 當且僅當該屬性的enumerable為true時,該屬性才能夠出現在對象的枚舉屬性中
     enumerable: true,
     // 當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除
     configurable: true, 
     get: ()=>{ // 一個給屬性提供 getter 的方法
       console.info(`get ${key}-${val}`)
       return val;
     },
     set: (newVal)=>{ // 一個給屬性提供 setter 的方法
       // 當屬性值發生變化時我們可以進行額外操作 如調用監聽器
       if(newVal === val ){ // 如果未發生變化 不做其他操作
        return;
       } 
       console.log(`觸發視圖更新函數${newVal}`);
     },
   });   
});

data.age=25 // 觸發set方法 

2、基於Object.defineProperty的數據劫持優勢以及實現方式

Object.defineProperty的對象以及對象屬性的劫持有以下優勢:

(1)無需顯示調用,如Vue2.x使用Object.defineProperty對象以及對象屬性的劫持+發布訂閱模式,只要數據發生變化直接通知變化 並驅動視圖更新。

(2)可在set函數中精確得知變化數據而不用逐個遍歷屬性獲取變化值,減少性能損耗。

實現思路:

(1)利用Object.defineProperty重新定義一遍目標對象,完成對目標對象的劫持,在屬性值變化后即觸發set方法 后通知訂閱者,告訴該對象的某個屬性值發生了變化。

(2)解析器Compile解析模板中的指令,收集指令所依賴的方法和數據,等待數據變化然后進行渲染。

(3)Watcher在收到屬性值發生變化后,根據解析器Compile提供的指令進行視圖渲染。

為更好的說明vue2.x的響應式原理,下面vue2.x的源碼引用了Vue源碼解讀

監聽數據變化

對data進行改造,所有屬性設置set&get,用於在屬性獲取或者設置時,添加邏輯

// Dep用於訂閱者的存儲和收集,將在下面實現
import Dep from 'Dep'
// Observer類用於給data屬性添加set&get方法
export default class Observer{
    constructor(value){
        this.value = value
        this.walk(value)
    }
    walk(value){
        Object.keys(value).forEach(key => this.convert(key, value[key]))
    }
    convert(key, val){
        defineReactive(this.value, key, val)
    }
}
export function defineReactive(obj, key, val){
    var dep = new Dep()
    // 給當前屬性的值添加監聽
    var chlidOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=> {
            console.log('get value')
            // 如果Dep類存在target屬性,將其添加到dep實例的subs數組中
            // target指向一個Watcher實例,每個Watcher都是一個訂閱者
            // Watcher實例在實例化過程中,會讀取data中的某個屬性,從而觸發當前get方法
            // 此處的問題是:並不是每次Dep.target有值時都需要添加到訂閱者管理員中去管理,需要對訂閱者去重,不影響整體思路,不去管它
            if(Dep.target){
                dep.addSub(Dep.target)
            }
            return val
        },
        set: (newVal) => {
            console.log('new value seted')
            if(val === newVal) return
            val = newVal
            // 對新值進行監聽
            chlidOb = observe(newVal)
            // 通知所有訂閱者,數值被改變了
            dep.notify()
        }
    })
}
export function observe(value){
    // 當值不存在,或者不是復雜數據類型時,不再需要繼續深入監聽
    if(!value || typeof value !== 'object'){
        return
    }
    return new Observer(value)
}

管理訂閱者

對訂閱者進行收集、存儲和通知

export default class Dep{
    constructor(){
        this.subs = []
    }
    addSub(sub){
        this.subs.push(sub)
    }
    notify(){
        // 通知所有的訂閱者(Watcher),觸發訂閱者的相應邏輯處理
        this.subs.forEach((sub) => sub.update())
    }
}

訂閱者

每個訂閱者都是對某條數據的訂閱,訂閱者維護着每一次更新之前的數據,將其和更新之后的數據進行對比,如果發生了變化,則執行相應的業務邏輯,並更新訂閱者中維護的數據的值

import Dep from 'Dep'
export default class Watcher{
    constructor(vm, expOrFn, cb){
        this.vm = vm // 被訂閱的數據一定來自於當前Vue實例
        this.cb = cb // 當數據更新時想要做的事情
        this.expOrFn = expOrFn // 被訂閱的數據
        this.val = this.get() // 維護更新之前的數據
    }
    // 對外暴露的接口,用於在訂閱的數據被更新時,由訂閱者管理員(Dep)調用
    update(){
        this.run()
    }
    run(){
        const val = this.get()
        if(val !== this.val){
            this.val = val;
            this.cb.call(this.vm)
        }
    }
    get(){
        // 當前訂閱者(Watcher)讀取被訂閱數據的最新更新后的值時,通知訂閱者管理員收集當前訂閱者
        Dep.target = this
        const val = this.vm._data[this.expOrFn]
        // 置空,用於下一個Watcher使用
        Dep.target = null
        return val;
    }
}

Vue

將數據代理到Vue實例上,真實數據存儲於實例的_data屬性中

import Observer, {observe} from 'Observer'
import Watcher from 'Watcher'
export default class Vue{
    constructor(options = {}){
        // 簡化了$options的處理
        this.$options = options
        // 簡化了對data的處理
        let data = this._data = this.$options.data
        // 將所有data最外層屬性代理到Vue實例上
        Object.keys(data).forEach(key => this._proxy(key))
        // 監聽數據
        observe(data)
    }
    // 對外暴露調用訂閱者的接口,內部主要在指令中使用訂閱者
    $watch(expOrFn, cb){
        new Watcher(this, expOrFn, cb)
    }
    _proxy(key){
        Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get: () => this._data[key],
            set: (val) => {
                this._data[key] = val
            } 
        })
    }
}

調用這個極簡版演示數據雙向綁定原理的Vue

import Vue from './Vue';
let demo = new Vue({
    data: {
        'a': {
            'ab': {
                'c': 'C'
            }
        },
        'b': {
            'bb': 'BB'
        },
        'c': 'C'
    }
});
demo.$watch('c', () => console.log('c is changed'))
// get value
demo.c = 'CCC'
// new value seted
// get value
// c is changed
demo.c = 'DDD'
// new value seted
// get value
// c is changed
demo.a
// get value
demo.a.ab = {
    'd': 'D'
}
// get value
// get value
// new value seted
console.log(demo.a.ab)
// get value
// get value
// {get d: (), set d: ()}
demo.a.ab.d = 'DD'
// get value
// get value
// new value seted
console.log(demo.a.ab);
// get value
// get value
// {get d: (), set d: ()}

總結:

在一些技術博客上,有人指出Object.defineProperty存在缺陷,只能監聽到非數組對象的變化,而監聽不到數組的變化,實際上這是錯誤的理解,Object.defineProperty是可以監聽數組變化的,只是從性能/體驗的性價比考慮,放棄了這個特性,vue設置

7個變異數組(pushpopshiftunshiftsplicesortreverse)改用hack的方式解決數組變化的問題。

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM