基礎類型的響應性 —— ref
在vue3里面糯崎,我們可以通過 reactive 來實現(xiàn)引用類型的響應性几缭,那么基礎類型的響應性如何來實現(xiàn)呢?
可能你會想到這樣來實現(xiàn):
const count = reactive({value: 0})
count.value += 1
這么做確實可以實現(xiàn)沃呢,而且也很像 ref 的使用方式年栓,都是要 .value 嘛。那么 ref內部 是不是這么實現(xiàn)的呢薄霜?
我們先定義兩個 ref 的實例并且打印看看某抓。
const refCount = ref(0) // 基礎類型
console.log('refCount ', refCount )
const refObject = ref({ value: 0 }) // 引用類型
console.log('refObject ', refObject )
看一下結果:
我們都知道 reactive 是通過 ES6 的 Proxy 來實現(xiàn)的,基礎類型的 ref 顯然和 Proxy 沒啥關系惰瓜,而引用類型的 ref 是先把原型變成 reactive否副, 然后再掛到 value 上面。
這樣看來崎坊,和我們的猜測不太一樣呢备禀,那么 ref 到底是如何實現(xiàn)的呢?我們可以看一下 ref 的源碼奈揍。
ref 的源碼
代碼來自于 vue.global.js 曲尸,調整了一下先后順序。
function ref(value) {
return createRef(value);
}
function createRef(rawValue, shallow = false) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
class RefImpl {
constructor(_rawValue, _shallow = false) {
this._rawValue = _rawValue;
this._shallow = _shallow;
this.__v_isRef = true;
this._value = _shallow ? _rawValue : convert(_rawValue); // 深層 ref or 淺層ref
}
get value() {
track(toRaw(this), "get" /* GET */, 'value');
return this._value;
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal;
this._value = this._shallow ? newVal : convert(newVal);
trigger(toRaw(this), "set" /* SET */, 'value', newVal);
}
}
}
const convert = (val) => isObject(val) ? reactive(val) : val;
ref
這是我們使用的函數(shù)男翰,里面使用 createRef 來創(chuàng)建一個實例另患。createRef
做一些基礎判斷,然后進入主題蛾绎,正式創(chuàng)建ref昆箕。這里還可以創(chuàng)建 shallowRef。RefImpl
這個才是主體秘通,顯然這是 ES6 的 class为严,constructor 是初始化函數(shù),依據(jù)參數(shù)創(chuàng)建一個實例肺稀,并且設置實例的屬性第股。這個和上面 ref 的打印結果也是可以對應上的。
整個class的代碼也是非常簡單话原,設置幾個“內部”屬性夕吻,記錄需要的數(shù)據(jù),然后設置“外部”屬性 value繁仁,通過setter涉馅、getter 實現(xiàn)對 value 的操作攔截,set 里面主要是 trigger 這個函數(shù)黄虱,由它調用模板的自動刷新的功能稚矿。convert
很顯然,判斷一下參數(shù)是不是 object,如果是的話晤揣,變成 reactive 的形式桥爽。
這個就可以解釋,引用類型的 ref 是如何實現(xiàn)響應性的昧识,明顯是先變成 reactive钠四,然后在掛到 value 上面(掛之前判斷一下是不是淺層的)。
ref 和 reactive 的關系
通過打印結果的對比以及分析源碼可以發(fā)現(xiàn):
- 基礎類型的 ref 和 reactive 沒有任何關系跪楞。
- 引用類型的 ref 缀去,先把 object 變成 reactive ,即利用 reactive 來實現(xiàn)引用類型的響應性甸祭。
關系就是這樣的缕碎,千萬不要再混淆了。
shallowRef
淺層響應式淋叶,只監(jiān)聽 .value 的變化阎曹,真簡單類型的響應式。
function shallowRef(value) {
return createRef(value, true); // true 淺層
}
通過源碼我們可以發(fā)現(xiàn)煞檩,在把引用類型掛到 value 之前处嫌,先判斷一下是不是淺層的,如果是淺層的斟湃,并不會變成 reactive熏迹,而是直接把原來的對象掛在 value 上面,shallowRef 和 ref 的區(qū)別就在于這一點凝赛。
我們寫幾個實例看看效果:
setup () {
// 淺層的測試
// 基礎類型
const srefCount = shallowRef(0)
console.log('refCount ', srefCount )
// 引用類型
const srefObject = shallowRef({ value: 0 })
console.log('refObject ', srefObject )
// 嵌套對象
const srefObjectMore = shallowRef({ info: {a: 'jyk'} })
console.log('shallowRef ', srefObjectMore )
// reactive 的 shallowRef
const ret = reactive({name: 'jyk'})
const shallowRefRet = shallowRef(ret)
console.log('shallowRefRet ', shallowRefRet )
// ==================== 事件 ==================
// 修改基礎類型
const setNumber = () => {
srefCount.value = new Date().valueOf()
console.log('srefCount ', srefCount )
}
// 修改引用類型的屬性
const setObjectProp = () => {
srefObject.value.value = new Date().valueOf()
console.log('srefObject ', srefObject )
}
// 修改引用類型的value
const setObject = () => {
srefObject.value = { value: new Date().valueOf() }
console.log('srefObject ', srefObject )
}
// 修改嵌套引用類型的屬性
const setObjectMoreProp = () => {
srefObjectMore.value.info.a = new Date().valueOf()
console.log('srefObjectMore ', srefObjectMore )
}
// 修改嵌套引用類型的value
const setObjectMore = () => {
srefObjectMore.value = { qiantao: 1234567 }
console.log('srefObjectMore ', srefObjectMore )
}
// 修改reactive 的淺層ref
const setObjectreactive = () => {
shallowRefRet.value.name = '淺層的reactive'
console.log('shallowRefRet ', shallowRefRet )
}
}
看看結果:
測試了一下響應性:
- 基礎類型 srefCount 有響應性注暗;
- 引用類型 srefObject 的屬性沒有響應性,但是直接修改 .value 是有響應性的墓猎。
- 嵌套的引用類型 srefObjectMore 捆昏,屬性和嵌套屬性都是沒有響應性的,但是直接修改 .value 是有響應性的毙沾。
- reactive 套上 shallowRef 骗卜,然后修改 shallowRef.value.屬性 = xxx ,也是可以響應的左胞,所以淺層的ref 也不絕對寇仓,還要看內部結構。
triggerRef
手動執(zhí)行與 shallowRef 關聯(lián)的任何效果烤宙。
官網(wǎng)的中文版里面寫的很繞遍烦,其實就是 讓 shallowRef 原本不具有響應性的部分,具有響應性躺枕。
shallowRef 是淺層的服猪,深層部分是沒有響應性的供填,那么如果非得讓這部分也具有響應性呢?
這時候可以用 triggerRef 來實現(xiàn)蔓姚。
好吧捕虽,目前還沒有想到有啥具體的應用場景,因為一般都直接簡單粗暴的用 ref 或者 reactive 了坡脐,全都自帶響應性。
測試了各種情況房揭,發(fā)現(xiàn) triggerRef 并不支持 shallowReactive备闲,還以為能支持呢。(或許是我寫的測試代碼有問題吧捅暴,官網(wǎng)也沒提 shallowReactive)
基于上面的例子恬砂,在適當?shù)奈恢眉由? triggerRef(xxx)就可以了。
setup () {
// 引用類型
const srefObject = shallowRef({ value: 0 })
// 嵌套對象
const srefObjectMore = shallowRef({ value: {a: 'jyk'} })
// reactive 的 shallowRef
const ret = reactive({name: 'reactive'})
const shallowRefRet = shallowRef(ret)
// 淺層的reactive
const myShallowReactive = shallowReactive({info:{name:'myShallowReactive'}})
const setsRet = () => {
myShallowReactive.info.name = new Date().valueOf()
triggerRef(myShallowReactive) // 修改后使用蓬痒,不支持
}
// ==================== 事件 ==================
// 修改引用類型的屬性
const setObjectProp = () => {
srefObject.value.value = new Date().valueOf()
triggerRef(srefObject) // 修改后使用
}
// 修改引用類型的value
const setObject = () => {
srefObject.value = { value: new Date().valueOf() }
triggerRef(srefObject)
}
// 修改嵌套引用類型的屬性
const setObjectMoreProp = () => {
srefObjectMore.value.value.a = new Date().valueOf()
triggerRef(srefObjectMore)
}
// 修改嵌套引用類型的value
const setObjectMore = () => {
srefObjectMore.value.value = { value: new Date().valueOf() }
triggerRef(srefObjectMore)
}
// 修改reactive 的淺層ref
const setObjectreactive = () => {
shallowRefRet.value.name = '淺層的reactive' + new Date().valueOf()
triggerRef(shallowRefRet)
}
return {
srefObject, // 引用類型
srefObjectMore, // 嵌套引用類型
shallowRefRet, // reactive 的淺層ref
myShallowReactive, // 淺層的reactive
setsRet, // 修改淺層的reactive
setObjectProp, // 修改引用類型的屬性
setObject, // 修改引用類型的value
setObjectMoreProp, // 修改嵌套引用類型的屬性
setObjectMore, // 修改嵌套引用類型的value
setObjectreactive // 試一試reactive的淺層ref
}
}
深層部分泻骤,不使用 triggerRef 就不會刷新模板,使用了 triggerRef 就可以刷新模板梧奢。
話說,為啥會有這個函數(shù)?
isRef
通過 __v_isRef 屬性 判斷是否是 ref 的實例消约。這個沒啥好說的孩锡。
vue.global.js 源碼:
function isRef(r) {
return Boolean(r && r.__v_isRef === true);
}
unref
- 使用.value的語法糖
unref 是一個語法糖,判斷是不是 ref 的惦蚊,如果是則取.value器虾,不是的話取原型。
vue.global.js 源碼:
function unref(ref) {
return isRef(ref) ? ref.value : ref;
}
- unref 的用途
普通對象直接.屬性
即可使用蹦锋,但是 ref 卻需要.value才可以兆沙,混合使用的時候容易暈頭,尤其在函數(shù)內部接收參數(shù)的時候莉掂,無法確定傳入的是 reactive 還是 ref葛圃,如果每次都用 isReactive 或者 isRef 來判斷類型,然后決定是否用.value巫湘,那就太麻煩了装悲。于是有了這個語法糖。
toRef 和 toRefs
toRef 可以用來為源響應式對象上的 property 性創(chuàng)建一個
ref
尚氛。然后可以將 ref 傳遞出去诀诊,從而保持對其源 property 的響應式連接。
toRefs 將響應式對象轉換為普通對象阅嘶,其中結果對象的每個 property 都是指向原始對象相應 property 的ref
属瓣。
話說载迄,官網(wǎng)的解釋為啥總是這么令人費解呢?
我們還是先看看例子
setup () {
/**
* 定義 reactive
* 直接解構屬性抡蛙,看響應性
* 使用toRef解構护昧,看響應性
* 使用toRefs解構,看響應性
* 按鈕只修改reactive
*/
const person = reactive({
name: 'jyk',
age: 18
})
console.log('person ', person )
// 直接獲取屬性
const name = person.name
console.log('name ', name )
const refName = toRef(person, 'name')
console.log('refName ', refName )
const personToRefs = toRefs(person)
console.log('personToRefs ', personToRefs )
const update = () => {
// 修改原型
person.name = new Date()
}
return {
person, // reactive
name, // 獲取屬性
refName, // 使用toRef
personToRefs,
update // 修改屬性
}
}
當我們修改person的屬性值的時候粗截,toRef 和 toRefs 的實例也會自動變化惋耙。而直接獲取的name屬性并不會變化。
toRef 就是想實現(xiàn)直接使用對象的屬性名熊昌,并且仍然享有響應性的目的绽榛。
toRef 就是對reactive 進行解構,然后仍然享有響應性的目的婿屹。
其實灭美,說是變成了ref,但是我們看看打印結果就會發(fā)現(xiàn)昂利,其實并不完全是ref届腐。
看類名和屬性,toRef 和 ref 也是有區(qū)別的蜂奸。
toRef 為啥可以響應
toRef 也是一個語法糖犁苏。
如果使用常規(guī)的方式對 reactive 進行解構的話就會發(fā)現(xiàn)窝撵,雖然解構成功了,但是也失去響應性(僅限于基礎類型的屬性碌奉,嵌套對象除外)。
那么如何實現(xiàn)解構后還具有響應性呢赐劣?這時候就需要使用 toRef 了嫉拐。
看上面那個例子魁兼,使用 refName 的時候,相當于使用 person['name']咐汞,這樣就具有響應性了盖呼。
你可能會問,就這化撕?對就是這么簡單,不信的話植阴,我們來看看源碼:
function toRef(object, key) {
return isRef(object[key])
? object[key]
: new ObjectRefImpl(object, key);
}
class ObjectRefImpl {
constructor(_object, _key) {
this._object = _object;
this._key = _key;
this.__v_isRef = true;
}
get value() {
return this._object[this._key]; // 相當于 person['name']
}
set value(newVal) {
this._object[this._key] = newVal;
}
}
看 get 部分圾浅,是不是 相當于 person['name'] 憾朴?
另外,雖然 toRef 看起來好像是變成了 ref众雷,但是其實只是變成了 ref (RefImpl)的雙胞胎兄弟(ObjectRefImpl),并沒有變成 ref(RefImpl)株搔。
ref 是 RefImpl纯蛾, 而 toRef 是 ObjectRefImpl,這是兩個不同的class 翻诉。
toRef 可以看做是 ref 同系列的 class捌刮,后面還有一個同系列的。
toRefs
了解 toRef 之后芦圾,toRefs 就好理解了俄认,從表面上看,可以把 reactive 的所有屬性都解構出來夜焦,從內部代碼來看岂贩,就是把多個 toRef 放在了數(shù)組(或者對象)里面。
function toRefs(object) {
if ( !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`);
}
const ret = isArray(object) ? new Array(object.length) : {};
for (const key in object) {
ret[key] = toRef(object, key);
}
return ret;
}
customRef
自定義一個ref卸伞,并對其依賴項跟蹤和更新觸發(fā)進行顯式控制锉屈。它需要一個工廠函數(shù),該函數(shù)接收 track 和 trigger 函數(shù)作為參數(shù)部念,并應返回一個帶有 get 和 set 的對象氨菇。
如果上面那段沒看懂的話妓湘,可以跳過。
簡單的說榜贴,就是在 ref 原有的 set、get 的基礎上唬党,加入我們自己寫的代碼鹃共,以達到一定的目的驶拱。
話說,官網(wǎng)寫的例子還真是……
反正一開始我是沒看懂蓝纲,后來又反復看,又把代碼敲出來運行永丝,又查了一下“debounce”的意思箭养。
最后終于明白了,這是一個防抖(延遲響應)的代碼毕泌。
一般用戶在文本框里輸入內容,立即就會響應懈词,但是如果在查詢功能里面的話就會有點小郁悶。
用戶輸入個“1”纺涤,立即就去后端查詢“1”抠忘,
然后用戶又輸入個“2”,立即又去后端查詢“12”崎脉,
然后用戶又輸入個“3”,立即又去后端查詢“123”囚灼。
……
這個響應是很快祭衩,但是有點“折騰”的嫌疑阅签,那么能不能等用戶把“123”都輸入好了,再去后端查詢呢路克?
官網(wǎng)的例子就是實現(xiàn)這樣的功能的,我們把例子完善一下养交,就很明顯了。
const useDebouncedRef = (value, delay = 200) => {
let timeout
return customRef((track, trigger) => {
return {
get() {
track() // vue內部的跟蹤函數(shù)
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // vue內部的自動更新的函數(shù)灰羽。
}, delay) // 延遲時間
}
}
})
}
setup () {
const text = useDebouncedRef('hello', 1000) // 定義一個 v-model
console.log('customRef', text)
const update = () => {
// 修改后延遲刷新
text.value = '1111' + new Date().valueOf()
}
return {
text,
update
}
}
customRef 對象:{{text}} <br><br>
<input v-model="text" type="text">
get
沒有改變鱼辙,直接用原方法。set
使用 setTimeout 實現(xiàn)延遲響應的功能,把 Vue 內部的 trigger() 放在 setTimeout 里面就好摘悴。
這樣就可以了蹂喻,延遲多長的時間可以自定義,這里是一秒孵运。一秒內用戶輸入的內容蔓彩,會一次性更新,而不是每輸入一個字符就更新一次赤嚼。
- v-model="text"
可以作為 v-model 來使用。
customRef 的 源碼
我們再來看看 customRef 內部源碼的實現(xiàn)方式更卒。
function customRef(factory) {
return new CustomRefImpl(factory);
}
class CustomRefImpl {
constructor(factory) {
this.__v_isRef = true;
const { get, set } = factory(() => track(this, "get" /* GET */, 'value'), () => trigger(this, "set" /* SET */, 'value'));
this._get = get;
this._set = set;
}
get value() {
return this._get();
}
set value(newVal) {
this._set(newVal);
}
}
很簡單的是不是蹂空,就是先這樣果录,然后在那樣咐熙,最后就搞定了弱恒。
好吧糖声,就是把 factory參數(shù)解構出來蘸泻,分成set和get,做成內部函數(shù)悦施,然后在內部的set和get里面調用。
自定義 ref 的實例 —— 寫一個自己的計算屬性抡诞。
一提到計算屬性,我們會想到 Vue 提供的 computed肴熏,那么如果讓我們使用自定義ref 來實現(xiàn)計算屬性的功能的話顷窒,要如何實現(xiàn)呢?(注意:只是練習用)
我們可以這樣來實現(xiàn):
const myComputed = (_get, _set) => {
return customRef((track, trigger) => {
return {
get() {
track()
if (typeof _get === 'function') {
return _get()
} else {
console.warn(`沒有設置 get 方法`)
}
},
set(newValue) {
if (typeof _set === 'function') {
_set(newValue)
trigger()
} else {
console.warn(`沒有設置 set 方法`)
}
}
}
})
}
setup () {
const refCount = ref(0)
const myCom = myComputed(() => refCount.value + 1)
// const myCom = myComputed(() => refCount.value, (val) => { refCount.value = val})
const update = () => {
// 修改原型
refCount.value = 3
}
const setRef = () => {
// 直接賦值
refCount.value += 1
}
return {
refCount, // 基礎類型
myCom, // 引用類型
update, // 修改屬性
setRef // 直接設置
}
}
<div>
展示 自定義 的 customRef 實現(xiàn)計算屬性 <br>
ref 對象:{{refCount}} <br><br>
自定義的計算屬性 對象:{{myCom}} <br><br>
<input v-model="myCom" type="text">
<el-button @click="update" type="primary">修改屬性</el-button><br><br>
</div>
myComputed
首先定義一個函數(shù)鸦做,接收兩個參數(shù)谓着,一個get,一個set治筒。customRef
返回一個 customRef 的實例舷蒲,內部設置get 和 set。調用方法
調用的時候句灌,可以只傳入get函數(shù),也可以傳入get胰锌、set兩個函數(shù)。
修改 refCount.value 的時候酬土,v-model 的 myCom 也會發(fā)生變化格带。實用性
那么這種方式有啥使用效果呢?
在做組件的時候叽唱,組件的屬性props是不能直接用在內部組件的 v-model 里面的棺亭,因為props只讀,那么怎么辦呢镶摘?
可以在組件內部設置一個ref,然后對props做監(jiān)聽碌冶,或者用computed來做涝缝。
除了上面的幾種方法外,也可以用這里的方法來實現(xiàn)俊卤,把 refCount 變成 props 的屬性就可以了害幅,然后set里面使用 smit 提交。
computed
寫完了自己的計算屬性后以现,我們還是來看看 Vue 提供的計算屬性。
代碼來自于 vue.global.js 佣赖,調整了一下先后順序记盒。
function computed(getterOrOptions) {
let getter;
let setter;
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => {
console.warn('Write operation failed: computed value is readonly');
}
;
}
else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set);
}
class ComputedRefImpl {
constructor(getter, _setter, isReadonly) {
this._setter = _setter;
this._dirty = true;
this.__v_isRef = true;
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
trigger(toRaw(this), "set" /* SET */, 'value');
}
}
});
this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
}
get value() {
if (this._dirty) {
this._value = this.effect();
this._dirty = false;
}
track(toRaw(this), "get" /* GET */, 'value');
return this._value;
}
set value(newValue) {
this._setter(newValue);
}
}
computed
暴露給我們用的方法,來定義一個計算屬性俩檬。只有一個參數(shù),可以是一個函數(shù)(function)技竟,也可以是一個對象屈藐。內部會做一個判斷,然后做拆分联逻。ComputedRefImpl
是不是有點眼熟?這個是 ref 同款系列擅编,都是 RefImpl 風格的箫踩,而且內部代碼結構也很相似。
這個是computed 的主體類锦担,也是先定義內部屬性慨削,然后設置value的get和set。在get和set里面缚态,調用外部設置的函數(shù)。
源碼:
https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi
在線演示:
https://naturefw.gitee.io/nf-vue-cdn/cdn/project-compositionapi/