响应式对象是一个什么对象
笨鸟问答录
2020年01月07日 07:14
收录于文集
共7篇

当我们利用vue实现了把数据映射成DOM时,也就是数据驱动,接下来就自然会想着,如果想要通过修改数据,让vue自动根据新数据更新DOM,也即重新渲染。

想要实现这样一个功能。我们就要关注几个问题:

  1. DOM 更新如何实现?

通过重新执行 render 函数来实现

  1. 数据改变,自动执行render 函数,那么问题时,要执行哪些 render 函数?

每个 render 函数都是一个组件,一个数据可能被多个 render 函数使用。因此需要知道该数据被哪些组件使用。

  1. 数据改变,也知道该数据被哪些 render 函数使用,那么 vue 要如何兼听监听数据的变化呢?

于是我们发现:render 函数与数据项是多对多的关系,一个 render 函数使用了多个数据项,而一个数据项往往被多个 render 函数使用。

vue 的响应式对象作用就是用来收集对象中的数据被哪些 render 函数使用,以及监听数据的改变。那么这个响应式对象是如何实现的呢?

响应式原理

响应式对象

vue 中的响应式对象就是利用了对象的访问器属性:setter,getter。如果一个对象(o)的属性(key) 定义了 setter 和 getter。当访问key属性(o.key)时,就会调用定义的 getter 函数,同理,设置key属性(o.key = 'a&#​39;)则会调用定义的 setter 函数。

在 getter 实现依赖收集,在 setter 中实现派发更新便可称为响应式对象。

依赖收集

如此,当vue组件树在渲染时,调用了响应式对象里的属性时,通过 getter 收集这个调用者 render,这便是依赖收集。

派发更新

当响应式对象里的属性发生改变时,通过 setter 就监听到了这个数据的变化,然后执行之前收集的 render 列表。

这便是 vue 的响应式原理。

vue 源码实现

当我搞清楚响应式原理就是通过 getter 和 setter 分别完成依赖收集和派发更新时,就不得不思考一个问题:收集的依赖存储在哪儿?

因为这个存储位置需要解决以下几个问题:

  1. getter 和 setter 需要能访问到

  2. 响应式对象的每一个 key 都要有对应的依赖列表

源码利用闭包轻松解决来这个问题,每个key都通过 defineReactive 来构建响应式,依赖列表就存储在函数的执行上下文中,在函数内部设置的 setter 和 getter 函数中来操作依赖列表。

下图将展示一个依赖收集过程:

源码实现部分看起来有点复杂,原因在于考虑到了依赖列表的去重问题,因此,在 watcher 实例中维护了一个 deps 列表(就响应式过程来讲,并不需要)。另外,Dep 构造函数的静态属性 target 用来保存当前 watcher 实例的引用,因为从图中也可以看到,watcher 与 defineReactive 是没办法直接进行数据交互,只能通过实例化 watcher 时,保存到 Dep.target,然后在执行 getter 时,拿到 watcher 实例通过接口进行数据交换。

最后,派发更新时,直接通过 setter 依次执行 dep.subs 中的 watcher.update 完成组件更新,组件更新的过程中,依然会触发依赖收集的流程,但此时,通过 depIds 维护的状态就可避免充分添加,造成一次数据修改,多次组件更新。