(距离上一次写文章已通过去了四个月,羞愧...)这几个月对vue的使用很多,可是自觉地始终停留在比较粗浅的层面,一直没法提高,因此尝试着开始阅读源码。 文中内容仅表明我的理解,若是错误,欢迎指正。html
Vue中一个显著特性是数据响应式系统:当数据被修改时,视图会相应更新。从而方便的完成状态管理。官方文档中对此进行了简要的描述,本文将结合vuejs的源码,作出进一步的解析。vue
首先简单介绍一些在响应式系统中重要的概念。react
vue实例中的数据项git
数据属性的观察者,监控对象的读写操做。github
(dependence的缩写),字面意思是“依赖”,扮演角色是消息订阅器,拥有收集订阅者、发布更新的功能。express
消息订阅者,能够订阅dep,以后接受dep发布的更新并执行对应视图或者表达式的更新。npm
dep
和watcher
的关系,能够理解为:dep
是报纸,watcher
是订阅了报纸的人,若是他们创建了订阅 的关系,那么每当报纸有更新的时候,就会通知对应的订阅者们。api
暂且认为就是在浏览器中显示的dom(关于virtual dom的内容暂时不在本文讨论)数组
watcher在自身属性中添加dep的行为,后面会详细介绍浏览器
dep在自身属性中添加watcher的行为,后面会详细介绍
首先给出官方文档的流程图
在此基础上,咱们根据源码更细一步划分出watcher和data之间的部分,即Dep
和observer
。
总的来讲,vue的数据响应式实现主要分红2个部分:
第一部分是上图中data
、observer
、dep
之间联系的创建过程,第二部分是watcher
、dep
的关系创建
本文中采用的源码是vuejs 2.5.0,Git地址
PS:简单的代码直接添加中文注释,因此在关键代码部分作<数字>
标记,在后文详细介绍
首先咱们在源码中找到vue进行数据处理的方法initData
:
/* 源码目录 src/core/instance/state.js */ function initData (vm: Component) { let data = vm.$options.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 ) } // 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] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { //<1>data属性代理 proxy(vm, `_data`, key) } } // observe data //对data调用observe observe(data, true /* asRootData */) }
这一段代码主要作2件事:
代码<1>
在while
循环内调用proxy
函数把data的属性代理到vue实例上。完成以后能够经过vm.key
直接访问data.key
。以后对data
调用了observe
方法,在这里说明一下,若是是在实例化以前添加的数据,由于被observe
过,因此会变成响应式数据,而在实例化以后使用vm.newKey = newVal
这样设置新属性,是不会自动响应的。解决方法是:
- 若是你知道你会在晚些时候须要一个属性,可是一开始它为空或不存在,那么你仅须要设置一些初始值 - 使用`vm.$data`等一些api进行数据操做
接下来来看对应代码:
/* 源码目录 src/core/observer/index.js */ export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void //检测当前数据是否被observe过,若是是则没必要重复绑定 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( //<1>检测当前的数据是不是对象或者数组,若是是,则生成对应的Observer observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
代码<1>
处,对传入的数据对象进行了判断,只对对象和数组类型生成Observer
实例,而后看Observer
这个类的代码,export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value // 生成了一个消息订阅器dep实例 关于dep的结构稍后详细介绍 this.dep = new Dep() this.vmCount = 0 //def函数给当前数据添加不可枚举的__ob__属性,表示该数据已经被observe过 def(value, '__ob__', this) //<1>对数组类型的数据 调用observeArray方法;对对象类型的数据,调用walk方法 if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ /* 循环遍历数据对象的每一个属性,调用defineReactive方法 只对Object类型数据有效 */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } /** * Observe a list of Array items. */ /* observe数组类型数据的每一个值, */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } /* defineReactive的核心思想改写数据的getter和setter */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { //<2>生成一个dep实例,注意此处的dep和前文Observer类里直接添加的dep的区别 const dep = new Dep() //检验该属性是否容许从新定义setter和getter const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters // 获取原有的 getter/setters const getter = property && property.get const setter = property && property.set //<3>此处对val进行了observe let childOb = !shallow && observe(val) //<4>下面的代码利用Object.defineProperty函数把数据转化成getter和setter,而且在getter和setter时,进行了一些操做 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // dep.depend()其实就是dep和watcher进行了互相绑定,而Dep.target表示须要绑定的那个watcher,任什么时候刻都最多只有一个,后面还会解释 dep.depend() if (childOb) { //<5>当前对象的子对象的依赖也要被收集 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 } //<6>观察新的val并通知订阅者们属性有更新 childOb = !shallow && observe(newVal) dep.notify() } }) }
_![图片描述][2]ob_
属性上,而后把_ob_
挂在该数据上,它是该数据项被observe
的标志,咱们能够在控制台看到这个属性,,例如://例子 1 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>vue demo</title> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <div id="app"> <div>obj:{{ obj}}</div> </div> </body> <script> app = new Vue({ el: '#app', data: { str: "a", obj: { key: "val" } } }); console.log(app._data) </script> </html>
在控制台咱们能够看到这样的数据
能够看到,首先,data对象上已经有_ob_
属性,这是被observe
的标志;其次,obj
和arr
属性上有_ob_
属性,而str
没有,这个进一步证实了前文提到的:observe
只对对象和数组有效
observe
里面的每一个值,对于对象,咱们执行walk()
方法,而walk()
就是对于当前数据对象的每一个key,执行defineReactive()
方法,因此接下来重点来看defineReactive()
。defineReactive()
中,在代码<2>
处生成了一个dep
实例,并在接下来的代码里,把这个dep
对象放在当前数据对象的key
(好比上面例子1中的str
)的getter
里,这个以前Observer
中的dep
是有区别的:
Observer
中的dep
挂在Object
或者Array
类型的数据的dep
属性上,能够在控制台直接查看;dep
挂在属性的getter/setter上
,存在于函数闭包中,不可直接查看为何会有2种`Dep`呢?由于对于`Object`或者`Array`类型的数据,可能会有**添加
或者删除成员的操做而其余类型的值只有赋值操做,赋值操做能够在getter/setter上
中检测到。**,
代码<3>
处的是为了处理嵌套的数据对象,好比例子1中,data
是最顶层的Object
,obj
就是data
下的Object
,而obj
里面也能够再继续嵌套数据,有了此处的代码以后,就能够对嵌套的每一层都作observe
处理。代码<4>
处是defineReactive()
的核心:利用Object.defineProperty()
(这个函数建议了解一下mdn地址)在当前属性的getter和setter中插入操做:
watcher
(也就是Dap.target
)和dep
之间的绑定,这里有个注意点是在代码<5>
处,若是当前数据对象存在子对象,那么子对象的dep
也要和当前watcher
进行绑定,以此类推。val
,而后经过dep.notify()
来通知当前dep所绑定的订阅者们数据有更新。接下来介绍一下dep
。源码以下:
/* 源码目录 src/core/observer/dep.js */ let uid = 0 /** * A dep is an observable that can have multiple * directives subscribing to it. */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } //添加一个watcher addSub (sub: Watcher) { this.subs.push(sub) } //移除一个watcher removeSub (sub: Watcher) { remove(this.subs, sub) } //让当前watcher收集依赖 同时Dep.target.addDep也会触发当前dep收集watcher depend () { if (Dep.target) { Dep.target.addDep(this) } } //通知watcher们对应的数据有更新 notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
这个类相对简单不少,只有2个属性:第一个是id
,在每一个vue实例中都从0开始计数;另外一个是subs
数组,用于存放wacther
,根绝前文咱们知道,一个数据对应一个Dep
,因此subs
里存放的也就是依赖该数据须要绑定的wacther
。
这里有个Dep.target
属性是全局共享的,表示当前在收集依赖的那个Watcher,在每一个时刻最多只会有一个。
接下里看watcher的源码,比较长,可是咱们只关注其中的几个属性和方法:
/* 源码目录 src/core/observer/watcher.js */ /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ /* watcher用来解析表达式,收集依赖,而且当表达式的值改变时触发回调函数 用在$watch() api 和指令中 */ 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: ISet; newDepIds: ISet; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm vm._watchers.push(this) // options //这里暂时不用关注 if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } 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 //deps和newDeps表示现有的依赖和新一轮收集的依赖 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 //<1>解析getter的表达式 if (typeof expOrFn === 'function') { this.getter = expOrFn } else { //<2>获取实际对象的值 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} 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.lazy为true是计算属性的watcher,另外处理,其余状况调用get this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. */ get () { pushTarget(this) let value const vm = this.vm try { 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() //<3>清除先前的依赖 this.cleanupDeps() } return value } /** * 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)) { dep.addSub(this) } } } /** * Clean up for dependency collection. */ /* 清除旧依赖 */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } /** * 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) } } /** * Scheduler job interface. * Will be called by the scheduler. */ /* 调度接口 调度时会触发 */ run () { if (this.active) { //<14>从新收集依赖 const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false } /** * Depend on all deps collected by this watcher. */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } }
首先看官方文档的英文注释可知,watcher用于watcher用来解析表达式,收集依赖,而且当表达式的值改变时触发回调函数,用在$watch()
api 和指令之中。
watcher函数主要内容是:
deps
、newDeps
、depIds
、newDepIds
,分别表示现有依赖和新一轮收集的依赖,这里的依赖就是前文介绍的数据对应的dep
。<1>
判断传入的表达式类型:多是函数,也多是表达式。若是是函数,那么直接设置成getter,若是是表达式,因为代码<2>
处的expOrFn
只是字符串,好比例子1中的obj.key
,在这里仅仅是一个字符串,因此要用parsePath
获取到实际的值代码<3>
`cleanupDeps`清除旧的依赖。这是必需要作的,由于数据更新以后可能有新的数据属性添加进来,前一轮的依赖中没有包含这个新数据,因此要从新收集。get()
首先从新收集依赖,而后使用this.cb.call
更新模板或者表达式的值。在最后,咱们再总结一下这个流程:首先数据从初始化data开始,使用observe
监控数据:给每一个数据属性添加dep
,而且在它的getter过程添加收集依赖操做,在setter过程添加通知更新的操做;在解析指令或者给vue实例设置watch
选项或者调用$watch
时,生成对应的watcher
并收集依赖。以后,若是数据触发更新,会通知watcher
,wacther
在从新收集依赖以后,触发模板视图更新。这就完成了数据响应式的流程。
本文的流程图根据源码的过程画出,而在官方文档的流程图中,没有单独列出dep
和obvserver
,由于这个流程最核心的思路就是将data的属性转化成getter
和setter
而后和watcher
绑定。
而后依然是惯例:若是这篇文章对你有帮助,但愿能够收藏和推荐,以上内容属于我的看法,若是有不一样意见,欢迎指出和探讨。请尊重做者的版权,转载请注明出处,如做商用,请与做者联系,感谢!