Vue 响应式系统的原理
大多数情况下,一个Vue组件的UI样式由它自身所维护的数据状态决定(例如,按钮上的文字等等), 当我们动态地改变组件的数据,对应的组件UI样式也随之改变。 这就是Vue组件的响应式更新,个特性使得组件的状态管理和视图变化变得更加简单。
响应式系统是贯穿Vue框架的基础,声明式渲染、输入绑定、计算属性和侦听器等各种框架特性和语法糖都是以此为基础的。
我们在深入Vue源码去了解框架细节之前,不妨思考一下如果是我们自己应该如何实现这套机制,站在更宏观的角度上理解一下响应式系统的是如何设计和运转。
Overview
我们在一个Vue组件的定义中声明了组件本身的数据和状态。 响应式系统需要在组件创建和初始化的时候,首先要收集到这个组件所有的数据状态信息,然后监听组件状态的变化, 最后根据数据状态的改变作出更新响应,渲染页面展示,保证整个过程中,数据从Model层流向View层。
从功能需求和业务场景来看,这是明显是一个观察者模式的应用场景。
模式结构很简单,在进一步完善和扩展过程中,我们还需要思考和回答以下几个问题:
- 哪些数据和状态需要被监听和响应?
- 需要被监听的数据是如何收集起来作为被观察者(Observable)的?
- 观察者和被观察者是如何创建的?
- 被观察者(Observable)如何感知到数据变化,并通知给观察者的?
- 观察者和被观察者之间的订阅关系是如何建立起来的?(watcher被加入到watcher list中)
前面说过,Vue很多特性都是基于响应式系统来实现的,这里为了便于说明, 用一个简单的demo来讨论这些问题,先定义一个Vue组件:
const com = new Vue({
data: {
age: 1
},
watch: {
age: function(val) {
console.log('age is changed...')
}
}
})
定义一个侦听器,监听data对象中的age的变化。
被观察者的创建
对于一个Vue组件而言,它的状态主要来自于外部传入的Props(父组件状态)和自身声明的data。
毫无疑问,这些数据在整个体系中都扮演了被观察者
的角色。
在组件的创建和初始化过程中,Vue会在initState
中对这部分数据进行处理,
略去初始化相关操作,这里重点关注对于状态数据的data的处理:
function initData (vm: Component) {
let data = vm.$options.data
// data相关的初始化操作
...
// observe data
observe(data, true /* asRootData */)
}
对于组件自身状态data的相关处理会调用observe
为data对象创建一个附属的Observer
实例:
function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 为value创建过Observer实例
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&// 全局观测开关
!isServerRendering() &&// 非服务端渲染
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
这里,data对象的Observer
实例只有一个,缓存挂在data.__ob__
上。
Observer
实例在构造的过程中,会将对自己所附属的对象(data)的属性进行处理:
class Observer {
value: any;// 目标对象
dep: Dep; // 依赖收集器,先不用管,后面介绍
vmCount: number; // vmCount 统计了以此target为根$data的Vue实例的数量
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)// 将Observer实例绑定到对应目标对象上
if (Array.isArray(value)) {// 覆写对于数组的操作,大约7种
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历并使用observe方法处理数组的每一个元素
this.observeArray(value)
} else {
// 遍历目标对象的各个属性
this.walk(value)
}
}
...
}
对于数组行为的覆写操作我们这里暂且不关心。
walk
方法会遍历目标对象的每个属性,对每个属性调用defineReactive
进行处理:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
defineReactive
通过重写getter
/setter
的方法Hook了对于该属性的读写操作。
读到这里相必你对之前问题有点想法了:
被观察者(Observable)如何感知到数据变化,并通知给观察者的?
data中定义的数据毫无疑问是我们的被观察者,
当data中定义的数据发生改变(写操作),该属性的setter
方法就会被执行,这样我们就有机会发消息给观察者了。
我们的被观察者虽然有了感知数据变化的能力,但还需要维护一个列表(watcher list)来存放订阅它的观察者,也就是上面Observer
中出现过的Dep
。
我们看看defineReactive
中具体是如何完成这两部分工作的:
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 为每个属性都定义了一个watch list
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 获取到该属性的`Observer`对象, 如果val不是object则为null
let childOb = !shallow && observe(val)
// 在目标对象上重新定义该属性,主要是重写getter setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
....
return value
},
set: function reactiveSetter (newVal) {
....
}
})
}
我们以demo中的data为例子,属性age经过defineReactive
处理后:
dep
中维护了一个数组subs,也就是我们说的观察者列表:
class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;// 观察者列表
constructor () {
this.id = uid++
this.subs = []
}
}
数据属性的Observer
已经创建完毕,那么观察者是如何创建的呢?
观察者的创建
我们在demo中定义了一个侦听器,用来监听age的变化,并回调指定函数完成操作。
对于组件中所定义的侦听器,同样也是在initState
中来进行初始化:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
基本上是兼容侦听器的各种姿势的语法糖,最后调用挂载Vue上的$watch
方法:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
...
const watcher = new Watcher(vm,
expOrFn,// age
cb, // 回调函数
options)
...
return function unwatchFn () {
watcher.teardown()
}
}
这里直接new了一个Watcher
实例出来,来看下我们的观察者在创建阶段做了哪些工作:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
vm._watchers.push(this)
this.cb = cb // 回调函数
this.id = ++uid // uid for batching
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 这里解析出了之前所声明观察对象的age的getter方法
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
当我们创建一个观察者来响应数据变化,首先要知道要观察什么数据(age),例如组件的侦听器声明了自己要监听age的变化。
另外一点需要注意的是,组件中所声明的数据在初始化过程中,被挂在了组件实例上,也就是说可以通过vm.age获取到对应的值。
观察者创建完毕后,此时还无法接收到被观察者的消息,两者之间需要建立连接关系。 这里通常会有两种设计选择:
- 被观察者维护一个观察者注册表,被观察者数据变化,主动推送数据给观察者;
- 观察者当需要获取数据的时候,主动去找被观察者拉取数据
我们来看下Vue是如何构建和维护两者之间的关系的。
建立订阅关系
在Watcher
对象的构造函数中,除了完成各种初始化工作,还在末尾调用了get
方法,需要把自己注册给被观察者。
末尾调用了watcher的get
方法:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 调用age的getter方法
value = this.getter.call(vm, vm)
} catch (e) {
...
} finally {
...
popTarget()
this.cleanupDeps()
}
return value
}
调用get
方法除了让观察者主动拉取被观察者的数据之外,还会将自己注册到被观察者的注册表中。
这里实质上就是调用了age
属性的getter
方法,
也就是之前在defineReactive
中被hook为reactiveGetter
:
get: function reactiveGetter () {
// 原始数据值
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()// 收集依赖
if (childOb) {// 递归处理属性值为object类型的属性
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
这里我们可能会有一个疑问:Dep.target
是干啥的?
从上面代码中我们不难看出,age和侦听器之间的订阅关系是在reactiveGetter
中建立的;
但是,age的值又不仅仅是只被一个侦听器所使用,只要我们对age进行读操作都会调用reactiveGetter
方法。
所以Dep
就维护了一个全局的标识位target
,在watcher调用的时候才设置:
pushTarget(this)
...
popTarget()
被观察者也只有在Dep.target
不为空的时候执行depend
方法建立订阅关系:
// dep.depend
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// watcher.addDep
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
这里的订阅关系建立其实是双向的:watcher对象本身也维护了一个数组存放被观察者,也就是说一个观察者可以订阅多个被观察者对象。
这里完成的操作就是互相把自己注册给对方。
更新响应
当被观察者的数据发生变化,一定会调用对应数据的setter
方法:
set: function reactiveSetter (newVal) {
// 原始值
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 只定义了getter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 属性的子属性需要重新被observe
childOb = !shallow && observe(newVal)
// 通知观察者数据改变
dep.notify()
}
除了对数据进行赋值的写操作外,还调用了dep.notify()
来通知观察者们执行数据更新后的回调函数。
其他
在Vue组件中,我们声明了所要观察的数据状态,这些数据状态都会在组件初始化过程中被observe
递归转换为被观察者对象(descriptor被Hook)。
但是并不是所有的被观察者都会被订阅,由于Vue把两者订阅关系的建立放在了具体观察者的构建过程中完成,这样就保证了只有那些真正被使用到的数据状态才会被监听。
总结
上面我们讨论了Vue响应式系统构建的主要流程以及这个过程中的相关知识,其中也忽略掉了很多细节上的处理。 在学习源码的过程中,我们需要理解这背后的设计思想和原因,而不是一味地拿着观察者模式去生搬硬套。
其他
- ES6 Proxy
- 观察者模式
- 代理模式