--本文采自本人公众号【猴哥别瞎说】java
这篇文章咱们来详细了解数据响应式的原理与具体实现。react
聊到 Vue 的数据响应,不少人都会对其非侵入式的数据响应系统津津乐道,大概都知道它是经过数据劫持的方式(修改 Object.defineProperty() )来轻量化实现数据响应式。express
所谓轻量化,指的是:Vue 中的数据模型仅仅是一个个普通的 javaScript 对象,当使用者修改它们的时候,对应的页面 UI 会进行更新。无需直接操做 DOM 元素的同时,对于数据模型的状态管理也变得简单明了。数组
回顾上篇文章,咱们讲到了Watcher: 它与Vue组件实例之间是一一对应的关系。说点题外话:实际上啊,在Vue1.x系列的时候,每个响应式变量都会有一个Watcher。开发者发现这样的粒度太细了,因而在Vue2.x的时候,就变成了更高粒度的划分:一个Vue组件实例对应一个Watcher。bash
Vue的数据化响应的具体实现,实际上依赖的还有两个重要成员:Observer 与 Dep。Observer、Dep、Watcher三者之间的关系在 Vue2.x 中可经过下图简单展现:函数
Observer、Dep、Watcher 之间,经过发布-订阅模式的方式来进行交互。在数据初始化的时候,Watcher 就会订阅对应变量的 Dep。当有数据变化的时候,Observer 经过数据劫持的方式,将数据的变动告知 Dep,而 Dep 则会通知有关联关系的 Watcher 进行数据更新。正如上一节课讲到的,Watcher的notify 过程当中调用了 updateComponent,其包含了两个重要步骤:render 与 update。这两个步骤最终会更新真实页面。post
在一个 Vue 组件实例中,Watcher 只有一个。而实例中的每个响应式变量都会有一个 Dep。因而,一个组件中的 Watcher 与 Dep 之间的关系,是一对多的关系。ui
而现实应用中,Vue 组件确定不止一个啊。组件内部还会嵌套组件,而响应式变量有可能会与多个组件产生关联。因而,在这个层面上,Dep 会对应多个 Watcher。this
综上,Watcher 与 Dep 之间,是多对多的关系。spa
咱们的目标是:尝试经过阅读源码的方式,将整个知识点串起来。
沿着上节课的引子,咱们能够在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 操做。
因而,这个针对对象的数据响应化过程,就基本走通了。在下一文章中,咱们来看看针对数组的数据响应化过程是怎样的,它与对象的响应化过程有何不一样?