Vue 响应式原理

436 阅读3分钟

从几个问题切入

  1. Vue 响应式的概念
  2. Vue 如何实现响应式系统
  3. 如何做依赖收集和通知更新的
  4. 对数组和对象如何实现响应式

概念

响应式是指,当数据模型被修改时,相关的视图会进行更新,无需开发者手动更新视图

如何实现

Vue 内部在初始化阶段会对 data 选项进行递归遍历,使用 Object.defineProperty 将每个 property 转为 getter/setter,当运行阶段时,property 被访问就会触发 getter 进行依赖收集,当 property 被修改就会触发 setter 通知更新。

如何收集依赖和通知更新

Vue 遍历到每个property,都会调用 defineReactive,其作用是定义 getter/setter。首先会创建一个 Dep 实例对象,与当前的 property 是一一对应的,代表一个依赖。

每个组件实例都对应一个 Watcher 实例,当某个组件正在挂载或更新时,就会访问到任意个property,触发到对应的getter并调用 dep.depend(),当前的 watcher 会依次将每个 dep 添加到自身的依赖队列 newDeps 中(这里有个 newDeps,意味着还有一个 deps,其作用下面会讲解)同时,每个dep 也会将 watcher 添加到自身的订阅者队列 subs 中。完成更新后,会进行依赖清除,并且把 newDeps 给到 deps。

当某个property被修改时,会调用其对应的 dep.notify 通知到 subs 中所有 watcher 进行更新。

到这里还没结束,当组件更新时,又会触发一轮依赖收集,更新完毕之后,又会调用依赖清除,作用是把没用的依赖清掉,什么是没用的依赖呢?比如一开始有显示的数据,后面由于 v-if 等原因不显示了,这个时候即使数据修改了,也不应该去更新该组件。那具体是怎么实现的呢?

watcher 实例中有 newDepsdeps 两个队列。分别存储最新收集的依赖和上一次收集的依赖,更新完之后会找到 deps 中有且 newDeps 中没有的依赖,前面说到依赖 dep 存储了相关的 watcher 用于通知更新,此时要将这个 watcher 移除掉,等到 dep 被修改时,就不会通知到这个 watcher 更新了。清理完之后,将 newDeps 赋值给 deps,并清空 newDeps 等待下一次依赖收集。

举个例子来说明为什么要清除依赖:

假设当前组件对应的 watcher实例为 w,一开始挂载时,baz 有被访问到,因此baz这个依赖会被收集起来,并且当baz被修改时会通知 w 更新;当 show 修改为false之后,又触发一轮更新和依赖收集,此时 baz 不会被访问到,因此 baz 这个 dep 收集的 w 会被移除,当 baz 被修改时,不会通知 w 进行更新。

<template>
  <div>
    <div>{{foo}}</div>
    <div v-if="show">{{baz}}</div>
  </div>
</template>
<script>
export default {
  data () {
    return {
      foo: 1,
      baz: 'a',
      show: true
    }
  },
  mounted () {
    this.show = false
  }
}
</script>

对数组和对象如何实现响应式

对于数组

Vue 无法检测到通过下标索引直接设置数组项,和修改length属性,这是由于对数组进行响应式拦截带来的性能问题与收益无法成正比

解决办法是 vue 对数组的变更方法进行包裹,使之可以触发视图更新,其内部是去调用了整个数组这个 property 对应的 dep 的 notify 方法进行通知更新。

对于对象

Vue 无法检测 property 的添加或移除。因此要使用 Vue.set(object, propertyName, value) 方法手动定义一个响应式 property。