1 為什么需要ref
由于`proxy`只能代理`引用類型`數據(如: 對象, 數組, Set, Map...), 需要一種方式代理`普通類型`數據(String, Number, Boolean...)設計ref
主要是為了處理普通類型
數據, 使普通類型
數據也具有響應式
除此之外, 通過reactive
代理的對象可能會出現響應丟失的情況. 使用ref
可以在一定程度上解決響應丟失問題
2 初步實現
1) 包裹對象
既然`proxy`不能代理`普通類型`數據, 我們可以在`普通類型`數據的外層包裹一個對象用proxy
代理包裹的對象(wrapper). 為了統一, 給包裹對象定義value
屬性, 最后返回wrapper
的代理對象
function ref(value) {const wrapper = {value: value,}return reactive(wrapper)
}
測試用例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./reactive.js"></script></head><body><script>function ref(value) {const wrapper = {value: value,}return reactive(wrapper)}// count是一個proxy對象const count = ref(1)effect(() => {// 訪問proxy對象的屬性 觸發 getter 收集依賴console.log(count.value)})setTimeout(() => {count.value = 2}, 1000)</script></body>
</html>
2) 添加標識
按照上面的實現, 我們就無法區分一個代理對象是由`ref`創建, 還是由`reactive`創建, 比如下面的代碼ref(1)
reactive({value: 1})
為了后續能夠對ref
創建的代理對象自動脫ref
處理, 即不用.value
訪問.
考慮給ref
創建的代理對象添加一個標識
示例
function ref(value) {const wrapper = {value: value,}// 給wrapper添加一個不可枚舉, 不可寫的屬性__v_isRefObject.defineProperty(wrapper, '__v_isRef', {value: true,})return reactive(wrapper)
}
在Vue3源碼中, 雖然不是按上述方式實現的, 但是可以這樣去理解
3 響應丟失問題
> 將`reactive`定義的代理對象** 賦值**給其它變量時, 會出現** 響應丟失問題** >賦值主要有如下三種情況:
:::danger
- 如果將
reactive
定義的代理對象的屬性賦值給新的變量, 新變量會失去響應性 - 如果對
reactive
定義的代理對象進行展開操作. 展開后的變量會失去響應性 - 如果對
reactive
定義的代理對象進行解構操作. 解構后的變量會失去響應性
:::
1) 賦值操作
> 示例 ><!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./reactive.js"></script></head><body><script>const obj = reactive({ foo: 1, bar: 2 })// 將reactive創建的代理對象的屬性賦值給一個新的變量foolet foo = obj.foo // foo此時就是一個普通變量, 不具備響應性effect(() => {console.log('foo不具備響應性...', foo)})foo = 2</script></body>
</html>
obj.foo
表達式的返回值是1- 相當于定義了一個普通變量
foo
, 而普通變量是不具備響應性的
2) 展開操作
> 示例 ><!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./reactive.js"></script></head><body><script>const obj = reactive({ foo: 1, bar: 2 })// 展開運算符應用在proxy對象上, 會從語法層面遍歷源對象的所有屬性// ...obj ===> foo:1, bar:2const newObj = {...obj,}// 此時的newObj是一個新的普通對象, 和obj之間不存在引用關系console.log(newObj) // {foo:1, bar:2}effect(() => {console.log('newObj沒有響應性...', newObj.foo)})// 改變newObj的屬性值, 不會觸發副作用函數的重新執行// 此時, 我們就說newObj失去了響應性newObj.foo = 2</script></body>
</html>
:::tips
說明
這里對proxy對象展開會經歷如下過程
- proxy對象屬于異質對象(exotic object)
- 當沒有自定義proxy對象的[[GetOwnProperty]]內部方法時, 會使用源對象的方法, 獲取所有屬性
- 調用GetValue, 獲取屬性值
參考文獻
ECMAScript規范2022-對象初始化
:::
2) 解構操作
> 示例 ><!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./reactive.js"></script></head><body><script>const obj = reactive({ foo: 1, bar: 2 })// 對proxy對象進行解構操作后, foo和bar就是普通變量, 也失去了響應性let { foo, bar } = objeffect(() => {console.log('foo不具備響應性', foo)})// 給變量foo賦值, 不會觸發副作用函數重新執行foo = 2</script></body>
</html>
對proxy
對象解構后, foo
就是一個普通變量, 也失去了跟obj
的引用關系.
因此, 對foo
的修改不會觸發副作用函數重新執行
4 toRef與toRefs
1) 基本使用
為了解決在賦值過程中響應丟失問題, Vue3提供了兩個API- toRef: 解決賦值問題
- toRefs: 解決展開, 解構問題
使用演示
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script></head><body><script>const { reactive, effect, toRef, toRefs } = Vue// obj是reactive創建的響應式數據(proxy代理對象)const obj = reactive({ foo: 1, bar: 2 })effect(() => {console.log('obj.foo具有響應性:', obj.foo)})// 使用toRef定義, 取代基本賦值操作 foo = obj.fooconst foo = toRef(obj, 'foo')effect(() => {console.log('foo.value具有響應性:', foo.value)})</script></body>
</html>
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script></head><body><script>const { reactive, effect, toRef, toRefs } = Vue// obj是reactive創建的響應式數據(proxy代理對象)const obj = reactive({ foo: 1, bar: 2 })// 使用toRefs解構賦值 取代 {foo, bar} = objconst { foo, bar } = toRefs(obj)effect(() => {console.log('bar.value具有響應性', bar.value)})</script></body>
</html>
2) toRef的實現
> 基本實現 >function toRef(obj, key) {const wrapper = {get value() {return obj[key]},set value(val) {obj[key] = val},}Object.defineProperty(wrapper, '__v_isRef', {value: true,})return wrapper
}
在Vue3中, 將wrapper抽象成了ObjectRefImpl
類的實例, 大致的實現如下
class ObjectRefImpl {constructor(_obj, _key) {this._obj = _objthis._key = _keythis.__v_isRef = true}get value() {return this._obj[this._key]}set value(newVal) {this._obj[this._key] = newVal}
}function toRef(obj, key) {return new ObjectRefImpl(obj, key)
}
源碼解讀
:::info
- 源碼中的
toRef
實現了默認值的功能 - 源碼中的
toRef
對要轉換的數據做了判斷, 如果已經是ref
類型就直接返回
:::
class ObjectRefImpl {// 支持默認值constructor(_object, _key, _defaultValue) {this._object = _objectthis._key = _keythis._defaultValue = _defaultValuethis.__v_isRef = true}get value() {const val = this._object[this._key]return val === undefined ? this._defaultValue : val}set value(newVal) {this._object[this._key] = newVal}
}
// 1. 支持默認值
function toRef(object, key, defaultValue) {const val = object[key]// 2. 如果要轉換的對象已經是ref類型, 直接返回// eg: state = reactive({foo: ref(1)}) state.foo已經是ref類型, 直接返回ref(1)return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue)
}
測試用例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./reactive.js"></script></head><body><script>// obj是響應式數據const obj = reactive({ foo: 1, bar: 2 })const foo = toRef(obj, 'foo')effect(() => {console.log('foo.value具備響應性:', foo.value)})</script></body>
</html>
3) toRefs的實現
> 基本實現 >function toRefs(obj) {const ret = {}for (const key in obj) {ret[key] = toRef(obj, key)}return ret
}
原碼解讀
:::info
- 源碼中對
obj
的類型做了判斷- 如果不是
reactive
類型的對象, 提示警告 - 支持代理是數組的情況
- 如果不是
:::
function toRefs(object) {// 如果傳入的對象不具備響應性, 提示警告if (!isProxy(object)) {console.warn(`toRefs() expects a reactive object but received a plain one.`)}// 支持代理是數組的情況// - 對象的情況: toRefs(reactive({foo: 1, bar: 2})) => {foo: ref(1), bar: ref(2)}// - 數組的情況: toRefs(reactive(['foo', 'bar'])) => [ref('foo'), ref('bar')]const ret = isArray(object) ? new Array(object.length) : {}for (const key in object) {ret[key] = toRef(object, key)}return ret
}
測試用例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./reactive.js"></script></head><body><script>// obj是響應式數據const obj = reactive({ foo: 1, bar: 2 })// 解構賦值const { foo, bar } = toRefs(obj)effect(() => {console.log('foo.value具備響應性:', foo.value)})</script></body>
</html>
5 自動脫ref
1) 什么是自動脫ref
> 所謂自動脫ref, 就是不寫`.value` >對于ref
類型數據, 每次在訪問時, 需要加.value
才能觸發響應式.
但是這樣做無疑增加了心智負擔, 尤其是在寫模板時, 不夠優雅
為此, Vue3提供一個API: proxyRefs
對傳入的ref
類型對象進行代理, 返回proxy
對象
個人理解: 有點類似toRefs的逆操作??
2) 基本使用
> 使用演示 ><!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./vue.js"></script></head><body><script>const { ref, proxyRefs, effect } = Vueconst count = ref(0)// 1.模擬setup的返回對象// setup函數返回的對象會經過proxyRefs處理// 這樣在模板中就不用寫.value了const setup = proxyRefs({count,})// 2.模擬頁面渲染effect(() => {console.log('不用通過.value訪問', setup.count)})</script></body>
</html>
3) proxyRefs的實現
> 基本實現 >function proxyRefs(objectWithRefs) {return new Proxy(objectWithRefs, {get(target, key, receiver) {// 使用Reflect讀取target[key]const obj = Reflect.get(target, key, receiver)// 如果obj是ref類型, 返回obj.value; 否則, 直接返回return obj.__v_isRef ? obj.value : obj},set(target, key, newVal, receiver) {const obj = target[key]if (obj.__v_isRef) {obj.value = newValreturn obj}return Reflect.set(target, key, newVal, receiver)},})
}
源碼解讀
:::info
- 源碼對傳入參數加強了判斷
- 如果objectWithRefs已經是
reactive
類型, 就直接使用
- 如果objectWithRefs已經是
- 源碼按功能進一步細化, 可讀性更高
unref
函數可以復用: 如果是ref
返回.value
; 否則直接返回- 將proxy的handler提取成
shallowUnwrapHandlers
函數 - 在set時, 加入了新舊值類型的判斷, 更嚴謹
:::
function unref(ref) {return isRef(ref) ? ref.value : ref
}
const shallowUnwrapHandlers = {get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),set: (target, key, value, receiver) => {const oldValue = target[key]if (isRef(oldValue) && !isRef(value)) {oldValue.value = valuereturn true} else {return Reflect.set(target, key, value, receiver)}},
}
function proxyRefs(objectWithRefs) {return isReactive(objectWithRefs)? objectWithRefs: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
6 改造
按照vue的源碼進行改造function isObject(val) {return typeof val === 'object' && val !== null
}
function toReactive(value) {return isObject(value) ? reactive(value) : value
}
class RefImpl {constructor(value) {this.dep = new Set()this._rawValue = valuethis.__v_isRef = truethis._value = toReactive(value)}get value() {trackEffects(this.dep)return this._value}set value(newValue) {if (this._rawValue != newValue) {this._rawValue = newValuethis._value = toReactive(newValue)triggerEffects(this.dep)}}
}
function ref(val) {return new RefImpl(val)
}