Vue做为一种MVVM框架,可以实现数据的双向绑定,让Vue技术栈的前端从业者摆脱了繁琐的DOM操做,这彻底得益于Vue框架开发者对原生Object对象的深度应用。Vue实现数据响应系统的核心技术就是数据劫持和订阅-发布,基本思想就是经过对数据操做进行截获,在数据进行getter操做的时候更新依赖,即依赖收集过程(更新订阅对象集);在数据进行setter时通知全部依赖的组件一并进行更新操做(通知订阅对象)。Vue数据响应系统原理示意图以下:javascript
接下来对Vue数据响应系统的这两个核心技术进行探究:html
Vue数据劫持主要利用Object.defineProperty来实现对对象属性的拦截操做,拦截工做主要就Object.defineProperty中的getter和setter作文章,看一栗子:前端
var person = { name: '小明', } var name = person.name; Object.defineProperty(person, 'name', { get: function() { console.log('name getter方法被调用'); return name; }, set: function(val) { console.log('name setter方法被调用'); name = '李' + val; } }) console.log(person.name); person.name = '小亮'; console.log(person.name); // 结果 name getter方法被调用 小明 name setter方法被调用 name getter方法被调用
能够看到,设置了属性的存取器后,在设置和获取person.name的时候,自定义的getter和setter将会被调用,这就给在这两个方法调用时进行其它内部的自定义操做提供了可能。了解了Object.defineProperty的基本使用后,咱们开始从Vue源码角度开始逐步了解其具体的应用,在Vue的_init初始化过程当中(传送门)有一个initState操做,该操做主要依次对options中定义的props、methods、data、computed、watch属性进行初始化(主要分析对data的初始化initData),该过程就主要是实现了将options.data变得可观察:
src/core/instance/state.jsjava
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} ... // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] ... else if (!isReserved(key)) { // 设置data的数据代理 proxy(vm, `_data`, key) } } // observe data // 将data数据属性变为可观察 observe(data, true /* asRootData */) }
继续看对observe的定义:react
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 若是已经处于观察中,则返回观察对象,不然将value变为可观察 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 } src/core/observer/index.js // Observer 监听器类 class Observer { constructor (value: any) { this.value = value this.dep = new Dep() // 订阅器 this.vmCount = 0 def(value, '__ob__', this) // 为value添加__ob__属性 if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } // 处理数组,数组中的每一个对象都会进行可观察化操做 this.observeArray(value) } else { // 处理对象,主要遍历对象中全部属性,并将全部属性变得可观察 this.walk(value) } } } /** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 定义一个订阅器 const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 判断是否有自定义的getter, 没有则直接取值 const value = getter ? getter.call(obj) : val // 判断是经过watcher进行依赖搜集调用仍是直接数据调用方式(好比在template目标中直接使用该值,此时 // Dep.target为undefined,该值为具备全局性,指向当前的执行的watcher) if (Dep.target) { dep.depend() // 将属性自身放入依赖列表中 // 若是子属性为对象,则对子属性进行依赖收集 if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { // 处理数组的依赖收集 dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 判断是否有自定义的getter, 没有则直接设置值,不然经过getter取值 const value = getter ? getter.call(obj) : val ... if (setter) { setter.call(obj, newVal) } else { val = newVal } // 从新计算该对象的数据是否须要进行观察 childOb = !shallow && observe(newVal) // 设置数据后通知全部的watcher订阅者 dep.notify() } }) }
经过上面步骤将options中data的的属性变得可观察,defineReactive方法中的闭包方法set比较好理解,就如上面所说,设置新值newVal,并判断该值是不是为非基本数据类型,如若不是,可能就须要重新将newVal变得可观察;而后通知订阅器中全部的订阅者,进行视图更新等操做;get中的Dep.target不太好理解,我也是研究了一两天才明白,这里先不说,反正就是知足Dep.target不为undefined,则进行依赖收集,不然就是普通的数据获取操做,返回数据便可。git
假使你们都晓得订阅-发布是个什么状况(不太清除可自行百度,不在此占据篇幅了),那么,咱们要知道,谁是订阅者,订阅的目标是什么?有了这个疑问,咱们接下来看看相关的数据结构定义github
// src/core/observer/dep.js export default class Dep { static target: ?Watcher; //target全局 id: number; subs: Array<Watcher>; 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() 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) } // 循环对订阅者进行更新操做(调用watcher的update方法) for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { // 将当前的watcher推入堆栈中,关于为何要推入堆栈,主要是要处理模板或render函数中嵌套了多层组件,须要递归处理 targetStack.push(target) // 设置当前watcher到全局的Dep.target,经过在此处设置,key使得在进行get的时候对当前的订阅者进行依赖收集 Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] } // src/core/observer/watcher.js 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)) { // 将订阅的watcher添加到当前的发布者watcher中 dep.addSub(this) } } }
以前在getter的依赖收集过程当中,当Dep.target成立时,会执行dep.depend()
方法,能够看到,Dep中定义的depend()方法会调用Dep.target
的addDep()
方法,这个过程传递的参数就是在defineReactive中定义的dep对象,那么Dep.target究竟是什么东西呢,这个能够看Watcher对应get方法中的定义:express
// Watcher -> get() get () { pushTarget(this) let value const vm = this.vm try { // 调用getter方法 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 } // Dep-> pushTarget() export function pushTarget (target: ?Watcher) { // 将当前的watcher推入堆栈中,关于为何要推入堆栈,主要是要处理模板或render函数中嵌套了多层组件,须要递归处理 targetStack.push(target) // 设置当前watcher到全局的Dep.target,经过在此处设置,key使得在进行get的时候对当前的订阅者进行依赖收集 Dep.target = target }
由于js是单线程执行,同一时刻只能执行一个Watcher,执行当前Watcher实例时候,Dep.target指向当前Watcher订阅者,当在执行下一个Watcher订阅者的get方法时候,即指向下一个订阅者,即Dep.target永远指向的是当前的Watcher订阅者,而后将当前订阅者添加到dep对应的subs订阅列表中,同时订阅者内部也须要记录有哪些订阅目标(dep),便于进行依赖收集过程的更新操做。用一张图来表述:数组
在数据劫持的闭包方法setter代码中,经过调用dep.notify()
进行数据更新通知,这个阶段的主要工做就是将当前订阅目标dep更新消息通知到订阅列表中的订阅者(Watcher),而后订阅者利用注册的回调方法进行视图渲染等操做。数据结构
// src/core/observer/dep.js notify () { ... // 循环对订阅者进行更新操做(调用Watcher的update方法) for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } // src/core/observer/watcher.js update () { /* istanbul ignore else */ if (this.lazy) { // 是否为懒加载(这个什么时候执行?) this.dirty = true } else if (this.sync) { // 是否为同步方式更新 this.run() } else { // 加入到订阅者更新队列(最终也要执行run方法) queueWatcher(this) } }
$emsp;在run()中主要调用Watcher -> get()
,在这说明一下get()
中this.getter.call(vm, vm)
的getter来源主要有两种-函数或表达式,函数好比render function、computed中的getter(), 表达式相似于"person.name"这种类型,通过parsePath
转换成为getter()方法。传送门中对Watcher的种类进行了总结。
以上就是我对Vue数据响应式系统的一些学习,因为工做缘由,先后花了一周左右的时间才写完,初步涉猎,估计仍是有很多理解上的不到位,但愿有幸能被你们看到并指出,这一过程当中参考了很多的前人好文,太多就罗列一二,但愿对你们也有帮助:
另外,欢迎去本人git 相互学习和star,不胜感激。