Vue響應式原理

Reactive-in-Depth.png

Vue數據劫持的實現,做一個自己的理解&簡單總結。雖然Vue3.0即將到來,我想Vue2.x也不至于馬上過時。

今天就從Vue2.x 與 Vue.3.0 數據劫持如何實現數據雙向綁定。

數據劫持: 指的是在訪問或者修改對象的某個屬性時,通過一段代碼攔截這個行為,進行額外的操作或者修改返回結果。

Vue2.x 選擇的 Object.defineProperty

Object.defineProperty 對大家都來說應該不陌生了。算是面試的一道必考題?(細品:那掌握好了是不是就是一道送分題呢?)可以點擊這里回顧一下 Object.defineProperty文檔

我們來認清Object.defineProperty的幾個局限性

  • 兼容性是IE8+,這也就是為什么Vue不支持IE8及以下版本的原因
  • 不能監聽數組的變化,Vue通過重寫數組原型的方法來實現數據劫持。
  • 對于深層次嵌套對象需要做遞歸遍歷。
  • 必須遍歷對象的每個屬性。如果要擴展該對象,就必須手動去為新的屬性設置setter、getter方法。 這也就是為什么Vue開發中的不在 data 中聲明的屬性無法自動擁有雙向綁定效果的原因。需要我們手動去調用Vue.set()

我們做個類似Vue簡易的數據劫持

  1. 視圖更新觸發的函數
// 當我們監聽的數據發生變化后調用改函數
function update() {
    console.log('數據變化啦,更新視圖')
}

  1. 通過 Object.defineProperty 處理 data 中的每個屬性
// 通過 Object.defineProperty 處理 target 中的每個屬性 key
function defineReactive(target, key, value) {
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(val) {
            // 如果改變的數據和原來一樣將不做任何處理
            if (val !== value) {
                 // 數據更新了,調用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 監聽data的函數
function observer(target) {
    // 如果不是對象,直接返回;如果是null也直接返回
    if (typeof target !== 'object' || !target) return target;
    
    // 遍歷對象obj的所有key,完成屬性配置
    Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}
  1. 測試步驟1、2、3
// 需要監聽的data對象
const data = {
    level: 1,
    info: {
        name: 'cc'
    }
}

// 調用監聽函數監聽 data
observer(data)

// 修改data的值 視圖更新
data.level = 2

// 看到視圖確實更新了

// 我們不妨嘗試了一下data深層次對象的修改
data.info.name = 'yy'

// 控制臺什么都是沒有

  1. 想必你也發現了,監聽data只到了對象的第一層。data深層次的數據,并沒有被監聽。所以我們需要對data做一個逐層遍歷(遞歸),直到把每個對象的每個屬性都調用 Object.defineProperty() 為止。
// 改改步驟二的代碼
function defineReactive(target, key, value) {
    // 在這里新增代碼
    // 當value為object我們再做一次數據監聽,直到value不是object為止
    if (typeof value === 'object') {
        observer(value)
    }
    
    // 以下代碼和步驟2沒有區別
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(val) {
            // 如果改變的數據和原來一樣將不做任何處理
            if (val !== value) {
                 // 數據更新了,調用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 再對步驟5的修改做一次測試
const data = {
    level: 1,
    info: {
        name: 'cc'
    },
    a: {
        a: {
            a: {
                a: 1
            }
        }
    }
}

// 我們嘗試改變data.info.name的值
data.info.name = 'xy'  // 視圖更新了!

// 我們嘗試跟深層次的修改
data.a.a.a.a = 2  // ok 視圖也更新了

// 那么我再試試其他方式
// 先修改data.info的值
data.info = { name: 'cc' } // 沒毛病,視圖更新了,但此時data.info的指向已經發生了變化
// 然后再修改data.info.name
data.info.name = 'xy' // emmmmmm... 又是什么都沒有
  1. 我們針對步驟5再做一次修改
// 修改步驟5的代碼
function defineReactive(target, key, value) {
    if (typeof value === 'object') {
        observer(value)
    }
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(newVal) {
            // 如果改變的數據和原來一樣將不做任何處理
            if (newVal !== value) {
                // 在這里新增代碼
                // 如果設置newVal是object,對newVal做監聽
                if (typeof newVal === 'object') {
                    observer(newVal)
                }
                 // 數據更新了,調用update
                 update();
                 value = newVal;
            } 
        }
    })
}
  1. 再對步驟7的修改做一次測試
const data = {
    level: 1,
    info: {
        name: 'cc'
    }
}

// 先修改data.info的值
data.info = { name: 'cc' } // 沒毛病,視圖更新了
// 然后再修改data.info.name
data.info.name = 'xy' // 也沒毛病,視圖更新了
  1. 我們都知道typeof 數據返回的也是object
const data = {
    arr: []
}

// 嘗試對數組做更改
arr.push(1); // 然鵝,并沒有任何輸出
  1. 前面有說明Object.defineProperty 對數組是起不到任何作用的。那Vue如何實現的呢? Vue是通過修改數組的原型方法來實現數據劫持(做一些視圖更新、渲染的操作)。
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

// 遍歷methods數組
methods.forEach(method => {
    // 原生Array的原型方法
    const originalArray = Array.prototype[method]
    
    // 重寫Array原型上對應的方式
    Array.prototype[method] = function() {
        // 做視圖更新或者渲染操作
        update();
        
        // 視圖更新了,調用對應的原生方法
        // arguments 將該有的參數也傳進來
        originalArray.call(this, ...arguments);
    }
})
  1. 又到了驗證一下步驟10的時候啦!
const data = {
    arr: []
}

data.arr.push(1) // 視圖更新了
  1. 看了上面的代碼,可能就有疑問了。我們明顯直接修改的是 Array.prototype的方法。這樣會導致一個問題。沒有被監聽的數組,也會觸發update()。如下:
var normalArray = [];

normalArray.push(1); // wtf 竟然也觸發了視圖更新

結果明顯不是我們想要的。我們希望的是:Array原有的方法保持不變,但是又要引用到原來的方法的實現。

我們可以簡單地處理下啦。

①先修改步驟10的代碼

const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = [] 

// 遍歷methods數組
methods.forEach(method => {
    // 原生Array的原型方法
    const originalArray = Array.prototype[method]
    
    // 重寫Array原型上對應的方式
    arrayList[method] = function() {
        // 做視圖更新或者渲染操作
        update();
        
        // 視圖更新了,調用對應的原生方法
        // arguments 將該有的參數也傳進來
        originalArray.call(this, ...arguments);
    }
})

②再修改步驟7的代碼

function defineReactive(target, key, value) {
    if (typeof value === 'object') {
        // 通過鏈去找我們定義好的方法
        if (Array.isArray(value)) {
            value.__proto__ = arrayList
        }
        observer(value)
    }
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(val) {
            // 如果改變的數據和原來一樣將不做任何處理
            if (val !== value) {
                // 在這里新增代碼,如果設置val是object,對val做監聽
                if (typeof val === 'object') {
                    // 通過鏈去找我們定義好的方法
                    if (Array.isArray(val)) {
            val.__proto__ = arrayList
          }
                    observer(val)
                }
                 // 數據更新了,調用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 完整代碼
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = []

// 遍歷methods數組
methods.forEach(method => {
    // 原生Array的原型方法
    const originalArray = Array.prototype[method]

    // 重寫Array原型上對應的方式
    arrayList[method] = function() {
        // 做視圖更新或者渲染操作
        update();

        // 視圖更新了,調用對應的原生方法
        // arguments 將該有的參數也傳進來
        originalArray.call(this, ...arguments);
    }
})


// 當我們監聽的數據發生變化后調用改函數
function update() {
    console.log('數據變化啦,更新視圖')
}

function observer(target) {
    // 如果不是對象,直接返回
    if (typeof target !== 'object' || !target) return target;

    // 遍歷對象obj的所有key,完成屬性配置
    Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}


function defineReactive(target, key, value) {
    if (typeof value === 'object') {
        if (Array.isArray(value)) {
            value.__proto__ = arrayList
        }
        observer(value)
    }
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(newVal) {
            // 如果改變的數據和原來一樣將不做任何處理
            if (newVal !== value) {
                // 在這里新增代碼,如果設置newVal是object,對newVal做監聽
                if (typeof newVal === 'object') {
                    if (Array.isArray(newVal)) {
            newVal.__proto__ = arrayList
          }
                    observer(newVal)
                }
                 // 數據更新了,調用update
                 update();
                 value = newVal;
            }
        }
    })
}


const data = {
  level: 1,
  info: {
    name: 'cc'
  },
  arr: []
}

observer(data)

// 自行打開注釋行測試即可

// ①
// data.level = 2

// ②
// data.info.name = 'xy'

// ③
/*
data.info = {name: 'cc'}
data.info.name = 'xy'
*/

// ④
// data.arr.push(1)

// ⑤
/*
data.arr = []
data.arr.push(1)
*/


值得注意的是:數組不支持長度的修改,也不支持通過數組的索引進行更改。例如以下方式是不會觸發視圖更新,只有上面列舉的7個方式或者直接替換一個新的數組才會觸發視圖更新。數組更新檢測

data.arr.length = 3
data.arr[1] = 1

Vue3.0 選擇的 Proxy

Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。

function update() {
  console.log('數據變化啦,更新視圖')
}

const data = {
  level: 1,
  info: {
    name: 'cc'
  },
  arr: []
}

const handler = {
  get(target, property) {
    // 如果值為對象,在對該值進行數據劫持
    if (typeof target[property] === 'object' && target[property] !== null) {
      return new Proxy(target[property], handler)
    }
    return Reflect.get(target, property)
  },

  set(target, property, value) {
    if (property === 'length') {
      return true
    }
    update()
    return Reflect.set(target, property, value)
  }
}

const proxy = new Proxy(data, handler)

proxy.level = 2
proxy.info.name = 'yy'
proxy.arr.push(1)
proxy.arr[1] = 1

Proxy最大的問題應該就是兼容性了,但是3.0都準備發布了,我們值得簡單一試~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。