實(shí)現(xiàn)雙向綁定Object.defineProperty與proxy的VS
關(guān)注 入坑互聯(lián)網(wǎng) ,回復(fù)“加群”
加入我們一起學(xué)習(xí),天天進(jìn)步
實(shí)現(xiàn)雙向綁定的方法有很多,KnockoutJS基于觀察者模式的雙向綁定,Ember基于數(shù)據(jù)模型的雙向綁定,Angular基于臟檢查的雙向綁定,本篇文章我們重點(diǎn)講面試中常見(jiàn)的基于數(shù)據(jù)劫持的雙向綁定。
常見(jiàn)的基于數(shù)據(jù)劫持的雙向綁定有兩種實(shí)現(xiàn),一個(gè)是目前Vue在用的Object.defineProperty,另一個(gè)是ES2015中新增的Proxy,而Vue的作者宣稱將在Vue3.0版本后加入Proxy從而代替Object.defineProperty,通過(guò)本文你也可以知道為什么Vue未來(lái)會(huì)選擇Proxy。
(嚴(yán)格來(lái)講Proxy應(yīng)該被稱為『代理』而非『劫持』,不過(guò)由于作用有很多相似之處,我們?cè)谙挛闹芯筒辉僮鰠^(qū)分,統(tǒng)一叫『劫持』。)
什么是數(shù)據(jù)劫持
指的是在訪問(wèn)或者修改對(duì)象的某個(gè)屬性時(shí),通過(guò)一段代碼攔截這個(gè)行為,進(jìn)行額外的操作或者修改返回結(jié)果。
比較典型的是 Object.defineProperty() 和 ES2015 中新增的 Proxy 對(duì)象。數(shù)據(jù)劫持最著名的應(yīng)用當(dāng)屬雙向綁定,這也是一個(gè)已經(jīng)被討論爛了的面試必考題。例如 Vue 2.x 使用的是 Object.defineProperty()(Vue 在 3.x 版本之后改用 Proxy 進(jìn)行實(shí)現(xiàn))。
Object.defineProperty
ES5 提供了 Object.defineProperty 方法,該方法可以在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性,并返回這個(gè)對(duì)象。
語(yǔ)法:
Object.defineProperty(obj, prop, descriptor)
參數(shù):
obj: 要在其上定義屬性的對(duì)象。
prop: 要定義或修改的屬性的名稱。
descriptor: 將被定義或修改的屬性的描述符。
來(lái)個(gè)例子:
// 這是將要被劫持的對(duì)象const data = {name: '',};function say(name) {if (name === '孫紅雷') {console.log('給大家推薦一款超好玩的游戲');} else if (name === '綠茶婊') {console.log('綠茶婊,撩妹屬性爆表');} else {console.log('來(lái)做我的兄弟');}}// 遍歷對(duì)象,對(duì)其屬性值進(jìn)行劫持Object.keys(data).forEach(function(key) {Object.defineProperty(data, key, {//當(dāng)且僅當(dāng)該屬性的 enumerable 為 true 時(shí),該屬性才能夠出現(xiàn)在對(duì)象的枚舉屬性中。默認(rèn)為 false。enumerable: true,//當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時(shí),該屬性描述符才能夠被改變,也能夠被刪除。默認(rèn)為 false。configurable: true,get: function() {console.log('get');},set: function(newVal) {// 當(dāng)屬性值發(fā)生變化時(shí)我們可以進(jìn)行額外操作console.log(`大家好,我系${newVal}`);say(newVal);},});});data.name = '綠茶婊';//大家好,我系綠茶婊//綠茶婊,撩妹屬性爆表
Proxy
Proxy 用于修改某些操作的默認(rèn)行為,等同于在語(yǔ)言層面做出修改,所以屬于一種“元編程”(meta programming),即對(duì)編程語(yǔ)言進(jìn)行編程。
Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問(wèn),都必須先通過(guò)這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫(xiě)。Proxy 這個(gè)詞的原意是代理,用在這里表示由它來(lái)“代理”某些操作,可以譯為“代理器”。
我們來(lái)看看它的語(yǔ)法:
var proxy = new Proxy(target, handler);proxy 對(duì)象的所有用法,都是上面這種形式,不同的只是handler參數(shù)的寫(xiě)法。其中,new Proxy()表示生成一個(gè)Proxy實(shí)例,target參數(shù)表示所要攔截的目標(biāo)對(duì)象,handler參數(shù)也是一個(gè)對(duì)象,用來(lái)定制攔截行為。
var proxy = new Proxy({}, {get: function(obj, prop) {console.log('設(shè)置 get 操作')return obj[prop];},set: function(obj, prop, value) {console.log('設(shè)置 set 操作')obj[prop] = value;}});proxy.time = 35; // 設(shè)置 set 操作console.log(proxy.time); // 設(shè)置 get 操作 // 35
除了 get 和 set 之外,proxy 可以攔截多達(dá) 13 種操作,比如 has(target, propKey),可以攔截 propKey in proxy 的操作,返回一個(gè)布爾值。
// 使用 has 方法隱藏某些屬性,不被 in 運(yùn)算符發(fā)現(xiàn)var handler = {has (target, key) {if (key[0] === '_') {return false;}return key in target;}};var target = { _prop: 'foo', prop: 'foo' };var proxy = new Proxy(target, handler);console.log('_prop' in proxy); // false
又比如說(shuō) apply 方法攔截函數(shù)的調(diào)用、call 和 apply 操作。
apply 方法可以接受三個(gè)參數(shù),分別是目標(biāo)對(duì)象、目標(biāo)對(duì)象的上下文對(duì)象(this)和目標(biāo)對(duì)象的參數(shù)數(shù)組,不過(guò)這里我們簡(jiǎn)單演示一下:
var target = function () { return 'I am the target'; };var handler = {apply: function () {return 'I am the proxy';}};var p = new Proxy(target, handler);p();// "I am the proxy"
又比如說(shuō) ownKeys 方法可以攔截對(duì)象自身屬性的讀取操作。具體來(lái)說(shuō),攔截以下操作:
Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()
下面的例子是攔截第一個(gè)字符為下劃線的屬性名,不讓它被 for of 遍歷到。
let target = {_bar: 'foo',_prop: 'bar',prop: 'baz'};let handler = {ownKeys (target) {return Reflect.ownKeys(target).filter(key => key[0] !== '_');}};let proxy = new Proxy(target, handler);for (let key of Object.keys(proxy)) {console.log(target[key]);}// "baz"
我們使用 proxy 再來(lái)寫(xiě)一下 watch 函數(shù)。使用效果如下:
(function() {var root = this;function watch(target, func) {var proxy = new Proxy(target, {get: function(target, prop) {return target[prop];},set: function(target, prop, value) {target[prop] = value;func(prop, value);}});if(target[name]) proxy[name] = value;return proxy;}this.watch = watch;})()var obj = {value: 1}var newObj = watch(obj, function(key, newvalue) {if (key == 'value') document.getElementById('container').innerHTML = newvalue;})document.getElementById('button').addEventListener("click", function() {newObj.value += 1});
我們也可以發(fā)現(xiàn),使用 defineProperty 和 proxy 的區(qū)別,當(dāng)使用 defineProperty,我們修改原來(lái)的 obj 對(duì)象就可以觸發(fā)攔截,而使用 proxy,就必須修改代理對(duì)象,即 Proxy 的實(shí)例才可以觸發(fā)攔截。
vue3為什么用proxy?舍棄Object.defineProperty
Object.defineProperty缺點(diǎn):
①不能監(jiān)聽(tīng)數(shù)組的變化
數(shù)組的以下幾個(gè)方法不會(huì)觸發(fā) set:
push、pop、shift、unshift、splice、sort、reverse;
Vue 把這些方法定義為變異方法 (mutation method),指的是會(huì)修改原來(lái)數(shù)組的方法。與之對(duì)應(yīng)則是非變異方法 (non-mutating method),例如 filter, concat, slice 等,它們都不會(huì)修改原始數(shù)組,而會(huì)返回一個(gè)新的數(shù)組。
let arr = [1,2,3]let obj = {}Object.defineProperty(obj, 'arr', {get () {console.log('get arr')return arr},set (newVal) {console.log('set', newVal)arr = newVal}})obj.arr.push(4) // 只會(huì)打印 get arr, 不會(huì)打印 setobj.arr = [1,2,3,4] // 這個(gè)能正常 set
②必須遍歷對(duì)象的每個(gè)屬性
使用 Object.defineProperty() 多數(shù)要配合 Object.keys() 和遍歷,于是多了一層嵌套。
Object.keys(obj).forEach(key => {Object.defineProperty(obj, key, {// ...})})
③必須深層遍歷嵌套的對(duì)象
如果是這一類嵌套對(duì)象,那就必須逐層遍歷,直到把每個(gè)對(duì)象的每個(gè)屬性都調(diào)用 Object.defineProperty() 為止。Vue 的源碼中就能找到這樣的邏輯 (叫做 walk 方法)。
let obj = {info: {name: 'eason'}}
針對(duì)Object.defineProperty有這些缺點(diǎn),為什么用proxy?
1.proxy可以直接監(jiān)聽(tīng)數(shù)組的變化;
2.proxy可以監(jiān)聽(tīng)對(duì)象而非屬性.它在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問(wèn),都必須先通過(guò)這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫(xiě)。
3.Proxy返回的是一個(gè)新對(duì)象,我們可以只操作新的對(duì)象達(dá)到目的,而Object.defineProperty只能遍歷對(duì)象屬性直接修改。
4.Proxy有多達(dá)13種攔截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具備的。
當(dāng)然,Proxy的劣勢(shì)就是兼容性問(wèn)題,而且無(wú)法用polyfill磨平,因此Vue的作者才聲明需要等到下個(gè)大版本(3.0)才能用Proxy重寫(xiě)。
?? 看完三件事
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)贊,讓更多的人也能看到這篇內(nèi)容(收藏不點(diǎn)贊,都是耍流氓)。 關(guān)注公眾號(hào)「入坑互聯(lián)網(wǎng)」,不定期分享原創(chuàng)知識(shí)。 也看看其它文章
- END -
結(jié)伴同行前端路

