数据响应式的通俗点解释就是我们改变了数据,应用对我们改变的行为做出响应。在Vue中响应式的实现基本原理可以用上面这张官方原理图来解释,下面我们将分为一步步解释这张原理图。
数据对象代理
Vue中数据是以一个data对象的形式存储的,那么我们基本的需求自然就为改变对象中的某个键值,应用对我们的行为做出响应,由此我们需要某种方法在改变键值的时候能插入我们自定义的行为,下面我们将这种行为成为代理。es2015提供了 Object.defineProperty 方法实现代理, Object.definePropery 可以定义对象每个key的 getter 和 setter ,分别能在获取和设定值的时候插入我们需要的行为。
下面以一个最基本的需求开始:改变获取对象中的某个键值,应用打印我们改变获取了什么键值
function def(obj, key, val) {Object.defineProperty(obj, key, {get() {console.log('getting', key)return val},set(newVal) {console.log('setting', key)val = newVal}}
下面需求升级:接受一个对象,代理其中全部的key
function proxy(obj) {Object.keys(obj).forEach(key => {let val = obj[key]Object.defineProperty(obj, key, {get() {console.log('getting', key)return val},set(newVal) {console.log('setting', key, newVal)val = newVal}})})}
需求再升级:我们需要一个新对象,它代理另一个对象的所有属性,但这个代理只是简单引用
function getSimpleProxyObj(obj) {Object.keys(obj).forEach(key => {Object.defineProperty(obj, key, {get() {return obj[key]},set(newVal) {obj[key] = newVal}})})}
需求最终版:我们给出一个对象obj,改变对象中的某个键值,应用打印我们改变获取了什么键值,然后我们需要一个新对象newObj,这个对象能通过点操作符直接访问obj的属性,譬如newObj.name会返回obj.name的值
data = {x: 1,y: 2,z: 3}function proxy(obj) {Object.keys(obj).forEach(key => {let val = obj[key]Object.defineProperty(obj, key, {get() {console.log('getting', key)return val},set(newVal) {console.log('setting', key, newVal)val = newVal}})})}function getSimpleProxyObj(obj) {const newObj = {}Object.keys(obj).forEach(key => {Object.defineProperty(newObj, key, {get() {return obj[key]},set(newVal) {obj[key] = newVal}})})return newObj}proxy(data)const newObj = getSimpleProxyObj(data)
上面的需求最终版就是类似数据代理在Vue中的应用了。Vue接受一个data对象,将它转换成代理对象A(对应着原理图当中的紫色data),然后再将Vue实例简单代理这个代理对象A,这个我们就能通过vm.x直接访问到代理对象中的x属性了。
你可能发现上面我们只是讲了怎么代理,并没有讲 getter 和 setter 到底做了什么。下面我们先补充一个设计模式:观察者模式,然后再解释 getter 和 setter 的具体行为。
观察者模式
一句话概括:A观察B,B能通知A进行更新。这里称A为观察者,B为被观察,两者分别称作Observer,和Subject,它们分别有以下基本方法:
- Observer
- update:当Subject通知时调用Observer此方法
- Subject
- register(observer):注册一个observer
- delete(observer):删除一个observer
- notify():通知所有注册在本Subject上面的observer进行更新
简易实现如下:
class Observer {constructor(subject) {subject && subject.register(this)}update() {console.log('update')}}class Subject {constructor() {this.observers = new Set()}register(observer) {this.observers.add(observer)}unregister(observer) {this.observers.delete(observer)}notify() {[...this.observers].forEach(observer => {observer.update && observer.update()})}}
这个模式被应用在Vue的响应式原理当中,看下面的原理图,Dep对应着模式中的Subject,在Vue的经过代理的data对象中的每个key都一个Dep,也就是每个key都是一个被观察者,而需要观察这个key值变化的统称为watcher,对应着模式中的Observer。Dep对象能通知所有注册到自己上面的watcher进行更新。
依赖收集与消息派发
依赖收集
上面我们知道对象中的每个key对应着一个dep,那么很自然就需要一个过程来让各种watcher注册到key的dep当中,而Vue把这个过程称为依赖收集,也就是第一张图中的Collect as Dependency过程。
Vue把watcher注册到dep的行为放到了对应key的getter上,也就是说,如果这个key被读取了,那么就会把对应的watcher注册到自己的dep上面。
以原理图为例,当组件渲染时,会新建一个渲染的watcher,渲染时需要读取响应式对象的key值(对应原理图的”touch”),此时渲染watcher会注册到key的dep上面。因此,所有渲染过程中引用到的key对应的dep都会收集到渲染watcher。到此,依赖收集的含义更清晰了,就是dep收集依赖于自己的watcher。
消息派发
消息派发比较简单,以原理图为例,响应式对象某个key值发生了变化,对应的dep通知收集到的watcher进行update操作(对应原理图的”notify”),而这个通知的行为自然要放到key的setter上面,watcher的update执行re-render操作,对组件进行重新渲染。
总结
简单总结一下响应式原理的关键点:
- 利用
Object.defineProperty劫持获取和设定key值的过程 - 每个key值对应一个dep,dep负责收集所有依赖自己的watcher,收集的过程在key的getter当中
- 当key值更新时,对应dep会通知所有收集到的watcher进行更新
优点:精准对watcher进行更新
缺点:对于数组和对象新增属性,直接赋值则无法实现响应式,需要用Vue.set或者vm.$set来实现

