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, {/* 略 */})
浙公网安备 33010602011771号