為什么采用Proxy重構(gòu)響應(yīng)系統(tǒng) | Vue3源碼系列
前言
我們先看一下官方對(duì)其的定義
用于定義基本操作的自定義行為
修改的是程序默認(rèn)形為,形同于在編程語(yǔ)言層面上做修改,屬于元編程(meta programming)
元編程(英文:Metaprogramming,又譯超編程,是指某類(lèi)計(jì)算機(jī)程序的編寫(xiě),這類(lèi)計(jì)算機(jī)程序編寫(xiě)或者操縱其它程序(或者自身)作為它們的數(shù)據(jù),或者在運(yùn)行時(shí)完成部分本應(yīng)在編譯時(shí)完成的工作
一段代碼來(lái)理解元編程
#!/bin/bash
#?metaprogram
echo?'#!/bin/bash'?>program
for?((I=1;?I<=1024;?I++))?do
????echo?"echo?$I"?>>program
done
chmod?+x?program
這段程序每執(zhí)行一次能幫我們生成一個(gè)名為program的文件,文件內(nèi)容為1024行echo,如果我們手動(dòng)來(lái)寫(xiě)1024行代碼,效率顯然低效
元編程優(yōu)點(diǎn):與手工編寫(xiě)全部代碼相比,程序員可以獲得更高的工作效率,或者給與程序更大的靈活度去處理新的情形而無(wú)需重新編譯
proxy 譯為代理,可以理解為在操作目標(biāo)對(duì)象前架設(shè)一層代理,將所有本該我們手動(dòng)編寫(xiě)的程序交由代理來(lái)處理
生活中也有許許多多的proxy, 如代購(gòu),中介,因?yàn)樗麄兯械男袨槎疾粫?huì)直接觸達(dá)到目標(biāo)對(duì)象
正文
本篇文章作為 Vue3 源碼系列前置篇章之一,Proxy 的科普文,跟Vue3并沒(méi)有絕對(duì)關(guān)系,但是當(dāng)你靜下心讀完了前置篇章,再去讀后續(xù)的源碼系列,感受定會(huì)截然不同
前置篇章包含
這么痛苦的學(xué)習(xí)源碼到底圖個(gè)啥! 重新認(rèn)識(shí)Typescript | Vue3源碼系列 理解函數(shù)式編程 搞明白Proxy 摸清楚Set、Map、WeakSet、WeakMap
下來(lái)將介紹 Proxy 的基本使用
語(yǔ)法
target 要使用 Proxy 包裝的目標(biāo)對(duì)象(可以是任何類(lèi)型的對(duì)象,包括原生數(shù)組,函數(shù),甚至另一個(gè)代理 handler 一個(gè)通常以函數(shù)作為屬性的對(duì)象,用來(lái)定制攔截行為
const?proxy?=?new?Proxy(target,?handle)
舉個(gè)例子
const?origin?=?{}
const?obj?=?new?Proxy(origin,?{
??get:?function?(target,?propKey,?receiver)?{
??return?'10'
??}
});
obj.a?//?10
obj.b?//?10
origin.a?//?undefined
origin.b?//?undefined
上方代碼我們給一個(gè)空對(duì)象的get架設(shè)了一層代理,所有get操作都會(huì)直接返回我們定制的數(shù)字10,需要注意的是,代理只會(huì)對(duì)proxy對(duì)象生效,如上方的origin就沒(méi)有任何效果
Handler 對(duì)象常用的方法
| 方法 | 描述 |
|---|---|
| handler.has() | in 操作符的捕捉器。 |
| handler.get() | 屬性讀取操作的捕捉器。 |
| handler.set() | 屬性設(shè)置操作的捕捉器。 |
| handler.deleteProperty() | delete 操作符的捕捉器。 |
| handler.ownKeys() | Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 |
| handler.apply() | 函數(shù)調(diào)用操作的捕捉器。 |
| handler.construct() | new 操作符的捕捉器 |
下面挑handler.get重點(diǎn)講一下,其它方法的使用也都大同小異,不同的是參數(shù)的區(qū)別
handler.get
get我們?cè)谏厦胬右呀?jīng)體驗(yàn)過(guò)了,現(xiàn)在詳細(xì)介紹一下,用于代理目標(biāo)對(duì)象的屬性讀取操作
授收三個(gè)參數(shù) get(target, propKey, ?receiver)
target 目標(biāo)對(duì)象 propkey 屬性名 receiver Proxy 實(shí)例本身
舉個(gè)例子
const?person?=?{
??like:?"vuejs"
}
const?obj?=?new?Proxy(person,?{
??get:?function(target,?propKey)?{
????if?(propKey?in?target)?{
??????return?target[propKey];
????}?else?{
??????throw?new?ReferenceError("Prop?name?\""?+?propKey?+?"\"?does?not?exist.");
????}
??}
})
obj.like?//?vuejs
obj.test?//?Uncaught?ReferenceError:?Prop?name?"test"?does?not?exist.
上面的代碼表示在讀取代理目標(biāo)的值時(shí),如果有值則直接返回,沒(méi)有值就拋出一個(gè)自定義的錯(cuò)誤
注意:
如果要訪問(wèn)的目標(biāo)屬性是不可寫(xiě)以及不可配置的,則返回的值必須與該目標(biāo)屬性的值相同 如果要訪問(wèn)的目標(biāo)屬性沒(méi)有配置訪問(wèn)方法,即get方法是undefined的,則返回值必須為undefined
如下面的例子
const?obj?=?{};
Object.defineProperty(obj,?"a",?{?
??configurable:?false,?
??enumerable:?false,?
??value:?10,?
??writable:?false?
})
const?p?=?new?Proxy(obj,?{
??get:?function(target,?prop)?{
????return?20;
??}
})
p.a?//?Uncaught?TypeError:?'get'?on?proxy:?property?'a'?is?a?read-only?and?non-configurable..
可撤消的Proxy
proxy有一個(gè)唯一的靜態(tài)方法,Proxy.revocable(target, handler)
Proxy.revocable()方法可以用來(lái)創(chuàng)建一個(gè)可撤銷(xiāo)的代理對(duì)象
該方法的返回值是一個(gè)對(duì)象,其結(jié)構(gòu)為:{"proxy": proxy, "revoke": revoke}
proxy 表示新生成的代理對(duì)象本身,和用一般方式 new Proxy(target, handler) 創(chuàng)建的代理對(duì)象沒(méi)什么不同,只是它可以被撤銷(xiāo)掉。 revoke 撤銷(xiāo)方法,調(diào)用的時(shí)候不需要加任何參數(shù),就可以撤銷(xiāo)掉和它一起生成的那個(gè)代理對(duì)象。
該方法常用于完全封閉對(duì)目標(biāo)對(duì)象的訪問(wèn), 如下示例
const?target?=?{?name:?'vuejs'}
const?{proxy,?revoke}?=?Proxy.revocable(target,?handler)
proxy.name?//?正常取值輸出?vuejs
revoke()?//?取值完成對(duì)proxy進(jìn)行封閉,撤消代理
proxy.name?//?TypeError:?Revoked
Proxy的應(yīng)用場(chǎng)景
Proxy的應(yīng)用范圍很廣,下方列舉幾個(gè)典型的應(yīng)用場(chǎng)景
校驗(yàn)器
想要一個(gè)number,拿回來(lái)的卻是string,驚不驚喜?意不意外?
下面我們使用Proxy實(shí)現(xiàn)一個(gè)邏輯分離的數(shù)據(jù)格式驗(yàn)證器
嗯,真香!
const?target?=?{
??_id:?'1024',
??name:??'vuejs'
}
const?validators?=?{??
????name(val)?{
????????return?typeof?val?===?'string';
????},
????_id(val)?{
????????return?typeof?val?===?'number'?&&?val?>?1024;
????}
}
const?createValidator?=?(target,?validator)?=>?{
??return?new?Proxy(target,?{
????_validator:?validator,
????set(target,?propkey,?value,?proxy){
??????let?validator?=?this._validator[propkey](value)
??????if(validator){
????????return?Reflect.set(target,?propkey,?value,?proxy)
??????}else?{
????????throw?Error(`Cannot?set?${propkey}?to?${value}.?Invalid?type.`)
??????}
????}
??})
}
const?proxy?=?createValidator(target,?validators)
proxy.name?=?'vue-js.com'?//?vue-js.com
proxy.name?=?10086?//?Uncaught?Error:?Cannot?set?name?to?10086.?Invalid?type.
proxy._id?=?1025?//?1025
proxy._id?=?22??//?Uncaught?Error:?Cannot?set?_id?to?22.?Invalid?type?
私有屬性
在日常編寫(xiě)代碼的過(guò)程中,我們想定義一些私有屬性,通常是在團(tuán)隊(duì)中進(jìn)行約定,大家按照約定在變量名之前添加下劃線(xiàn) _ 或者其它格式來(lái)表明這是一個(gè)私有屬性,但我們不能保證他能真正‘私有化’,
下面使用Proxy輕松實(shí)現(xiàn)私有屬性攔截
const?target?=?{
??_id:?'1024',
??name:??'vuejs'
}
const?proxy?=?new?Proxy(target,?{
??get(target,?propkey,?proxy){
????if(propkey[0]?===?'_'){
??????throw?Error(`${propkey}?is?restricted`)
????}
????return?Reflect.get(target,?propkey,?proxy)
??},
??set(target,?propkey,?value,?proxy){
????if(propkey[0]?===?'_'){
??????throw?Error(`${propkey}?is?restricted`)
????}
????return?Reflect.get(target,?propkey,?value,?proxy)
??}
})
proxy.name?//?vuejs
proxy._id?//?Uncaught?Error:?_id?is?restricted
proxy._id?=?'1025'?//?Uncaught?Error:?_id?is?restricted
Proxy 使用場(chǎng)景還有很多很多,不再一一列舉,如果你需要在某一個(gè)動(dòng)作的生命周期內(nèi)做一些特定的處理,那么Proxy 都是適合的
為什么要用Proxy重構(gòu)
在 Proxy 之前,JavaScript 中就提供過(guò) Object.defineProperty,允許對(duì)對(duì)象的 getter/setter 進(jìn)行攔截
Vue3.0之前的雙向綁定是由 defineProperty 實(shí)現(xiàn), 在3.0重構(gòu)為 Proxy,那么兩者的區(qū)別究竟在哪里呢?
首先我們?cè)賮?lái)回顧一下它的定義
Object.defineProperty() 方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性,并返回此對(duì)象
上面給兩個(gè)詞劃了重點(diǎn),對(duì)象上,屬性,我們可以理解為是針對(duì)對(duì)象上的某一個(gè)屬性做處理的
語(yǔ)法
obj 要定義屬性的對(duì)象 prop 要定義或修改的屬性的名稱(chēng)或 Symbol descriptor 要定義或修改的屬性描述符
Object.defineProperty(obj,?prop,?descriptor)
舉個(gè)例子
const?obj?=?{}
Object.defineProperty(obj,?"a",?{
??value?:?1,
??writable?:?false,?//?是否可寫(xiě)?
??configurable?:?false,?//?是否可配置
??enumerable?:?false?//?是否可枚舉
})
//?上面給了三個(gè)false,?下面的相關(guān)操作就很容易理解了
obj.a?=?2?//?無(wú)效
delete?obj.a?//?無(wú)效
for(key?in?obj){
??console.log(key)?//?無(wú)效?
}
Vue中的defineProperty
Vue3之前的雙向綁定都是通過(guò) defineProperty 的 getter,setter 來(lái)實(shí)現(xiàn)的,我們先來(lái)體驗(yàn)一下 getter,setter
const?obj?=?{};
Object.defineProperty(obj,?'a',?{
??set(val)?{
????console.log(`開(kāi)始設(shè)置新值:?${val}`)
??},
??get()?{?
????console.log(`開(kāi)始讀取屬性`)
????return?1;?
??},
??writable?:?true
})
obj.a?=?2?//?開(kāi)始設(shè)置新值:?2
obj.a?//?開(kāi)始獲取屬性?
看到這里,我相信有些同學(xué)已經(jīng)想到了實(shí)現(xiàn)雙向綁定背后的流程了,其實(shí)很簡(jiǎn)單嘛,只要我們觀察到對(duì)象屬性的變更,再去通知更新視圖就好了
我們摘抄一段 Vue 源碼中的核心實(shí)現(xiàn)驗(yàn)證一下,這一部分一筆代過(guò),不是本文重點(diǎn)
??//?源碼位置:https://github.com/vuejs/vue/blob/ef56410a2c/src/core/observer/index.js#L135
??//?...
??Object.defineProperty(obj,?key,?{
????enumerable:?true,
????configurable:?true,
????get:?function?reactiveGetter?()?{
??????//?...
??????if?(Dep.target)?{
????????//?收集依賴(lài)
????????dep.depend()
??????}
??????return?value
????},
????set:?function?reactiveSetter?(newVal)?{
??????//?...
??????//?通知視圖更新
??????dep.notify()
????}
??})
對(duì)象新增屬性為什么不更新
這個(gè)問(wèn)題用過(guò)Vue的同學(xué)應(yīng)該有超過(guò)95%比例遇到過(guò)
data??()?{
??return??{
????obj:?{
??????a:?1
????}
??}
}
methods:?{
??update?()?{
????this.obj.b?=?2
??}
}
上面的偽代碼,當(dāng)我們執(zhí)行 update 更新 obj 時(shí),我們預(yù)期視圖是要隨之更新的,實(shí)際是并不會(huì)
這個(gè)其實(shí)很好理解,我們先要明白 vue 中 data init 的時(shí)機(jī),data init 是在生命周期 created 之前的操作,會(huì)對(duì) data ?綁定一個(gè)觀察者 Observer,之后 data 中的字段更新都會(huì)通知依賴(lài)收集器Dep觸發(fā)視圖更新
然后我們回到 defineProperty 本身,是對(duì)對(duì)象上的屬性做操作,而非對(duì)象本身
一句話(huà)來(lái)說(shuō)就是,在 Observer data 時(shí),新增屬性并不存在,自然就不會(huì)有 getter, setter,也就解釋了為什么新增視圖不更新,解決有很多種,Vue 提供的全局$set 本質(zhì)也是給新增的屬性手動(dòng) observer
//?源碼位置?https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L201
function?set?(target:?Array?|?Object,?key:?any,?val:?any ):?any?{
??//?....
??if?(!ob)?{
????target[key]?=?val
????return?val
??}
??defineReactive(ob.value,?key,?val)
??ob.dep.notify()
??return?val
}
數(shù)組變異
由于 JavaScript 的限制,Vue 不能檢測(cè)以下數(shù)組的變動(dòng):當(dāng)你利用索引直接設(shè)置一個(gè)數(shù)組項(xiàng)時(shí),例如:vm.items[indexOfItem] = newValue
先來(lái)看一段代碼
var?vm?=?new?Vue({
??data:?{
????items:?['1',?'2',?'3']
??}
})
vm.items[1]?=?'4'?//?視圖并未更新
文檔已經(jīng)做出了解釋?zhuān)⒉皇?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 100, 65);">defineProperty的鍋,而是尤大在設(shè)計(jì)上對(duì)性能的權(quán)衡,下面這段代碼可以驗(yàn)證
function?defineReactive(data,?key,?val)?{
??Object.defineProperty(data,?key,?{
????enumerable:?true,
????configurable:?true,
??????get:?function?defineGet()?{
????????console.log(`get?key:?${key}?val:?${val}`);
????????return?val;
??????},
??????set:?function?defineSet(newVal)?{
????????console.log(`set?key:?${key}?val:?${newVal}`);
????????val?=?newVal;
??????}
??})
}
function?observe(data)?{
??Object.keys(data).forEach(function(key)?{
????defineReactive(data,?key,?data[key]);
??})
}
let?test?=?[1,?2,?3];
observe(test);
test[0]?=?4?//?set?key:?0?val:?4
雖然說(shuō)索引變更不是 defineProperty 的鍋,但新增索引的確是 defineProperty 做不到的,所以就有了數(shù)組的變異方法
能看到這里,大概也能猜到內(nèi)部實(shí)現(xiàn)了,還是跟$set一樣,手動(dòng) observer,下面我們驗(yàn)證一下
const?methodsToPatch?=?[
??'push',
??'pop',
??'shift',
??'unshift',
??'splice',
??'sort',
??'reverse'
]
methodsToPatch.forEach(function?(method)?{
??//?緩存原生數(shù)組
??const?original?=?arrayProto[method]
??//?def使用Object.defineProperty重新定義屬性
??def(arrayMethods,?method,?function?mutator?(...args)?{
????const?result?=?original.apply(this,?args)?//?調(diào)用原生數(shù)組的方法
????const?ob?=?this.__ob__??//?ob就是observe實(shí)例observe才能響應(yīng)式
????let?inserted
????switch?(method)?{
??????// push和unshift方法會(huì)增加數(shù)組的索引,但是新增的索引位需要手動(dòng)observe的
??????case?'push':
??????case?'unshift':
????????inserted?=?args
????????break
??????//?同理,splice的第三個(gè)參數(shù),為新增的值,也需要手動(dòng)observe
??????case?'splice':
????????inserted?=?args.slice(2)
????????break
????}
????//?其余的方法都是在原有的索引上更新,初始化的時(shí)候已經(jīng)observe過(guò)了
????if?(inserted)?ob.observeArray(inserted)
????//?dep通知所有的訂閱者觸發(fā)回調(diào)
????ob.dep.notify()
????return?result
??})
})
對(duì)比
一個(gè)優(yōu)秀的開(kāi)源框架本身就是一個(gè)不斷打碎重朔的過(guò)程,上面做了些許鋪墊,現(xiàn)在我們簡(jiǎn)要總結(jié)一下
Proxy作為新標(biāo)準(zhǔn)將受到瀏覽器廠商重點(diǎn)持續(xù)的性能優(yōu)化Proxy能觀察的類(lèi)型比defineProperty更豐富Proxy不兼容IE,也沒(méi)有polyfill,defineProperty能支持到IE9Object.definedProperty是劫持對(duì)象的屬性,新增元素需要再次definedProperty。而Proxy劫持的是整個(gè)對(duì)象,不需要做特殊處理使用
defineProperty時(shí),我們修改原來(lái)的obj對(duì)象就可以觸發(fā)攔截,而使用proxy,就必須修改代理對(duì)象,即Proxy的實(shí)例才可以觸發(fā)攔截
參考文獻(xiàn)
https://zh.wikipedia.org/wiki/ https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy https://es6.ruanyifeng.com/#docs/proxy#Proxy-revocable https://youngzhang08.github.io/ 推薦閱讀
我的公眾號(hào)能帶來(lái)什么價(jià)值?(文末有送書(shū)規(guī)則,一定要看) 每個(gè)前端工程師都應(yīng)該了解的圖片知識(shí)(長(zhǎng)文建議收藏) 為什么現(xiàn)在面試總是面試造火箭?
