vue3 响应式Reactivity源码阅读

329 阅读8分钟

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前做了非常多的边界值判断,具体详细每个就不细究了~因为一开始的版本也没那么详细,估计是后面社区需求越提越多给兼容的

image.png

此时我们看到createRef函数生成的obj,居然没有任何Proxy相关的操作。在之前的信息中我们知道reactive能构建出响应式数据,但要求传参必须是对象。但ref的入参是对象时,同样也需要reactive做转化。那ref这个函数的目的到底是什么呢?

image.png

image.png

对于基本数据类型,函数传递或者对象解构时,会丢失原始数据的引用,换言之,我们没法让基本数据类型,或者解构后的变量(如果它的值也是基本数据类型的话),成为响应式的数据。

人话就是: Proxy对基本类型用不了,只能劫持引用类型

举个栗子: 我们是永远没办法让ax这样的基本数据成为响应式的数据的,Proxy也无法劫持基本数据。

const a = 1;
const { x: 1 } = { x: 1 }

所以到这里我们看明白了,reactive时响应式数据在做下层的劫持时,遇到基本类型数据实际上会转换refs~ 所以其实refs可以说是服务于reactive的,虽然refs也导出在外层了

这里track、trigger不着急看,先继续把reactive看完

Reactive: 外部引用

image.png

工厂函数统一处理数据代理的处理

image.png

工具函数,看名字可知是判断,def不知道是啥

image.png

验证了上面提到的结论,这里会引入ref使用

image.png

image.png

image.png

上面三段就是该文件的核心代码,不难理解这里的weakmap就是用来存放响应式数据的地方

关键的地方在于他外部引入的工厂函数,baseHandlers,collectionHandlers的具体实现,以及为什么要这样区分?

baseHandles: 看到外部引用发现除了effect文件其他大致都能明白是啥了

然后看到

image.png

image.png

发现他们只是在get中用来过滤的,builtinsymbols是过滤object原生操作,另一个是vue内部的操作

image.png

这种边界值处理还有很多,应该不是逻辑重点

直接找到reactive引入的mutableHandlers

image.png

可以发现他是ProxyHandler

那么这里我们就可以发现,触发响应式的关键就在于这五个函数里get,set,deleteProperty,has,ownKeys

get: 由createGetter创建(当然createGetter还能创建其他三种模式的get但是跟响应式逻辑重点无关,我觉得应该是为了提供不同类型的响应式数据才有这四种类型,本文仅讨论正常情况)

看代码:

image.png

可以看到,对数组、对象等情况做了单独返回 这里关键点在于,中间做了一个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:

image.png

image.png

Shallow结合文档我们知道:

image.png

而里面这层判断其实是为了方便使用,不然内层是ref的时候还要加个.value去设置 他的意思就是如果旧值是 Ref 数据,但新值不是,那更新旧的值的 value 属性值,返回更新成功

后面一段:

image.png

不难明白在数组的情况下,就拿这个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)

image.png

虽然实际逻辑也确实需要这样,但明显如果没用到那么多个下标的值的时候是不会用到这个下标的trigger的,所以这个trigger生产的effect会如何处理? 估计是后面effect中会消费掉,或者根本不产生这个的reactiveEffect

collectionHandlers我感觉是一些不常用的数据类型做的特殊处理

effect:应该是整个响应式的核心了

image.png

image.png

image.png

并且发现每次执行前判断effectStack里有没有当前effect,没有才执行,执行后会往effectStack里推,这里是为了避免两个effect循环触发对方的响应式值的改变

比如: const foo = reactive({value1: 1, value2: 0}) effect(() => { foo.value2 = foo.value1 foo.value1++ })

cleanup里面

image.png

将这个effect的dep全部清除

之后执行fn()的时候又会重新收集target

这里是因为有可能在effect中每次需要收集的是不同的,比如说:

If(一个响应式值 === true) { Console.log(另一个响应式值) }

这里很明显就发现每次执行effect的fn时,他需要收集的tartget是不同的

最后可以看到执行了effect之后有个全局变量 activeEffect = effectStack[effectStack.length - 1]

image.png

这里有5个不export的值 把targetMap展平就是 WeakMap<Target, Map<string | symbol, Set>> Target在前面我们知道是要被监听的原始数据 二维KeyToDepMap的key,就是这个原始对象的属性 key 而Dep就是存放着监听函数effect的集合

Track:

image.png

这里我们就可以看出,在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代码比较长就不截完整的图了

image.png

不难看出,在trigger的时候也会重新收集依赖,因为可能每次的改变类型不同导致需要执行的effect也不一样(猜测)

effect !== activeEffect 这个是为了避免死循环,比如foo.value++

接下来可以看到

image.png

对于不同情况做不同收集,可以看到,此处对数组添加情况收集的是length的effect 翻回去看到

image.png

当触发ownKeys时就会track数组的length

也就是说一些对数组递归的操作就会触发响应式