Reactivity
Readme:
谷歌翻译是:
该软件包内联到面向用户的渲染器的Global&Browser ESM版本中(例如,@ vue / runtime-dom),但也可以作为可独立使用的软件包发布。独立版本不应与面向用户的渲染器的预捆绑版本一起使用,因为它们将具有用于响应性连接的不同内部存储。面向用户的渲染器应重新导出此程序包中的所有API。
有关完整公开的API,请参见src / index.ts
。您也可以从回购根目录运行yarn build reactivity --types
,这将在temp / reactivity.api.md
生成API报告。
说人话是:
这个包会内嵌到vue的渲染器中(@vue/runtime-dom)。不过它也可以单独发布且被第三方引用(不依赖vue)。但是呢,你们也别瞎用,如果你们的渲染器是暴露给框架使用者的,它可能已经内置了一套响应机制,这跟咱们的reactivity是完全的两套,不一定兼容的(说的就是你,react-dom)。
关于它的api呢,大家就先看看源码或者看看types吧。
从index进入发现导出的我们需要关注的重点文件是ref 、reactive 、computed 、effect
Refs:
大量出现ts中的extends,info
举例说明:
extends:
T extends U ? X : Y
用大白话可以表示为:
如果T包含的类型 是 U包含的类型的 ‘子集’,那么取结果X,否则取结果Y
infer:
在extends语句中,还支持infer关键字,可以推断一个类型变量,高效的对类型进行模式匹配。但是,这个类型变量只能在true的分支中使用。
export type UnwrapRef = T extends Ref
? UnwrapRefSimple
: UnwrapRefSimple
大白话来说就是infer X 就相当于声明了一个变量,这个变量随后可以使用
这时回头看文件中最长的一段extends,就能大致理解这里是为了create/read一个ref前做了非常多的边界值判断,具体详细每个就不细究了~因为一开始的版本也没那么详细,估计是后面社区需求越提越多给兼容的
此时我们看到createRef函数生成的obj,居然没有任何Proxy相关的操作。在之前的信息中我们知道reactive能构建出响应式数据,但要求传参必须是对象。但ref的入参是对象时,同样也需要reactive做转化。那ref这个函数的目的到底是什么呢?
对于基本数据类型,函数传递或者对象解构时,会丢失原始数据的引用,换言之,我们没法让基本数据类型,或者解构后的变量(如果它的值也是基本数据类型的话),成为响应式的数据。
人话就是:
Proxy对基本类型用不了,只能劫持引用类型
举个栗子:
我们是永远没办法让a
或x
这样的基本数据成为响应式的数据的,Proxy也无法劫持基本数据。
const a = 1;
const { x: 1 } = { x: 1 }
复制代码
所以到这里我们看明白了,reactive时响应式数据在做下层的劫持时,遇到基本类型数据实际上会转换refs~
所以其实refs可以说是服务于reactive的,虽然refs也导出在外层了
这里track、trigger不着急看,先继续把reactive看完
Reactive:
外部引用
工厂函数统一处理数据代理的处理
工具函数,看名字可知是判断,def不知道是啥
验证了上面提到的结论,这里会引入ref使用
上面三段就是该文件的核心代码,不难理解这里的weakmap就是用来存放响应式数据的地方
关键的地方在于他外部引入的工厂函数,baseHandlers,collectionHandlers的具体实现,以及为什么要这样区分?
baseHandles:
看到外部引用发现除了effect文件其他大致都能明白是啥了
然后看到
发现他们只是在get中用来过滤的,builtinsymbols是过滤object原生操作,另一个是vue内部的操作
这种边界值处理还有很多,应该不是逻辑重点
直接找到reactive引入的mutableHandlers
可以发现他是ProxyHandler
那么这里我们就可以发现,触发响应式的关键就在于这五个函数里get,set,deleteProperty,has,ownKeys
get:
由createGetter创建(当然createGetter还能创建其他三种模式的get但是跟响应式逻辑重点无关,我觉得应该是为了提供不同类型的响应式数据才有这四种类型,本文仅讨论正常情况)
看代码:
可以看到,对数组、对象等情况做了单独返回
这里关键点在于,中间做了一个track的调用
Object的情况下还会对下层继续进行响应式代理,也就是说,这个key的值如果是对象,只有他被读取的时候这个底下的值才会进行响应式
以及这里的res为啥不直接获取,还要用Reflect去获取
其实这里涉及到this的原因,如果我们直接target[key]会出问题,
举个网上找的栗子:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
console.log(target) // user对象{_name: "Guest"}
return target[prop];
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
console.log(admin.name); // Guest
复制代码
此时:
1、当我们阅读时admin.name,由于admin对象没有自己的属性,搜索将转到其原型。
2、原型是userProxy
3、name从代理读取属性时,其get将触发并从原始对象中返回该属性,它在上下文中运行其代码this=target。因此,结果this._name来自原始对象target,即:from user。
如果我们用Reflect呢
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver);
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
console.log(admin.name); // Admin
复制代码
Reflect.get中receiver参数,保留了对正确引用this(即admin)的引用,该引用将Reflect.get中正确的对象使用传递给get。
这时除了track没仔细看,其他都大致明白了
看set:
Shallow结合文档我们知道:
而里面这层判断其实是为了方便使用,不然内层是ref的时候还要加个.value去设置
他的意思就是如果旧值是 Ref 数据,但新值不是,那更新旧的值的 value 属性值,返回更新成功
后面一段:
不难明白在数组的情况下,就拿这个key是否大于数组长度做判断
但是当我们调用数组的原始函数时,比如push
const proxy = new Proxy([], {
set(target, key, value, receiver) {
console.log(key, value, target[key])
return Reflect.set(target, key, value, receiver)
}
})
proxy.push(1)
// 0 1 undefined
// length 1 1
复制代码
内部逻辑就是先给下标赋值,然后设置length,相当于触发两次set,这里有个haschanged,第二次length的变化不会触发trigger
但是如果是使用unshift的时候
确会触发n次trigger
const proxy = new Proxy([1,2,1,3,1,1], {
set(target, key, value, receiver) {
console.log(key, value, target[key])
return Reflect.set(target, key, value, receiver)
}
})
proxy.unshift(1)
复制代码
虽然实际逻辑也确实需要这样,但明显如果没用到那么多个下标的值的时候是不会用到这个下标的trigger的,所以这个trigger生产的effect会如何处理? 估计是后面effect中会消费掉,或者根本不产生这个的reactiveEffect
collectionHandlers我感觉是一些不常用的数据类型做的特殊处理
effect:应该是整个响应式的核心了
并且发现每次执行前判断effectStack里有没有当前effect,没有才执行,执行后会往effectStack里推,这里是为了避免两个effect循环触发对方的响应式值的改变
比如:
const foo = reactive({value1: 1, value2: 0})
effect(() => {
foo.value2 = foo.value1
foo.value1++
})
cleanup里面
将这个effect的dep全部清除
之后执行fn()的时候又会重新收集target
这里是因为有可能在effect中每次需要收集的是不同的,比如说:
If(一个响应式值 === true) {
Console.log(另一个响应式值)
}
这里很明显就发现每次执行effect的fn时,他需要收集的tartget是不同的
最后可以看到执行了effect之后有个全局变量
activeEffect = effectStack[effectStack.length – 1]
这里有5个不export的值
把targetMap展平就是
WeakMap<Target, Map<string | symbol, Set>>
Target在前面我们知道是要被监听的原始数据
二维KeyToDepMap的key,就是这个原始对象的属性 key
而Dep就是存放着监听函数effect的集合
Track:
这里我们就可以看出,在track的时候往targetMap插入了值
所以我们就可以发现使用的时候effect是怎么知道内部用了什么响应式数据了,当在回调中触发get之后调用了track
当然了,也能看出如果在effect内使用异步函数,他就不会在track到这异步函数内使用的响应式数据
比如说:
let dummy
const obj = reactive({ prop: 1 })
effect(() => {
setTimeout(() => {
dummy = obj.prop
}, 1000)
})
obj.prop = 2
这里的obj就不能被effect监测到
Trigger:
这里的逻辑就很好猜了,因为之前targetMap已经收集了被使用的响应式数据,此时对应的数据一旦改变,就会根据target改变的key从Map中找到effect并执行,至此使用了响应式数据的地方就会跟着改变啦~但是也有很多细节可以继续学习
Trigger代码比较长就不截完整的图了
不难看出,在trigger的时候也会重新收集依赖,因为可能每次的改变类型不同导致需要执行的effect也不一样(猜测)
effect !== activeEffect 这个是为了避免死循环,比如foo.value++
接下来可以看到
对于不同情况做不同收集,可以看到,此处对数组添加情况收集的是length的effect
翻回去看到
当触发ownKeys时就会track数组的length
也就是说一些对数组递归的操作就会触发响应式