Vue中的响应式

Vue 初始化的过程,就是把原始的数据最终映射到 DOM 中,而 Vue 的数据驱动除了数据渲染 DOM 之外,还有一个很重要的体现就是数据的变更会触发 DOM 的变化。

其实前端开发最重要的两个工作,一个是把数据渲染到页面,另一个是处理用户交互。

考虑如下示例:
helloVue

当我们去修改 this.message 的时候,模板对应的插值也会渲染成新的数据,那么这一切是怎么做到的呢?

在分析前,我们先直观的想一下,如果不用 Vue 的话,我们会通过最简单的方法实现这个需求:监听点击事件,修改数据,手动操作 DOM 重新渲染。这个过程和使用 Vue 的最大区别就是多了一步“手动操作 DOM 重新渲染”。这一步看上去并不多,但它背后又潜在的几个要处理的问题:

  • 我需要修改哪块的 DOM?

  • 我的修改效率和性能是不是最优的?

  • 我需要对数据每一次的修改都去操作 DOM 吗?

  • 我需要 case by case 去写修改 DOM 的逻辑吗?

如果我们使用了 Vue,那么上面几个问题 Vue 内部就帮你做了,那么 Vue 是如何在我们对数据修改后自动做这些事情呢,接下来我们将进入一些 Vue 响应式系统的底层的细节。

响应式对象

很多小伙伴之前都了解过 Vue.js 实现响应式的核心是利用了 ES5Object.defineProperty,这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因,我们先来对它有个直观的认识。

Object.defineProperty

Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,先来看一下它的语法:

1
Object.defineProperty(obj, prop, descriptor)

obj 是要在其上定义属性的对象;prop 是要定义或修改的属性的名称;descriptor 是将被定义或修改的属性描述符。

比较核心的是 descriptor,它有很多可选键值,具体的可以去参阅它的文档。这里我们最关心的是 getsetget 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。

一旦对象拥有了 gettersetter,我们可以简单地把这个对象称为响应式对象。

defineReactive

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 gettersetter:
defineReactive

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 gettersetter

Vue 中的响应式,核心就是利用 Object.defineProperty 给数据添加了 gettersetter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新,

依赖收集

响应式对象 getter 相关的逻辑就是做依赖收集。

收集依赖的目的是为了当这些响应式数据发生变化时,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 WatcherDep 就是一个非常经典的观察者设计模式的实现

派发更新

Vue 数据修改派发更新的过程就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcherrun,最后执行它们的回调函数。