5分钟了解 Vue 响应式基本原理 🔥

262 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

背景

Vue 我们在日常生活中已经使用非常多了,和传统开发不同的是,Vue 是数据驱动视图的。只要我们修改了 data中的数据,相应的视图就会自动更新,而不用我们手动更新了,这也就是我们常说的响应式。下面我们就用几分钟的时间来看看 Vue2 中实现响应式的基本原理。

如何实现响应式更新

Vue 中实现响应式更新(追踪数据变化,再进行对应视图的更新)主要分为2步:

  • 将普通对象变为可追踪的数据
  • 组件实例记录使用到的数据,数据变更时再通知组件进行更新

一、将普通对象变为可追踪的数据

在Vue中比较常见的响应式数据有2种:

  • data 中初始化的数据
  • 通过 vue.set() 动态添加的数据。

这2种方式在内部实现时都是调用的 Vue.observable()函数。这个函数通过遍历数据对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter让一个普通对象变为可追踪的数据。

二、组件实例绑定数据

在 Vue 中,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把 "接触" 过(getter)的数据 property 记录为依赖(依赖收集)。之后当依赖项数据改变时,数据对应的 setter 触发(数据变更),会通知 watcher,从而使它关联的组件重新渲染(派发更新)。

大概流程如下图:

image

可以看到,先通过将数据 getter/setter化,让数据的使用和赋值都变得可追踪;然后再通过 Vue 实例对数据的使用, 依赖收集-数据变更-派发更新 的过程来实现数据变更时,更新对应视图的效果。

这里我们只介绍了大方向上的原理,所以细节并没有涉及到。如果大家想了解更多关于Vue开发相关的问题,可以看看这篇文章

检测变化的注意事项

对象

Vue 无法检测到对象属性的添加或删除。因为 Vue 会在实例初始化时对 data 对象属性执行 getter/setter 转化,所以初始化时 data 对象中存在数据才为响应式数据。那如果我们想要给对象添加新的属性并更新视图,就需要使用 Vue.set

  • 直接通过赋值添加新属性不生效(数据变更时不会更新视图) ❌
this.someObject.b = 2
  • 向嵌套对象添加响应式 property
Vue.set(vm.someObject, 'b', 2)

this.$set(this.someObject,'b',2)
  • 为已有对象赋值多个新 property ✅
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

数组

  • 不能检测以下数组的变动
  1. 利用索引直接设置一个数组项时。例如:

    直接赋值,非响应式的 ❌

    vm.items[index] = newValue
    

    响应式赋值 ✅

    // 1.Vue.set
    Vue.set(vm.items, index, newValue)
    
    // 2.Array.prototype.splice
    vm.items.splice(index, 1, newValue)
    
  2. 修改数组的长度时,例如:

    // 非响应式的 ❌
    vm.items.length = newLength
    
    // 响应式的 ✅
    vm.items.splice(newLength)
    

不允许动态添加根级响应式数据

必须在初始化实例前声明所有根级响应式数据,哪怕只是一个空值。

在部分比较特殊的场景中,通过 vue.set()设置数据,可能也会出现失效的情况。vue.set 失败场景

异步更新队列

Vue 在更新 DOM 时是 异步执行 的。

  1. 只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。
  2. 然后,在下一个的事件循环 tick 中, Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替

例如,当你设置 this.someData = 'new value',该组件不会立即重新渲染。如果想要基于更新后的 DOM 状态做些什么,可以使用 Vue.nextTick(callback)

这也是为什么如果要获取更新后的 DOM,通常在 Vue.nextTick() 回调中处理。

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})

总结

通过这篇文章我们了解到了 Vue2 实现数据响应式的基本原理,以及实现数据响应式的常见问题。因为只是介绍了大方向上的原理,所以细节并没有涉及到。如果大家想了解更多关于Vue开发相关的问题,可以看看这篇文章