你瞭解vue3.0響應式數據怎麼實現嗎?

從 Proxy 提及

什麼是Proxy

proxy翻譯過來的意思就是」代理「,ES6對Proxy的定位就是target對象(原對象)的基礎上經過handler增長一層」攔截「,返回一個新的代理對象,以後全部在Proxy中被攔截的屬性,均可以定製化一些新的流程在上面,先看一個最簡單的例子javascript

const target = {}; // 要被代理的原對象
// 用於描述代理過程的handler
const handler = {
  getfunction (target, key, receiver{
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  setfunction (target, key, value, receiver{
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
}
// obj就是一個被新的代理對象
const obj = new Proxy(target, handler);
obj.a = 1 // setting a!
console.log(obj.a) // getting a!
複製代碼

上面的例子中咱們在target對象上架設了一層handler,其中攔截了針對target的get和set,而後咱們就能夠在get和set中間作一些額外的操做了html

注意1:對Proxy對象的賦值操做也會影響到原對象target,同時對target的操做也會影響Proxy,不過直接操做原對象的話不會觸發攔截的內容~vue

obj.a = 1// setting a!
console.log(target.a) // 1 不會打印 "getting a!"
複製代碼

注意2:若是handler中沒有任何攔截上的處理,那麼對代理對象的操做會直接通向原對象java

const target = {};
const handler = {};
const obj = new Proxy(target, handler);
obj.a = 1;
console.log(target.a) // 1
複製代碼

既然proxy也是一個對象,那麼它就能夠作爲原型對象,因此咱們把obj的原型指向到proxy上後,發現對obj的操做會找到原型上的代理對象,若是obj本身有a屬性,則不會觸發proxy上的get,這個應該很好理解git

const target = {};
const obj = {};
const handler = {
    getfunction(target, key){
            console.log(`get ${key} from ${JSON.stringify(target)}`);
            return Reflect.get(target, key);
    }
}
const proxy = new Proxy(target, handler);
Object.setPrototypeOf(obj, proxy);
proxy.a = 1;
obj.b = 1
console.log(obj.a) // get a from {"a": 1}   1
console.log(obj.b) // 1
複製代碼

ES6的Proxy實現了對哪些屬性的攔截?

經過上面的例子瞭解了Proxy的原理後,咱們來看下ES6目前實現了哪些屬性的攔截,以及他們分別能夠作什麼? 下面是 Proxy 支持的攔截操做一覽,一共 13 種es6

  1. get(target, propKey, receiver):攔截對象屬性的讀取,好比proxy.foo和proxy['foo'];
  2. set(target, propKey, value, receiver):攔截對象屬性的設置,好比proxy.foo = v或proxy['foo'] = v,返回一個布爾值;
  3. has(target, propKey):攔截propKey in proxy的操做,返回一個布爾值。
  4. deleteProperty(target, propKey):攔截delete proxy[propKey]的操做,返回一個布爾值;
  5. ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循環,返回一個數組。該方法返回目標對象全部自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性;
  6. getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象;
  7. defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值;
  8. preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值;
  9. getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象;
  10. isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值;
  11. setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。若是目標對象是函數,那麼還有兩種額外操做能夠攔截;
  12. apply(target, object, args):攔截 Proxy 實例做爲函數調用的操做,好比proxy(…args)、proxy.call(object, …args)、proxy.apply(…);
  13. construct(target, args):攔截 Proxy 實例做爲構造函數調用的操做,好比new proxy(…args);

以上是目前es6支持的proxy,具體的用法不作贅述,有興趣的能夠到阮一峯老師的es6入門去研究每種的具體用法,其實思想都是同樣的,只是每種對應了一些不一樣的功能~github

實際場景中 Proxy 能夠作什麼?

實現私有變量

js的語法中沒有private這個關鍵字來修飾私有變量,因此基本上全部的class的屬性都是能夠被訪問的,可是在有些場景下咱們須要使用到私有變量,如今業界的一些作法都是使用」_變量名「來」約定「這是一個私有變量,可是若是哪天被別人從外部改掉的話,咱們仍是沒有辦法阻止的,然而,當Proxy出現後,咱們能夠用代理來處理這種場景,看代碼:數據庫

const obj = {
    _name'nanjin',
    age19,
    getName() => {
        return this._name;
    },
    setName(newName) => {
        this._name = newName;
    }
}

const proxyObj = obj => new Proxy(obj, {
    get(target, key) => {
        if(key.startsWith('_')){
            throw new Error(`${key} is private key, please use get${key}`)
        }
        return Reflect.get(target, key);
    },
    set(target, key, newVal) => {
        if(key.startsWith('_')){
            throw new Error(`${key} is private key, please use set${key}`)
        }
        return Reflect.set(target, key, newVal);
    }
})

const newObj = proxyObj(obj);
console.log(newObj._name) // Uncaught Error: _name is private key, please use get_name
newObj._name = 'newname'// Uncaught Error: _name is private key, please use set_name
console.log(newObj.age) // 19
console.log(newObj.getName()) // nanjin
複製代碼

可見,經過proxyObj方法,咱們能夠實現把任何一個對象都過濾一次,而後返回新的代理對象,被處理的對象會把全部_開頭的變量給攔截掉,更進一步,若是有用過mobx的同窗會發現mobx裏面的store中的對象都是相似於這樣的設計模式

有handler 和 target,說明mobx自己也是用了代理模式,同時加上Decorator函數,在這裏就至關於把proxyObj使用裝飾器的方式來實現,Proxy + Decorator 就是mobx的核心原理啦~api

vue響應式數據實現

VUE的雙向綁定涉及到模板編譯,響應式數據,訂閱者模式等等,有興趣的能夠看這裏,由於這篇文章的主題是proxy,所以咱們着重介紹一下數據響應式的過程。

2.x版本

在當前的vue2.x的版本中,在data中聲名一個obj後,vue會利用Object.defineProperty來遞歸的給data中的數據加上get和set,而後每次set的時候,加入額外的邏輯。來觸發對應模板視圖的更新,看下僞代碼:

const defineReactiveData = data => {
    Object.keys(data).forEach(key => {
        let value = data[key];
        Object.defineProperty(data, key, {
         get : function(){
            console.log(`getting ${key}`)
            return value;
         },
         set : function(newValue){
            console.log(`setting ${key}`)
            notify() // 通知相關的模板進行編譯
            value = newValue;
         },
         enumerable : true,
         configurable : true
        })
    })
}
複製代碼

這個方法能夠給data上面的全部屬性都加上get和set,固然這只是僞代碼,實際場景下咱們還須要考慮若是某個屬性仍是對象咱們應該遞歸下去,來試試:

const data = {
    name: 'nanjing',
    age: 19
}
defineReactiveData(data)
data.name // getting name  'nanjing'
data.name = 'beijing';  // setting name
複製代碼

能夠看到當咱們get和set觸發的時候,已經可以同時觸發咱們想要調用的函數拉,Vue雙向綁定過程當中,當改變this上的data的時候去更新模板的核心原理就是這個方法,經過它咱們就能在data的某個屬性被set的時候,去觸發對應模板的更新。

如今咱們在來試試下面的代碼:

const data = {
    userIds: ['01','02','03','04','05']
}
defineReactiveData(data);
data.userIds // getting userIds ["01", "02", "03", "04", "05"]
// get 過程是沒有問題的,如今咱們嘗試給數組中push一個數據
data.userIds.push('06'// getting userIds 
複製代碼

what ? setting沒有被觸發,反而由於取了一次userIds因此觸發了一次getting~,
不只如此,不少數組的方法都不會觸發setting,好比:push,pop,shift,unshift,splice,sort,reverse這些方法都會改變數組,可是不會觸發set,因此Vue爲了解決這個問題,從新包裝了這些函數,同時當這些方法被調用的時候,手動去觸發notify();看下源碼:

// 得到數組原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重寫如下函數
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]
methodsToPatch.forEach(function(method{
  // 緩存原生函數
  const original = arrayProto[method]
  // 重寫函數
  def(arrayMethods, method, function mutator(...args{
    // 先調用原生函數得到結果
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    // 調用如下幾個函數時,監聽新數據
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 手動派發更新
    ob.dep.notify()
    return result
  })
})
複製代碼

上面是官方的源碼,咱們能夠實現一下push的僞代碼,爲了省事,直接在prototype上下手了~

const push = Array.prototype.push;
Array.prototype.push = function(...args){
    console.log('push is happenning');
    return push.apply(this, args);
}
data.userIds.push('123'// push is happenning
複製代碼

經過這種方式,咱們能夠監聽到這些的變化,可是vue官方文檔中有這麼一個注意事項

因爲 JavaScript 的限制,Vue 不能檢測如下變更的數組:

  • 當你利用索引直接設置一個項時,例如:vm.items[indexOfItem] = newValue
  • 當你修改數組的長度時,例如:vm.items.length = newLength
    這個最根本的緣由是由於這2種狀況下,受制於js自己沒法實現監聽,因此官方建議用他們本身提供的內置api來實現,咱們也能夠理解到這裏既不是defineProperty能夠處理的,也不是包一層函數就能解決的,這就是2.x版本如今的一個問。

回到這篇文章的主題,vue官方會在3.x的版本中使用proxy來代替defineProperty處理響應式數據的過程,咱們先來模擬一下實現,看看可否解決當前遇到的這些問題;

3.x版本

咱們先來經過proxy實現對data對象的get和set的劫持,並返回一個代理的對象,注意,咱們只關注proxy自己,全部的實現都是僞代碼,有興趣的同窗能夠自行完善

const defineReactiveProxyData = data => new Proxy(data, 
    {
        getfunction(data, key){
            console.log(`getting ${key}`)
            return Reflect.get(data, key);
        },
        setfunction(data, key, newVal){
            console.log(`setting ${key}`);
            if(typeof newVal === 'object'){ // 若是是object,遞歸設置代理
                return Reflect.set(data, key, defineReactiveProxyData(newVal));
            }
            return Reflect.set(data, key, newVal);
        }
    })
const data = {
    name'nanjing',
    age19
};
const vm = defineReactiveProxyData(data);
vm.name // getting name  nanjing
vm.age = 20// setting age  20
複製代碼

看起來咱們的代理已經起做用啦,以後只要在setting的時候加上notify()去通知模板進行編譯就能夠了,而後咱們來嘗試設置一個數組看看;

vm.userIds = [1,2,3] //  setting userIds
vm.userIds.push(1);
// getting userIds 由於咱們會先訪問一次userids
// getting push 調用了push方法,因此會訪問一次push屬性
// getting length 數組push的時候 length會變,因此須要先訪問原來的length
// setting 3 經過下標設置的,因此set當前的index3
// setting length 改變了數組的長度,因此會set length
// 4 返回新的數組的長度
複製代碼

回顧2.x遇到的第一個問題,須要從新包裝Array.prototype上的一些方法,使用了proxy後不須要了,解決了~,繼續看下一個問題

vm.userIds.length = 2
// getting userIds 先訪問
// setting length 在設置
vm.userIds[1] = '123'
// getting userIds 先訪問
// setting 1 設置index=1的item
// "123"
複製代碼

從上面的例子中咱們能夠看到,不論是直接改變數組的length仍是經過某一個下標改變數組的內容,proxy都能攔截到此次變化,這比defineProperty方便太多了,2.x版本中的第二個問題,在proxy中根本不會出現了。

總結1

經過上面的例子和代碼,咱們看到Vue的響應模式若是使用proxy會比如今的實現方式要簡化和優化不少,很快在即未來臨的3.0版本中,你們就能夠體驗到了。不過由於proxy自己是有兼容性的,好比ie瀏覽器,因此在低版本的場景下,vue會回退到如今的實現方式。

總結2

迴歸到proxy自己,設計模式中有一種典型的代理模式,proxy就是js的一種實現,它的好處在於,我能夠在不污染自己對象的條件下,生成一個新的代理對象,全部的一些針對性邏輯放到代理對象上去實現,這樣我能夠由A對象,衍生出B,C,D…每一個的處理過程都不同,從而簡化代碼的複雜性,提高必定的可讀性,好比用proxy實現數據庫的ORM就是一種很好的應用,其實代碼很簡單,關鍵是要理解背後的思想,同時可以觸類旁通~

擴展:

1.Proxy.revocable()

這個方法能夠返回一個可取消的代理對象

const obj = {};
const handler = {};
const {proxy, revoke} = Proxy.revocable(obj, handler);
proxy.a = 1
proxy.a // 1
revoke();
proxy.a // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
複製代碼

一旦代理被取消了,就不能再從代理對象訪問了

打印proxy 能夠看到IsRevoked變爲true了

2.代理對象的this問題

由於new Proxy出來的是一個新的對象,因此在若是你在target中有使用this,被代理後的this將指向新的代理對象,而不是原來的對象,這個時候,若是有些函數是原對象獨有的,就會出現this指向致使的問題,這種場景下,建議使用bind來強制綁定this

看代碼:

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate(); // Uncaught TypeError: this is not a Date object.
複製代碼

由於代理後的對象並非一個Date類型的,不具備getDate方法的,因此咱們須要在get的時候,綁定一下this的指向

const target = new Date();
const handler = {
    getfunction(target, key){
        if(typeof target[key] === 'function'){
            return target[key].bind(target) // 強制綁定
            this到原對象
        }
        return Reflect.get(target, key)
    }
};
const proxy = new Proxy(target, handler);

proxy.getDate(); // 6
複製代碼

這樣就能夠正常使用this啦,固然具體的使用還要看具體的場景,靈活運用吧!

僞代碼部分都是筆者揣摩寫的,若有問題,歡迎指正~

相關文章
相關標籤/搜索