Vue 数据响应式原理

前言

Vue.js 的核心包括一套“响应式系统”。

“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码。

例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。

举个简单的例子,对于模板:

{{ name }}

创建一个 Vue 组件:

var vm = new Vue({

  el: '#root',

  data: {

    name: 'luobo'

  }

})

代码执行后,页面上对应位置会显示:luobo。

如果想改变显示的名字,只需要执行:

vm.name = 'tang'

这样页面上就会显示修改后的名字了,并不需要去手动修改 DOM 更新数据。

接下来,我们就一起深入了解 Vue 的数据响应式原理,搞清楚响应式的实现机制。

基本概念

Vue 的响应式,核心机制是 观察者模式

数据是被观察的一方,发生改变时,通知所有的观察者,这样观察者可以做出响应,比如,重新渲染然后更新视图。 

我们把依赖数据的观察者称为 watcher,那么这种关系可以表示为:

data -> watcher

数据可以有多个观察者,怎么记录这种依赖关系呢?

Vue 通过在 data 和 watcher 间创建一个 dep 对象,来记录这种依赖关系

data - dep -> watcher

dep 的结构很简单,除了唯一标识属性 id,另一个属性就是用于记录所有观察者的 subs

 

  • id - number

  • subs - [Watcher]

再来看 watcher。

Vue 中 watcher 的观察对象,确切来说是一个求值表达式,或者函数。这个表达式或者函数,在一个 Vue 实例的上下文中求值或执行。

这个过程中,使用到数据,也就是 watcher 所依赖的数据。

用于记录依赖关系的属性是 deps,对应的是由 dep 对象组成的数组,对应所有依赖的数据。

而表达式或函数,最终会作为求值函数记录到 getter 属性,每次求值得到的结果记录在 value 属性:

  • vm - VueComponent

  • deps - [Dep]

  • getter - function

  • value - *

另外,还有一个重要的属性 cb,记录回调函数,当 getter 返回的值与当前 value 不同时被调用

  • cb - function

我们通过示例来整理下 data、dep、watcher 的关系:

var vm = new Vue({
  data: {
    name: 'luobo',
    age: 18
  }
})

var userInfo = function () {
  return this.name + ' - ' + this.age
}

var onUserInfoChange = function (userInfo) {
  console.log(userInfo)
}
vm.$watch(userInfo, onUserInfoChange)

上面代码首先创建了一个新的 Vue 实例对象 vm,包含两个数据字段:name、age。对于这两个字段,Vue 会分别创建对应的 dep 对象,用于记录依赖该数据的 watcher。 

然后定义了一个求值函数 userInfo,注意,这个函数会在对应的 Vue 示例上下文中执行,也就是说,执行时的 this 对应的就是 vm。

回调函数 onUserInfoChange 只是打印出新的 watcher 得到的新的值,由 userInfo 执行后生成。

通过 vm.$watch(userInfo, onUserInfoChange),将 vm、getter、cb 集成在一起创建了新的 watcher。创建成功后,watcher 在内部已经记录了依赖关系,watcher.deps 中记录了 vm 的 name、age 对应的 dep 对象(因为 userInfo 中使用了这两个数据)。

接下来,我们修改数据:

vm.name = 'tang'

执行后,控制台会输出:

tang - 18

同样,如果修改 age 的值,也会最终触发 onUserInfoChange 打印出新的结果。

用个简单的图来整理下上面的关系:

vm.name -- dep1

vm.age  -- dep2

watcher.deps --> [dep1, dep2]

修改 vm.name 后,dep1 通知相关的 watcher,然后 watcher 执行 getter,得到新的 value,再将新的 value 传给 cb

vm.name -> dep1 -> watcher -> getter -> value -> cb

可能你也注意到了,上面例子中的 userInfo,貌似就是计算属性的作用嘛:

var vm = new Vue({
  data: {
    name: 'luobo',
    age: 18
  },
  computed: {
    userInfo() {
      return this.name + ' - ' + this.age
    }
  }
})

其实,计算属性在内部也是基于 watcher 实现的,每个计算属性对应一个 watcher,其 getter 也就是计算属性的声明函数。

不过,计算属性对应的 watcher 与直接通过 vm.$watch() 创建的 watcher 略有不同,毕竟如果没有地方使用到这个计算属性,数据改变时都重新进行计算会有点浪费,这个在本文后面会讲到。

上面描述了 data、dep、watcher 的关系,但是问题来了,这种依赖关系是如何建立的呢?数据改变后,又是如何通知 watcher 的呢?

接下来我们深入 Vue 源码,搞清楚这两个问题。

建立依赖关系

Vue 源码版本 v2.5.13,文中摘录的部分代码为便于分析进行了简化或改写。

响应式的核心逻辑,都在 Vue 项目的 “vue/src/core/observer” 目录下面。 

我们还是先顺着前面示例代码来捋一遍,首先是 Vue 实例化过程:

var vm = new Vue(/* ... */)

跟将传入的 data 进行响应式初始化相关的代码,在 “vue/src/core/instance/state.js” 文件中:

observer/state.js#L149

// new Vue() -> ... -> initState() -> initData()

observe(data)

函数 observe() 的目的是让传入的整个对象成为响应式的,它会遍历对象的所有属性,然后执行:

observer/index.js#L64

// observe() -> new Observer() -> observer.walk()

defineReactive(obj, key, value)

defineReactive() 就是用于定义响应式数据的核心函数。它主要做的事情包括:

  • 新建一个 dep 对象,与当前数据对应

  • 通过 Object.defineProperty() 重新定义对象属性,配置属性的 set、get,从而数据被获取、设置时可以执行 Vue 的代码

OK,先到这里,关于 Vue 实例化告一段落。

需要注意的是,传入 Vue 的 data 的所有属性,会被代理到新创建的 Vue 实例对象上,这样通过 vm.name 进行操作的其实就是 data.name,这也是借助 Object.defineProperty() 实现的。 

再来看 watcher 的创建过程:

vm.$watch(userInfo, onUserInfoChange)

上述代码执行后,会调用:

instance/state.js#L346

// Vue.prototype.$watch()
new Watcher(vm, expOrFn, cb, options)

也就是:

new Watcher(vm, userInfo, onUserInfoChange, {/**/})

 

posted @ 2019-11-28 16:17  guopengju  阅读(388)  评论(0)    收藏  举报