@frank-shaw
2019-11-30T10:19:08.000000Z
字数 5683
阅读 1453
vue
源码
数据响应式
这节课我们来详细了解数据响应式的原理与具体实现。聊到Vue的数据响应,很多人都会对其非侵入式的数据响应系统津津乐道,大概都知道它是通过数据劫持的方式(修改Object.defineProperty())来轻量化实现数据响应式。
所谓轻量化,指的是:Vue中的数据模型仅仅是一个个普通的javascript对象,当使用者修改它们的时候,对应的页面UI会进行更新。无需直接操作DOM元素的同时,对于数据模型的状态管理也变得简单明了。
回顾上节课,我们讲到了Watcher: 它与Vue组件实例之间是一一对应的关系。说点题外话:实际上啊,在Vue1.x系列的时候,每一个响应式变量都会有一个Watcher。开发者发现这样的粒度太细了,于是在Vue2.x的时候,就变成了更高粒度的划分:一个Vue组件实例对应一个Watcher。
Vue的数据化响应的具体实现,实际上依赖的还有两个重要成员:Observer与Dep。Observer、Dep、Watcher三者之间的关系在Vue2.x中可通过下图简单展示:
Observer、Dep、Watcher之间,通过发布-订阅模式的方式来进行交互。在数据初始化的时候,Watcher就会订阅对应变量的Dep。当有数据变化的时候,Observer通过数据劫持的方式,将数据的变更告知Dep,而Dep则会通知有关联关系的Watcher进行数据更新。正如上一节课讲到的,Watcher的notify过程中调用了updateComponent,其包含了两个重要步骤:render与update。这两个步骤最终会更新真实页面。
在一个Vue组件实例中,Watcher只有一个。而实例中的每一个响应式变量都会有一个Dep。于是,一个组件中的Watcher与Dep之间的关系,是一对多的关系。
而现实应用中,Vue组件肯定不止一个啊。组件内部还会嵌套组件,而响应式变量有可能会与多个组件产生关联。于是,在这个层面上,Dep会对应多个Watcher。
综上,Watcher与Dep之间,是多对多的关系。
我们的目标是:尝试通过阅读源码的方式,将整个知识点串起来。
沿着上节课的引子,我们可以在src/core/instance/init.js
文件的initState()
函数中查看:
function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initState中有很多需要初始化的属性:props/methods/coumputed。我们此时只关注data部分。留意到observe()
方法,进入src/core/observer/index.js
(与数据响应式相关的代码都是src/core/observer/
内)可知:
function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
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
}
可以看到,observe()
的作用就是返回一个Observer对象。于是重点来了:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
//值得留意:Observer对象在一个Vue组件实例中存在多个,取决于data数据嵌套了几个Object对象或数组
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
//如果是数组
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
//如果是对象
this.walk(value)
}
}
只看是对象的情况,于是进入到walk()
方法:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
这里就可以知道,Observer的作用就是:针对data对象的每一个属性,分别对其进行数据响应化处理。值得留意:Observer对象在一个Vue组件实例中存在多个,取决于data数据嵌套了几个Object对象或数组。
Observer是如何与Dep、Watcher关联起来的?我们先来看看Dep、Watcher长啥样子,然后再来进入到最核心的defineReactive()
。
看看Dep的结构吧:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
从内部变量属性可知,其包含的静态变量target是一个Watcher,其包含的常规变量subs是Watcher数组。其内部主要的两个方法:depend()
关联对应的Watcher,notify()
通知对应的Watcher进行update操作。
Watcher的结构如下:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
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)
}
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
这里对Watcher的代码进行了一定的简化。通过声明可知:deps
变量是Dep数组。其核心方法有这三个:addDeps()
与Dep之间相互关联,get()
调用updateComponent方法,update()
执行批量更新操作。
Dep中的subs为Watcher数组,Watcher中的deps为Dep数组。也验证了之前的描述:
Watcher与Dep之间,是多对多的关系。
此刻,我们进入到最核心的defineReactive()
:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//一个key 一个dep(),一一对应
const dep = new Dep()
...//忽略
//val如果是对象或者数组,会递归observe
//每一个对象或数组的出现,都会出现一个新的Observer
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
//
//值得注意的是,在initState的时候,并没有触发Dep.target,因为还没有Watcher生成,
//Watcher的产生是在第一次$mounted的过程中生成的
//而以后每次触发Dep.pushTarget的时候,都会将Dep.target再次被引用到具体的Watcher
//比如:
// watcher.js中的 get()
// state.js中的 getData()
// lifecycle.js中的 callHook()
if (Dep.target) {
//depend()是相互添加引用的过程
//一个Vue实例只有一个Watcher,一个key就有一个Dep
//在单一Vue组件实例中,Watcher与Dep之间,是一对多的关系
//考虑到Vue实例存在嵌套(或用户手写了watch表达式),Dep中会保存多个Watcher(存在subs数组中)
//这样,当key发生变化时,对应的Watcher的notify()方法就会被触发,对应Vue实例就会更新页面
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
...//忽略
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
//特别处理:如果最新赋值是对象,该对象仍然需要响应化处理
childOb = !shallow && observe(newVal)
//Dep通知更新
dep.notify()
}
})
}
从代码可知:
- 每一个data变量都会有一个Dep。
- get data变量的时候,会触发
dep.depend()
,将Dep与Watcher之间进行关联。- set data变量的时候,会触发
dep.notify()
,通知Dep对应的Watcher进行对应的更新操作。
关联上节课讲到的,Watcher更新的过程会触发updateComponent,于是会重新执行$._render()
函数与$._update()
函数,生成虚拟DOM,进而更新真实DOM操作。
于是,这个针对对象的数据响应化过程,就基本走通了。下一课,我们来看看,针对数组的数据响应化过程是怎样的,它与对象的响应化过程有何不同?