目录
前言
从本文开始,我不会再对源码做逐行解释,而是选择以api作为切入点,自顶向下地剖析其原理。 如果读者对源码的细节有兴趣,或者需要查看代码的逐行解析则可以参考这个项目(待上传)。这是我对源码加过注释的版本,注释几乎覆盖每一行代码,不仅有对源码的解释也有我自己当时的所感所想。
阅读本文前可以先阅读以下内容:
如果你暂时没有耐心阅读这些文章的话,不妨先开始阅读下面的正文,遇到相关知识再针对性地阅读它们也是一种好办法。
reactivity包是阅读源码的一个好开始
在vue3中,一切与响应式相关的代码都被写进了名为reactivity的包里。响应式是vue代替我们完成视图更新的手段,是vue的核心能力。学习这部分知识有助于我们更深刻的理解平时常用的api。不仅如此,由于reactivity完全不依赖于其他几个包,它甚至可以独立于vue存在,因此在阅读过程中可以将注意力全部放在这个包本身。另外,截止到文章发布前,vue3仍处于测试阶段,每天master分支都会有更新,但是reactivity包几乎没有过改动。基于上述三个理由,我认为从reactivity包开始阅读源码是一个好的选择。
Vue响应式基本思路
在使用vue的过程中我们发现无论是number、string、boolean、undefined、null、object还是Symbol,都可以被ref api转化为响应式对象,这是怎么实现的?reactive和ref究竟有什么区别? 副作用是如何被管理的? 本节内容就涵盖了上述问题的答案。
将响应式逻辑分为三个阶段更有助于理解
整个响应式的出场“人物”就两个,一个是响应式对象,通常也称为依赖。 另一个是副作用(effect)。一个响应式对象可以关联多个副作用,一个副作用也可以关联多个响应式对象,两者是多对多的关系。响应式这个庞杂的过程,就是以这两个角色作为主角展开的。
注意:在理解响应式对象和副作用的时候请暂且忽略computed,我稍后会单独提到它。因为它比较特殊——它的实例同时包含了effect和依赖,更新逻辑也与一般的响应式对象不同。
初始化
首先进行的是响应式对象的初始化工作,有两类api可以将一个值改造为响应式对象,分别是reactive(及readonly和shallowReactive)和ref。区别在于,reactive api改造的目标(target),只能是对象,而ref则接受任何类型的值。例如,如果我们尝试执行:
reactive(1)
在开发环境下会提示:value cannot be made reactive: 1
把1替换成对象外的任何一种类型的都是如此。
reactive和ref的这个差别是由于它们实现原理的不同引起的。下面分别解释一下两者的原理——同时这也是初始化过程。
对一个对象调用reactive其实是利用proxy对这个对象设置了get和set。在set中会调用trigger方法找到全部相关的effect,然后依次执行。在get中会调用track方法将effect和自身进行关联。而最后创建出的全部proxy都会被存储在全局的readonlyMap或reactiveMap中。
而对一个值调用ref则不同,vue会创建一个RefImpl类的实例,值会被保存在名为_value的属性内,并设置存值函数(set value)和取值函数(get value),同样是在set中调用trigger方法,以及在get中调用track方法。
我们通常使用ref将基本类型的值改造为响应式对象,而用reactive处理对象。
由此可以知道,reactive和ref其实是实现响应式的两种思路,ref通过将参数包裹成refImpl实例,通过存取器实现响应式。而reactive则是在target外架设一层proxy,然后把proxy返回给用户,用户之后的所有操作都要经由proxy处理,而响应式的能力就在proxy的get和set中被实现。
上述提到的proxy以及RefImpl类的实例都称为响应式对象,因为用户对它们的值的存取操作能够被vue拦截以便触发后续的动作。
依赖收集
我们知道当响应式对象的值发生变化时会执行effect,那么两者之间就必须先建立映射关系。 也就是说,vue必须能够通过响应式对象找到那些访问了(使用了)此对象的effect,并重新执行它们。上一步的状态初始化完成后,就可以和effect建立映射关系了,建立映射关系的过程就叫做依赖收集。在大多数情况下,首次依赖收集是在effect初始化过程中完成的。 既然依赖收集的最终目的是将响应式对象和effect关联起来。那么如何关联呢?vue是通过这样一个结构来实现的:
targetMap是WeakMap类型,KeyToDepMap是map类型,而dep是set类型。