Vue源码阅读笔记之响应式原理(二)
这是"Vue源码阅读笔记"系列第二篇文章。前一篇文章中我们提到initState
中调用的initData
方法,调用observe(data)
完成了对数据的观察。
在很早之前的对象描述符与响应式数据这篇文章中,对于defineProperty
做了简单的整理,并实现了一个比较粗糙的响应式数据更新视图的例子。接下来让我们深入Vue的核心部分:响应式数据的工作原理。
参考:
接下来我们看看Vue中initData
是如何实现的响应式数据的。跟前面一样,我们对initData
进行简化
function initData (vm: Component) {
let data = vm.$options.data
// 对数据进行代理
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
// 观察数据变化
observe(data, true /* asRootData */)
}
在initData
中做了两件事
- 将数据代理到this上
- 实现响应式数据
数据代理
在Vue中,我们可以通过 this.msg
访问到 this.$data.msg
,这是通过数据代理实现的,原理十分简单,即修改对应的getter和setter。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
响应式数据
在Vue文档中提到了响应式数据的基本原理,稍作整理得到下列结论
- 数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新
- 如何监听数据的变化呢?所谓监听数据变化,实际上是监听数据某个属性值的变化,Vue使用的是
Object.defineProperty
。 - 数据属性值变化时该如何更新视图呢?通过发布-订阅者模式,维护一个订阅该属性值的队列,并在值变化时通知所有订阅者,更新视图
其中,Object.defineProperty
和订阅者模式在对象描述符与响应式数据这里已经提到过,这里就不再赘述了,我们来看看Vue中是如何实现的。
在Vue中,由Observer
、Dep
和Watcher
这三个类构成了Vue响应式系统(即observer
模块)的核心。observer
模块位于/src/core/observer
下面。
此处强烈推荐observer-dep-watch这个项目,相当于一个精简版的响应式系统。下面的源码中,我移除了一些细节代码,这样可以更清晰地了解整个模块的结构。
###Observer
Observer
用来将数据转换成可监控的值
// /src/core/observer/index.js
class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 数据对象自身的观察者
def(value, '__ob__', this)
// 需要对数组进行单独处理
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
}
Watcher
Watcher
是数据变化的订阅者,用来执行数据变化之后的某些操作(比如更新视图)
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
this.cb = cb
this.expression = expOrFn.toString()
// parse expression for getter
// 把需要观察的属性值表达式解析成一个可执行的函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
// ...
}
// 这里会调用pushTarget和一次getter
this.value = this.lazy ? undefined : this.get()
}
}
构建一个Watcher
对象需要的构造参数中包括
expOrFn
,表示需要监听的属性名cb
,对应属性名的值发生变化时的回调,用于处理相应的业务逻辑(比如更新视图)
###Dep
Dep
用来连接Observer
和Watcher
,并充当事件变化的发布者:通过Observer
提供的set和get,在get中收集Watcher
,在set中通知Watcher
。
class Dep {
constructor () {
this.id = uid++
this.subs = []
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
上面只是复制粘贴了相关的实现源码,那么,这三个类是如何关联起来的呢?
Observer与Dep的关联
首先,在Observer
的walk方法中,对数据对象的每个属性进行劫持
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
可以看见实际上是defineReactive
在工作,这个函数在其他的源文件中也出现过很多次。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 递归劫持setter和getter
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val
if (Dep.target) {
// 收集依赖
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = val
// 新旧值相同不触发更新的原因
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = !shallow && observe(newVal)
// 通知订阅者
dep.notify()
}
})
}
这里通过defineReactive
与set
和get
形成闭包,为数据的每个属性(包括子属性)都维护了一个Dep
对象,可以看见:
- 在
get
中通过dep.depend()
添加订阅者 - 在
set
中通过dep.notify()
通知订阅者
其中还有一些检测属性描述符configurable
、已设置的setter
和getter
等代码,这里先省略了
Watcher与Dep的关联
这个就简单了,Dep作为发布者,Watcher作为订阅者,直接通过其接口关联即可
// Dep.addSub
addSub (sub: Watcher) {
this.subs.push(sub)
}
// Watcher.addDep
addDep (dep: Dep) {
const id = dep.id
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
然而在上面添加订阅者的时候,我们发现是通过dep.depend()
来实现的
// Dep.depend
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
这里使用了一个静态对象Dep.target
,来标识当前的Watcher
,
// dep.js
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
在Watcher
构造函数中的最后一句调用了this.get()
// watcher.js
this.value = this.lazy ? undefined : this.get()
// Watcher.get
get(){
// 这里先将Dep.target关联到当前的Watcher实例对象
pushTarget(this);
try {
// getter实际上就是Watcher观察的属性表达式
value = this.getter.call(vm, vm)
}catch (e) {
// ...
}
}
分析
下面一步步分析:
Watcher
的构造函数中,我们可以了解到this.getter
实际上就是构造参数expOrFn
的函数形式(这是在/src/core/util/lang.js
的parsePath
实现的),其作用就是获取数据对象对应属性表达式的值,这里相当于调用了一次get
,- 调用
get
,这样就可以触发defineReactive
中该属性描述符get
中的dep.depend()
, - 由于在调用
get
之前先通过pushTarget(this)
将Dep.target关联到当前的Watcher实例对象,从而为当前属性的发布者dep
添加订阅者。
再回头看一看整个流程,我们传入了data
数据对象
- 首先通过
observe(data
),实例化一个Observer
对象,其作用是通过defineReactive
劫持对象属性描述符 - 数据对象data的每一个属性值都有一个
Dep
实例,在属性的get
中收集订阅者,在属性的set
中通知订阅者 - 在需要观察的数据属性的地方,实例化
Watcher
对象,在实例化的过程中,通过pushTarget(this)
和一次this.getter.call(vm, vm)
,触发dep.depend
实现依赖的收集
可以看见,Dep
对象充当联结Observer
和watcher
对象之间的桥梁。实际上,属性值的set
描述符才是真正的发布者。
单独处理的数组
列表渲染的文档中提到
Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。
在JavaScript中,数组是一种特殊的对象,通过walk
遍历对象属性的方法,并不能实现预期的依赖收集和更新通知,那么该如何检测数组本身的变化,诸如添加元素、移除元素呢?
Vue的做法是新建一个数组原型并重写相关方法
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
其中arrayMethods
就是增强版的数组原型
// /src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
// ...
// 在这里通知所有的订阅者
ob.dep.notify()
})
}
然后修改数组数据对象的原型,使其指向增强版的数组原型
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
// 如果是数据对象为数组,修改其原型为增强版的数组原型
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
现在遍历数组,然后分别为每个数组元素均实例化Observer对象即可。
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
小结
通过分析,现在对于Vue中的响应式系统有了一个更直观的了解~尽管仍旧忽略了不少实现细节。然后明白了其中的一些限制
- Vue不能检测到对象属性的添加或删除,这是因为整个
observer(data)
是在初始化的时候对属性进行劫持的,解决这个问题的办法是通过Vue.set()
这个API,或者创建一个包含原对象属性和新属性的新对象,通过赋值的形式 - Vue无法检测到数组的长度变化,即
this.arr[this.arr.length] = xxx
无法触发更新。除非我们使用Vue实现的增强版数组原型方法才可以。
我们知道,响应式数据最大的好处就是在数据更新时自动更新视图,让开发者只需要关注数据本身。现在了解了整个响应式数据的原理,接下来就是了解Vue的视图渲染,以及数据更新时触发的视图更新。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。