概述
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.aconsole.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)observed.a = 2console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)复制代码
- 结果:
subscriber.a: 1, observed.a: 1subscriber.a: 2, observed.a: 2复制代码
参考