Vue響應(yīng)式原理

Reactive-in-Depth.png

Vue數(shù)據(jù)劫持的實(shí)現(xiàn),做一個(gè)自己的理解&簡(jiǎn)單總結(jié)。雖然Vue3.0即將到來,我想Vue2.x也不至于馬上過時(shí)。

今天就從Vue2.x 與 Vue.3.0 數(shù)據(jù)劫持如何實(shí)現(xiàn)數(shù)據(jù)雙向綁定。

數(shù)據(jù)劫持: 指的是在訪問或者修改對(duì)象的某個(gè)屬性時(shí),通過一段代碼攔截這個(gè)行為,進(jìn)行額外的操作或者修改返回結(jié)果。

Vue2.x 選擇的 Object.defineProperty

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

我們來認(rèn)清Object.defineProperty的幾個(gè)局限性

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

我們做個(gè)類似Vue簡(jiǎn)易的數(shù)據(jù)劫持

  1. 視圖更新觸發(fā)的函數(shù)
// 當(dāng)我們監(jiān)聽的數(shù)據(jù)發(fā)生變化后調(diào)用改函數(shù)
function update() {
    console.log('數(shù)據(jù)變化啦,更新視圖')
}

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

// 調(diào)用監(jiān)聽函數(shù)監(jiān)聽 data
observer(data)

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

// 看到視圖確實(shí)更新了

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

// 控制臺(tái)什么都是沒有

  1. 想必你也發(fā)現(xiàn)了,監(jiān)聽data只到了對(duì)象的第一層。data深層次的數(shù)據(jù),并沒有被監(jiān)聽。所以我們需要對(duì)data做一個(gè)逐層遍歷(遞歸),直到把每個(gè)對(duì)象的每個(gè)屬性都調(diào)用 Object.defineProperty() 為止。
// 改改步驟二的代碼
function defineReactive(target, key, value) {
    // 在這里新增代碼
    // 當(dāng)value為object我們?cè)僮鲆淮螖?shù)據(jù)監(jiān)聽,直到value不是object為止
    if (typeof value === 'object') {
        observer(value)
    }
    
    // 以下代碼和步驟2沒有區(qū)別
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(val) {
            // 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
            if (val !== value) {
                 // 數(shù)據(jù)更新了,調(diào)用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 再對(duì)步驟5的修改做一次測(cè)試
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' } // 沒毛病,視圖更新了,但此時(shí)data.info的指向已經(jīng)發(fā)生了變化
// 然后再修改data.info.name
data.info.name = 'xy' // emmmmmm... 又是什么都沒有
  1. 我們針對(duì)步驟5再做一次修改
// 修改步驟5的代碼
function defineReactive(target, key, value) {
    if (typeof value === 'object') {
        observer(value)
    }
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(newVal) {
            // 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
            if (newVal !== value) {
                // 在這里新增代碼
                // 如果設(shè)置newVal是object,對(duì)newVal做監(jiān)聽
                if (typeof newVal === 'object') {
                    observer(newVal)
                }
                 // 數(shù)據(jù)更新了,調(diào)用update
                 update();
                 value = newVal;
            } 
        }
    })
}
  1. 再對(duì)步驟7的修改做一次測(cè)試
const data = {
    level: 1,
    info: {
        name: 'cc'
    }
}

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

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

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

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

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

結(jié)果明顯不是我們想要的。我們希望的是:Array原有的方法保持不變,但是又要引用到原來的方法的實(shí)現(xiàn)。

我們可以簡(jiǎn)單地處理下啦。

①先修改步驟10的代碼

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

// 遍歷methods數(shù)組
methods.forEach(method => {
    // 原生Array的原型方法
    const originalArray = Array.prototype[method]
    
    // 重寫Array原型上對(duì)應(yīng)的方式
    arrayList[method] = function() {
        // 做視圖更新或者渲染操作
        update();
        
        // 視圖更新了,調(diào)用對(duì)應(yīng)的原生方法
        // arguments 將該有的參數(shù)也傳進(jìn)來
        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) {
            // 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
            if (val !== value) {
                // 在這里新增代碼,如果設(shè)置val是object,對(duì)val做監(jiān)聽
                if (typeof val === 'object') {
                    // 通過鏈去找我們定義好的方法
                    if (Array.isArray(val)) {
            val.__proto__ = arrayList
          }
                    observer(val)
                }
                 // 數(shù)據(jù)更新了,調(diào)用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 完整代碼
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = []

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

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

        // 視圖更新了,調(diào)用對(duì)應(yīng)的原生方法
        // arguments 將該有的參數(shù)也傳進(jìn)來
        originalArray.call(this, ...arguments);
    }
})


// 當(dāng)我們監(jiān)聽的數(shù)據(jù)發(fā)生變化后調(diào)用改函數(shù)
function update() {
    console.log('數(shù)據(jù)變化啦,更新視圖')
}

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

    // 遍歷對(duì)象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) {
            // 如果改變的數(shù)據(jù)和原來一樣將不做任何處理
            if (newVal !== value) {
                // 在這里新增代碼,如果設(shè)置newVal是object,對(duì)newVal做監(jiān)聽
                if (typeof newVal === 'object') {
                    if (Array.isArray(newVal)) {
            newVal.__proto__ = arrayList
          }
                    observer(newVal)
                }
                 // 數(shù)據(jù)更新了,調(diào)用update
                 update();
                 value = newVal;
            }
        }
    })
}


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

observer(data)

// 自行打開注釋行測(cè)試即可

// ①
// 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)
*/


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

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

Vue3.0 選擇的 Proxy

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

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

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

const handler = {
  get(target, property) {
    // 如果值為對(duì)象,在對(duì)該值進(jìn)行數(shù)據(jù)劫持
    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最大的問題應(yīng)該就是兼容性了,但是3.0都準(zhǔn)備發(fā)布了,我們值得簡(jiǎn)單一試~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容