VueJS 响应式原理及简单实现
栏目: JavaScript · 发布时间: 6年前
内容简介:Vue 的响应式模型指的是:上面的三种方式追根揭底,都是通过回调的方式去更新视图或者通知观察者更新数据Vue的响应式原理是基于观察者模式和JS的API:
Vue 的响应式模型指的是:
vm.$watch() 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<Watcher>; } 复制代码
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<Dep>; newDeps: Array<Dep>; 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)
,它的作用是保留旧值,用以对比新值,然后确定是否需要调用回调
总结:
__ob__
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<any> | 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<any> | 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 复制代码
参考
深入理解Vue响应式原理 vue.js源码 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定 50行代码的MVVM,感受闭包的艺术 Vue.js 技术揭秘
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Little Schemer
[美] Daniel P. Friedman、[美] Matthias Felleisen / 卢俊祥 / 电子工业出版社 / 2017-7 / 65.00
《The Little Schemer:递归与函数式的奥妙》是一本久负盛名的经典之作,两位作者Daniel P. Friedman、Matthias Felleisen在程序语言界名声显赫。《The Little Schemer:递归与函数式的奥妙》介绍了Scheme的基本结构及其应用、Scheme的五法十诫、Continuation-Passing-Style、Partial Function、......一起来看看 《The Little Schemer》 这本书的介绍吧!
CSS 压缩/解压工具
在线压缩/解压 CSS 代码
SHA 加密
SHA 加密工具