概述
Vue 的响应式模型指的是:
- 视图绑定的数据在更新后也会渲染到视图上
- 使用
vm.$watch()
监听数据的变化,并调用回调 - 使用Vue实例的属性
watch
注册需要监听的数据和回调
上面的三种方式追根揭底,都是通过回调的方式去更新视图或者通知观察者更新数据
Vue的响应式原理是基于观察者模式和JS的API:Object.defineProperty()
和Proxy
对象
主要对象
每一个被观察的对象对应一个Observer实例,一个Observer实例对应一个Dep实例,Dep和Watcher是多对多的关系,附上官方的图,有助于理解:
1. Observer
一个被观察的对象会对应一个Observer实例,包括options.data
。
一个Observer实例会包含被观察的对象和一个Dep实例。
export class Observer {
value: any;
dep: Dep;
vmCount: number;
}
复制代码
2. Dep
Dep实例的作用是收集被观察对象(值)的订阅者。
一个Observer实例对应一个Dep实例,该Dep实例的作用会在Vue.prototype.$set
和Vue.prototype.$del
中体现——通知观察者。
一个Observer实例的每一个属性也会对应一个Dep实例,它们的getter都会用这个Dep实例收集依赖,然后在被观察的对象的属性发生变化的时候,通过Dep实例通知观察者。
options.data
就是一个被观察的对象,Vue会遍历options.data
里的每一个属性,如果属性也是对象的话,它也会被设计成被观察的对象。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array;
}
复制代码
3. Watcher
一个Watcher对应一个观察者,监听被观察对象(值)的变化。
Watcher会维护一个被观察者的旧值,并在被通知更新的时候,会调用自身的this.getter()
去获取最新的值并作为要不要执行回调的依据。
Watcher分为两类:
-
视图更新回调,在数据更新(setter)的时候,watcher会执行
this.getter()
——这里Vue把this.getter()
作为视图更新回调(也就是重新计算得到新的vnode)。 -
普通回调,在数据更新(setter)的时候,会通知Watcher再次调用
this.getter()
获取新值,如果新旧值对比后需要更新的话,会把新值和旧值传递给回调。
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;
newDeps: Array;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
}
复制代码
使options.data
成为响应式对象的过程
Vue使用initData()
初始化options.data
,并在其中调用了observe
方法,接着:
- 源码中的
observe
方法是过滤掉不是对象或数组的其它数据类型,言外之意Vue仅支持对象或数组的响应式设计,当然了这也是语言的限制,因为Vue使用API:Object.defineProperty()
来设计响应式的。 - 通过
observe
方法过滤后,把传入的value再次传入new Observer(value)
- 在Observer构造函数中,把Observer实例连接到value的属性
__ob__
;如果value是数组的话,需要修改原型上的一些变异方法,比如push、pop
,然后调用observeArray
遍历每个元素并对它们再次使用observe
方法;如果value是普通对象的话,对它使用walk
方法,在walk
方法里对每个可遍历属性使用defineReactive
方法 - 在
defineReactive
方法里,需要创建Dep的实例,作用是为了收集Watcher实例(观察者),然后判断该属性的property.configurable
是不是false(该属性是不是不可以设置的),如果是的话返回,不是的话继续,对该属性再次使用observe
方法,作用是深度遍历,最后调用Object.defineProperty
重新设计该属性的descriptor - 在descriptor里,属性的getter会使用之前创建的Dep实例收集Watcher实例(观察者)——也是它的静态属性
Dep.target
,如果该属性也是一个对象或数组的话,它的Dep实例也会收集同样的Watcher实例;属性的setter会在属性更新值的时候,新旧值对比判断需不需要更新,如果需要更新的话,更新新值并对新值使用observe
方法,最后通知Dep实例收集的Watcher实例——dep.notify()
。至此响应设计完毕 - 看一下观察者的构造函数——
constructor (vm, expOrFn, cb, options, isRenderWatcher)
,vm表示的是关联的Vue实例,expOrFn用于转化为Watcher实例的方法getter并且会在初始化Watcher的时候被调用,cb会在新旧值对比后需要更新的时候被调用,options是一些配置,isRenderWatcher表示这个Watcher实例是不是用于通知视图更新的 - Watcher构造函数中的
expOrFn
会在被调用之前执行Watcher实例的get()
方法,该方法会把该Watcher实例设置为Dep.target,所以expOrFn
里的依赖收集的目标将会是该Watcher实例 - Watcher实例的value属性是响应式设计的关键,它就是被观察对象的getter的调用者——
value = this.getter.call(vm, vm)
,它的作用是保留旧值,用以对比新值,然后确定是否需要调用回调
总结:
- 响应式设计里的每个对象都会有一个属性连接到Observer实例,一般是
__ob__
,一个Observer实例的value属性也会连接到这个对象,它们是双向绑定的 - 一个Observer实例会对应一个Dep实例,这个Dep实例会在响应式对象里的所有属性的getter里收集Watcher实例,也就是说,响应式对象的属性更新了,会通知观察这个响应式对象的Watcher实例
- 在Vue里Watcher实例,可以是视图更新回调,也可以是普通回调,本质上都是一个函数,体现了JS高阶函数的特性
- Vue的响应式设计很多地方都使用了遍历、递归
Vue提供的其它响应式API
Vue除了用于更新视图的观察者API,还有一些其它的API
1. Vue实例的computed属性
构造Vue实例时,传入的options.computed
会被设计成既是观察者又是被观察对象,主要有下面的三个方法:initComputed、defineComputed、createComputedGetter
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
复制代码
2. Vue实例的watch属性
在实例化Vue的时候,会把options.watch
里的属性都遍历了,然后对每一个属性调用vm.$watch()
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)
}
复制代码
vm.$watch
被作为一个独立的API导出。
3. Vue.prototype.$watch
Vue.prototype.$watch
是Vue的公开API,可以用来观察options.data
里的属性。
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
复制代码
4. Vue.prototype.$set
Vue.prototype.$set
用于在操作响应式对象和数组的时候通知观察者,也包括给对象新增属性、给数组新增元素。
Vue.prototype.$set = set
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array | Object, key: any, val: any ): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
复制代码
ob.dep.notify()
之所以可以通知观察者,是因为在defineReactive
里有如下代码:
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
复制代码
上面的childOb.dep.depend()
也为响应式对象的__ob__.dep
添加了同样的Watcher实例。所以Vue.prototype.$set
和Vue.prototype.$del
都可以在内部通知观察者。
5. Vue.prototype.$del
Vue.prototype.$del
用于删除响应式对象的属性或数组的元素时通知观察者。
Vue.prototype.$del = del
/**
* Delete a property and trigger change if necessary.
*/
export function del (target: Array | Object, key: any ) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
复制代码
简单实现响应式设计
- 实现Watcher类和Dep类,Watcher作用是执行回调,Dep作用是收集Watcher
class Watcher {
constructor(cb) {
this.callback = cb
}
update(newValue) {
this.callback && this.callback(newValue)
}
}
class Dep {
// static Target
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify(newValue) {
this.subs.forEach(sub => sub.update(newValue))
}
}
复制代码
- 处理观察者和被观察者
// 对被观察者使用
function observe(obj) {
let keys = Object.keys(obj)
let observer = {}
keys.forEach(key => {
let dep = new Dep()
Object.defineProperty(observer, key, {
configurable: true,
enumerable: true,
get: function () {
if (Dep.Target) dep.addSub(Dep.Target)
return obj[key]
},
set: function (newValue) {
dep.notify(newValue)
obj[key] = newValue
}
})
})
return observer
}
// 对观察者使用
function watching(obj, key) {
let cb = newValue => {
obj[key] = newValue
}
Dep.Target = new Watcher(cb)
return obj
}
复制代码
- 检验代码
let subscriber = watching({}, 'a')
let observed = observe({ a: '1' })
subscriber.a = observed.a
console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
observed.a = 2
console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
复制代码
- 结果:
subscriber.a: 1, observed.a: 1
subscriber.a: 2, observed.a: 2
复制代码
CodePen演示
参考
深入理解Vue响应式原理 vue.js源码 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定 50行代码的MVVM,感受闭包的艺术 Vue.js 技术揭秘