概要
vue3的 reactivity 是一个独立的包,这是一个比较大的改动,所有响应式相关的实现都在里面,我主要讲的也就是这一块的。
知识准备
1.proxy: es6的代理实现方式
2.reflect: 将object对象一些明显属于语言内部方法,放到Reflect上,
3.weakMap: WeakMap 的 key 只能是 Object 类型。
4.weakSet: WeakSet 对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次.
响应式简要实现
我们曾经的书写响应式数据是这样的
data () {
return {
count: 0
}
}
然后vue3新的响应式书写方式(老的也兼容)
setup() {
const state = {
count: 0,
double: computed(() => state.count * 2)
}
function increment() {
state.count++
}
onMounted(() => {
console.log(state.count)
})
watch(() => {
document.title = `count ${state.count}`
})
return {
state,
increment
}
}
感觉setup这块就有点像 react hooks 理解成一个带有数据的逻辑复用模块,不再以vue组件为单位的代码复用了
和React钩子不同,setup()函数仅被调用一次。
所以新的响应书数据两种声明方式:
1.Ref
前提:声明一个类型 Ref
export interface Ref<T> {
[refSymbol]: true
value: UnwrapNestedRefs<T>
}
ref()函数源码:
function ref(raw: unknown) {
if (isRef(raw)) {
return raw
}
// convert 内容:判断 raw是不是对象,是的话 调用reactive把raw响应化
raw = convert(raw)
const r = {
_isRef: true,
get value() {
// track 理解为依赖收集
track(r, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
// trigger 理解为触发监听,就是触发页面更新好了
trigger(r, OperationTypes.SET, '')
}
}
return r as Ref
}
还是看下 convert 吧
const convert = val => isObject(val) ? reactive(val) : val
可以看得出 ref类型 只会包装最外面一层,内部的对象最终还是调用reactive,生成Proxy对象进行响应式代理。
疑问
可能有人想问,为什么不都用proxy, 内部对象都用proxy,最外层还要搞个 Ref类型,多此一举吗?
理由可能比较简单,那就是proxy代理的都是对象,对于基本数据类型,函数传递或对象结构是,会丢失原始数据的引用。
官方解释:
However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:
2.Reactive
前提:先了解下 weakMap
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // key:原始对象 value: Proxy
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
reactive(target)
源码如下:
注:target一定是一个对象,不然会报警告
function reactive(target) {
// 如果target是一个只读响应式数据
if (readonlyToRaw.has(target)) {
return target
}
// 如果是被用户标记的只读数据,那通过readonly函数去封装
if (readonlyValues.has(target)) {
return readonly(target)
}
// go ----> step2
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers, // 注意传递
mutableCollectionHandlers
)
}
createReactiveObject(target,toProxy,toRaw,baseHandlers,collectionHandlers)
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 判断target不是对象就 警告 并退出
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 通过原始数据 -> 响应数据的映射,获取响应数据
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 如果原始数据本身就是个响应数据了,直接返回自身
if (toRaw.has(target)) {
return target
}
// 如果是不可观察的对象,则直接返回原对象
if (!canObserve(target)) {
return target
}
// 集合数据与(对象/数组) 两种数据的代理处理方式不同
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 声明一个代理对象 ----> step3
observed = new Proxy(target, handlers)
// 两个weakMap 存target observed
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
baseHandles
(我们以对象类型为例,集合类型的handlers稍复杂点)
handlers如下,new Proxy(target, handles)的 handles就是下面这个对象
export const mutableHandlers = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
createGetter(false)
问题:如何代理多层嵌套的对象
关键词:利用 proxy 的 get
思路:当我们代理get获取到res时,判断res 是否是对象,如果是那么 继续reactive(res),可以说是一个递归
reactive(target) ->
createReactiveObject(target,handlers) ->
new Proxy(target, handlers) ->
createGetter(readonly) ->
get() -> res ->
isObject(res) ? reactive(res) : res
function createGetter(isReadonly: boolean) {
// isReadonly 用来区分是否是只读响应式数据
// receiver即是被创建出来的代理对象
return function get(target: object, key: string | symbol, receiver: object) {
// 获取原始数据的响应值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
// 收集依赖
track(target, OperationTypes.GET, key)
// 这里判断上面获取的res 是否是对象,如果是对象 则调用reactive并且传递的是获取到的res,
// 则形成了递归
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
set
set的一个主要作用去触发监听,使试图更新,需要注意的是控制什么时候才是视图需要真的更新
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 拿到新值的原始数据
value = toRaw(value)
// 获取旧值
const oldValue = (target as any)[key]
// 如果旧值是Ref类型,新值不是,那么直接更新值,并返回
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果是原始数据原型链上的数据操作,不做任何触发监听函数的行为。
if (target === toRaw(receiver)) {
// 更新的两种条件
// 1. 不存在key,即当前操作是在新增属性
// 2. 旧值和新值不等
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
return result
}
问题2:
对于数据的set操作会出发多次traps,
这里有个前提了解:就是我们日常修改数组,比如 let a = [1], a.push(2),
这个push操作,我们是实际上是对a做了2个属性的修改,1,set length 1; 2. set value 2
所以我们的set traps会出发多次
思路:通过属性值和value控制,比如当 set key是 length的时候,我们可以判断当前数组 已经有此属性,所以不需要出发更新,当新设置的值和老值一样是也不需要更新(说辞不够严谨)
问题3:
set的源码里面有 有一个 target === toRaw(receiver)条件下才继续操作 trigger更新视图
这里就暴露出一个东西,即存在 target !== toRaw(receiver)
Receiver: 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)
其实源码有注释
// don’t trigger if target is something up in the prototype chain of original
即如果我们的操作是操作原始数据原型链上的数据操作,target 就不等于 toRaw(receiver)
什么情况下 target !== toRaw(receiver)
例如:
const child = new Proxy(
{},
{ // 其他 traps 省略
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('child', receiver)
return true
}
}
)
const parent = new Proxy(
{ a: 10 },
{ // 其他 traps 省略
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('parent', receiver)
return true
}
}
)
Object.setPrototypeOf(child, parent) // child.__proto__ === parent true
child.a = 4
// 结果
// parent Proxy {a: 4}
// Proxy {a: 4}
从结果可以看出,理论上 parent的set应该不会触发,但实际是触发了,此时
target: {a: 10}
receiver: Proxy {a: 4}
// 在vue3中
toRaw(receiver): {a: 4}
为什么有了proxy做响应式还需要一个Ref呢?
因为Proxy无法劫持基础数据类型,所以设计了这么一个对象——Ref,其实还是有很多设计细节,就不一一赘述了,官网也给了他们不同点,可以自己去好好了解。