数据响应式原理
在之前的章节中,我们讲解了Vue的实例化,从模版编译-> ast -> render函数 -> vnode -> 初次渲染。
但是界面初次渲染之后,我们需要使用系统,在使用系统的过程中,我们会修改掉许多数据,在修改的同时,界面会随机发生变化,这就是所谓的数据响应式,也就是界面随着数据的变化而变化,我们只需修改数据,界面就会跟着响应,而不需要手动写代码再修改界面。
数据响应式最典型的就是当我们改变options.data中定义的值的时候,绑定在界面上的值就会跟着发生变化。那么Vue框架是如何处理data这个函数返回的数据(一般是返回一个对象)呢?
initState
Vue的_init方法中,initState(vm)方法中初始化了props,methods,data,computed,watch
export function initState (vm: Component) {
// 定义watcher
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化methods
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)
}
}
initData
接下来我们优先着重分析Vue是如何处理data,实现数据响应式的,initData(data)方法主要是调用了observe()方法
function initData (vm: Component) {
// 获得data数据
let data = vm.$options.data
// 如果是函数,通过getData拿到data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 如果不是对象,报错
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
/*
省略非重要代码
*/
// observe data
// 调用observe方法
observe(data, true /* asRootData */)
}
observe
接下来我们看一下observe(data, true)方法,此方法主要调用了new Observer(value)方法,实例化之后赋值给data._ _ ob _ _
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果data不是对象/数组 或者 是一个Vnode,直接返回,不做处理
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果value有__ob__属性,并且是由Observer构造出来的,就拿到这个属性
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
// 如果可监听,因为有时候可以关闭将data处理成响应式对象
shouldObserve &&
// 不是服务端渲染
!isServerRendering() &&
// 如果是数组或者对象
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
// 不是vue的实例
!value._isVue
) {
// new一个Observer对象
ob = new Observer(value)
}
// 如果已经存在Observer对象实例了,一般情况下初始化是没有的
if (asRootData && ob) {
ob.vmCount++
}
// 将Observer对象实例返回
return ob
}
new Observer(data)
接下来我们分析一下new Observer(data)发生了什么 ,Observer的构造方法主要是实例化了一个Dep对象,如果是数据是数组,递归调用observe()方法,如果不是数组,就遍历对象的key,调用defineReactive方法,使之成为响应式的数据
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
// 实力化dep属性
this.dep = new Dep()
// 一个data对象,作为vue实例根数据的次数,也就是多个vue实例可以使用同一份data数据
this.vmCount = 0
// 将Observer实例,赋值给value.__ob__
def(value, '__ob__', this)
// 如果value 是数组的话,这里主要是重写了数组的原型方法,使数组也能实现数据响应式,
// 即push(),shift(),pop()等方法调用后,界面可以跟着改变
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 如果是数组,又会递归调用observe(value)方法,将数组的每一项作为参数
this.observeArray(value)
} else {
// 如果不是数组,遍历,并调用defineReactive方法,使之成为响应式的数据
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
defineReactive
接下来我们着重分析一下defineReactive方法
defineReactive方法作用就是给属性设置get和set方法,每个属性值都会创建一个依赖收集对象dep,每当调用属性的get方法,就会调用dep.depend()方法进行依赖收集的过程。
每个组件会创建一个Watcher对象,如果某个组件使用了一个数据(也就是调用了该属性的get方法),那么这个数据的dep.subs就会保存这个组件的Watcher对象,这就是依赖收集的过程。
当我们给属性设置值的时候,会调用该属性的set方法,就会调用该数据的dep对象的notify()方法,notify()方法会触发所有保存在dep.subs中的Watcher对象(即观察者)的update方法,触发更新操作。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 定义依赖收集对象
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]
}
// 递归调用observe(val)方法将 val是数组或者对象变为响应式数据
let childOb = !shallow && observe(val)
// 定义属性的getter方法
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Dep.target指向当前正在渲染的组件的Watcher实例子
if (Dep.target) {
// 如果在组件渲染过程中用到了data中的响应式数据,也就会调用改属性的get方法,
// 那么就会进行依赖收集的过程,
// 其实也就是将当前正在渲染的组件的watcher对象,添加到dep.subs中
// 注意这里的dep对象因为闭包不会被销毁
dep.depend()
// 如果该数据是个数组或对象,那么同样会递归进行依赖收集的过程
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
/*省略非重要代码*/
// 通知watcher更新
dep.notify()
}
})
}
依赖收集Dep
上述分析中出现了Dep对象,和Watchaer对象,接下来我们先大致了解一下这两个类
其实依赖收集的过程就是以一个发布订阅者为模型而实现的,dep扮演的角色就是订阅中心,每个数据都有一个dep(订阅中心),Watcher就是订阅者(观察者),如果订阅者订阅了某一份数据(发布者),那么这份数据的dep就会记录下该订阅者,当这份数据(发布者)有修改的时候,他会告诉订阅中心dep,通知所有的观察者数据已经修改了。
export default class Dep {
// 静态的实例,类似全局变量,当前正在计算的Watcher
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
// 依赖收集对象id
this.id = uid++
// 保存依赖于该数据的观察者Watcher
this.subs = []
}
// 添加依赖该数据的观察者对象
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除观察者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 依赖收集
depend () {
// Dep.target指向当前正在渲染的组件的Watcher实例子
if (Dep.target) {
// this指向dep的实例
Dep.target.addDep(this)
}
}
// 通知观察者触发更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// 调用Watcher.update方法
subs[i].update()
}
}
}
观察者Watcher
我们知道每一个数据都会生成一个dep对象(订阅中心),订阅中心会收集并记录下所有的数据订阅者Watcher,那么我们看看Watcher这个类吧。
每个组件在实例化的时候,调用$mount方法,此方法会经过 模版编译-> ast -> Vnode -> 生成真实dom的过程,在mountComponent方法的最后,会实例化一个Watcher对象,我们称之为渲染Watcher
/* istanbul ignore if */
updateComponent = () => {
// 先调用_render()方法生成vnode 然后调用_update方法,更新真实dom
vm._update(vm._render(), hydrating)
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
我们看一下Watcher类的构造函数,其实就是定义了一些属性,最后调用了this.get方法
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
get()方法中主要逻辑就是执行this.getter,this.getter是在构造方法中做了处理并设置,如果是渲染时$mount方法中创建的渲染Watcher,this.getter就是传入的_update的一个渲染真实dom的方法,总之get()方法就是先执行完整的首次渲染操作。
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 设置当前正在渲染的组件的渲染Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// getter是在构造方法中设置的
// 执行getter方法,其实就是传入的_update方法,执行其可以生成vnode,首次渲染时创建真实dom
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
}
接下来我们看一些Watcher类的一些重要方法
addDep方法就是将当前的Watcher实例,push到dep.subs这个数组中,记录下订阅了dep的观察者,此方法会在响应式对象属性的getter方法中被调用,以记录下订阅了该数据的订阅者们
/**
* Add a dependency to this directive.
*/
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)) {
// this指向Watcher实例
// Watcher实例添加至Dep的subs中
dep.addSub(this)
}
}
}
update方法,如果是同步渲染,那么会执行this.run方法立即重新渲染,如果是异步渲染,那么会将渲染的Watcher推到异步队列中,延迟重新计算并渲染,一般情况下我们都是使用的异步渲染,也就是说频繁修改数据,只会触发一次 执行render函数 -> 生成新的vnode -> 更新dom 的过程,异步更新的实现将在专门的章节讲解,这里我们只要知道Watcher的update方法能响应数据的变化,重新渲染界面就可以了。
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 通常情况会走异步队列更新
queueWatcher(this)
}
}
示例
接下来我们以一个示例来说明Vue的数据响应式的过程
首先我们的代码是这样的,为了方便演示效果,我们将子组件和根实例使用同一份data。当我们点击p标签的时候,message的值会发生变化,界面也会随着发生变化
<body>
<div id="app">
<p @click="click">{{message}}</p>
<comp1></comp1>
</div>
</body>
<script>
let data = {
array: [1,2,3],
message: "hello vue",
}
var app = new Vue({
el: "#app",
data: data,
methods: {
click () {
this.message = "Hello World"
}
},
components: {
comp1: {
template: '<h1>{{message}}</h1>',
props: {
prop1: String
},
data: function () {
return data
},
}
},
})
以上代码有两个组件,一个是根组件,一个是comp1组件,这两个组件都会使用同一份数据,那就是data.message。
在初始化数据的过程中,observe方法会将data中的属性都变为响应式属性,也就是给data中的数据设置getter和setter方法。在渲染界面的时候,会用到data.message这个值,那么就会调用message这个属性的getter方法,getter方法中会进行依赖收集的过程,我们在getter方法中打印出该属性和创建的dep对象
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
// 打印
console.log(dep, value)
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
由上图我们可以看到,打印出了两个Dep对象,由于我们是使用的同一份数据,其实这两个dep对象是同一个,因为根组件和子组件Comp1都使用的data.message这个值,所以会调用两次getter方法。
我们再观察Dep中的subs中保存了两个Watcher对象,这两个Watcher对象就是根组件和Comp1子组件在实例化的时候创建的,因为这两个组件都使用了data.message这个数据,所以dep就会收集依赖,将Watcher 观察者们保存起来。
至此依赖收集的过程就算结束了。
那么当数据发生变化的时候,是如何通知到所有的数据订阅者呢?
当数据发生变化时(示例中是点击p标签会改变data.mesage的值),会调用该属性值的setter方法, setter方法中会调用dep实例的notify方法
set: function reactiveSetter (newVal) {
// 代码省略
dep.notify();
}
dep.notify方法中其实就是遍历dep.subs中的Watcher,调用每一个Watcher的update方法,这样就可以通知到根组件和Comp1子组件重新渲染界面了。
Dep.prototype.notify = function notify () {
// 代码省略
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
其实依赖收集(订阅)和派发更新(发布)的过程很简单。重点在于理解发布订阅者模式。
总结
Vue的响应式原理其实就是界面可以响应数据的变化随即跟着从新渲染,而不需要我们手动再更新界面。
实现数据响应式的关键就在于 对数据的处理,给对象的属性设置了getter和setter方法,在使用到该数据的地方会调用getter方法,每个数据会维护一个dep对象(类比订阅中心),该对象中保存了使用该数据的所有观察者(订阅者),当该数据(发布者)更新(发布更新)的时候,会通知到所有dep(订阅中心)中保存的观察者们,告诉他们数据发生了变化, 观察者们接收到数据发生变化的消息,就会调用update方法进行界面的重新渲染。