首先介绍后面要用到的几个类:
Observer类:用于将一个数据变为响应式(可被观测)
Watcher类:一个依赖(一个指令对应一个依赖),一旦有一个指令用到了某个对象属性,那么就会新建一个Watcher作为订阅者。
Dep类:依赖管理器,一个对象属性对应一个Dep,其有一个内部属性subs
用于存放依赖。
Vue初始化 Vue原型是在src/instance/idnex.js
中定义的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options ) { if (process.env .NODE_ENV !== 'production' && !(this instanceof Vue ) ) { warn ('Vue is a constructor and should be called with the `new` keyword' ) } this ._init (options) } initMixin (Vue )stateMixin (Vue )eventsMixin (Vue )lifecycleMixin (Vue )renderMixin (Vue )export default Vue
可以看到Vue原型是一个function。在这个函数只判断了是否是new出来的,否则报警告。然后直接调用了_init()方法,这个方法是在下面的initMixin(Vue)
中混入的初始化方法。下面看一下这个方法中的重要部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 initLifecycle (vm) initEvents (vm) initRender (vm) callHook (vm, 'beforeCreate' ) initInjections (vm) initState (vm) initProvide (vm) callHook (vm, 'created' ) if (process.env .NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName (vm, false ) mark (endTag) measure (`vue ${vm._name} init` , startTag, endTag) } if (vm.$options .el ) { vm.$mount(vm.$options .el ) }
这里按顺序进行了一下操作(最简化,不考虑分支):
首先初始化了生命周期 initLifecycle(vm)
初始化了事件($on,$emit,$once…) initEvents(vm)
初始化了render initRender(vm)
调用了beforeCreate
生命周期 callHook(vm, 'beforeCreate')
是初始化inject initInjections(vm)
接下来的initState中,初始化了data,props,methods。 initState(vm)
初始化provide initProvide(vm)
调用生命周期created, callHook(vm, 'created')
,如同官网所说:
在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el
属性目前尚不可用。
最后一步将el挂载到页面。vm.$mount(vm.$options.el)
响应式原理(以对象为例) 响应式的所有文件都放在observer文件夹下:
observer
array.js:数组的处理相关处理
dep.js:依赖管理器类的定义及其处理
index.js:整个observer的出口
scheduler.js:调度者相关文件
traverse.js:递归遍历一个对象,以唤醒所有转换getter,使每个嵌套的属性内的对象作为“深度”依赖项收集。
watcher.js:观测者,依赖的类定义与相关处理。
还是接着上面的第6步:initState
initState方法定义在state.js中,下面节选这一部分中内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export function proxy ( ){ } export function initState (vm: Component ) { vm._watchers = [] const opts = vm.$options if (opts.props ) initProps (vm, opts.props ) if (opts.methods ) initMethods (vm, opts.methods ) if (opts.data ) { initData (vm) } else { observe (vm._data = {}, true ) } if (opts.computed ) initComputed (vm, opts.computed ) if (opts.watch && opts.watch !== nativeWatch) { initWatch (vm, opts.watch ) } }
可以看到intiState中,Vue进行了下面主要操作:
initProps(vm, opts.props)
initMethods(vm, opts.methods)
initData(vm)
initWatch(vm, opts.watch)
然后以data
为例,分析对data的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 ) } 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)) { proxy (vm, `_data` , key) } } observe (data, true ) }
在这个函数里
首先看data是一个对象还是一个函数,对其进行对应的处理
然后判断他不能与props
,methods
中的属性同名,因为最终这三部分都会被挂载到vm实例上。
最后调用observe()
方法来使data变为可被观测的
然后我们看observe()
方法,这个方法就位于observer下的index.js中了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export function observe (value: any, asRootData: ?boolean ): Observer | void { if (!isObject (value) || value instanceof VNode ) { return } let ob : Observer | void 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 }
作为整个将数据变为响应式的入口函数,它进行了一下操作:
判断传入的val如果不是一个对象或者是一个Vnode,就直接返回,不做处理
判断整个val是否有__ob__
整个属性或者是不是Observer的子类,如果是的话,直接将val.__ob__
返回
进行了一系列的其他的判断,比如是否应该被观测(shouldObserve
这个对象定义在全局中,判
标识此时是否应该处理数据)、是否处于服务端渲染模式、是一个数组或是一个对象、是不是可扩展的、是不是Vue本身。
新建一个Observer对象,并将value传入。
继续向下看,新建Observer对象过程,Observer对象也在index中定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 export class Observer { value : any; dep : Dep ; vmCount : number; constructor (value: any ) { this .value = value this .dep = new Dep () this .vmCount = 0 def (value, '__ob__' , this ) if (Array .isArray (value)) { if (hasProto) { protoAugment (value, arrayMethods) } else { copyAugment (value, arrayMethods, arrayKeys) } this .observeArray (value) } else { this .walk (value) } } walk (obj : Object ) { const keys = Object .keys (obj) for (let i = 0 ; i < keys.length ; i++) { defineReactive (obj, keys[i]) } } observeArray (items : Array <any>) { for (let i = 0 , l = items.length ; i < l; i++) { observe (items[i]) } } }
可以看到Observer对象包含了三个私有属性:
value:当前观测对象
dep:依赖管理器
vmCount:将这个对象作为$data的数量
再看他的构造方法中,执行流程如下:
初始化了value,dep,vmCount
并给value的加上一个(不可枚举的)__ob__
属性,可以联系上面判断__ob__
的操作
判断value是否是一个数组,然后执行对数组的observer操作。现不看数组。
如果不是,则代表value是一个对象,则执行walk()
方法对其进行处理。
在walk()
中,可以看到Vue遍历了对象的所有属性并对其调用了defineReactive
方法
我们再跟进defineReactive
方法中(这个方法就差不多是核心了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 export function defineReactive ( obj: Object , key: string, val: any, customSetter?: ?Function , shallow?: boolean ) { const dep = new Dep () const property = Object .getOwnPropertyDescriptor (obj, key) if (property && property.configurable === false ) { return } const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments .length === 2 ) { val = obj[key] } 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 if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env .NODE_ENV !== 'production' && customSetter) { customSetter () } if (getter && !setter) return if (setter) { setter.call (obj, newVal) } else { val = newVal } childOb = !shallow && observe (newVal) dep.notify () } }) }
还是分析一下他的执行流程:
首先建立了一个Dep对象,即每一个属性都有一个依赖管理器用来收集用到这个属性的依赖,这一部分后面在讲。
判断这个对象属性能否被修改,及判断其configurable
属性。
判断这个对下给你属性是否自带了getter
与setter
,如果有的话就将其缓存下来。
判断是否在函数调用时为这个对下给你属性传入了customSetter
,如果有的话,则先调用getter将其值存储下来。
判断有无子对象或者是否在函数调用时确定只观测表层属性(不循环处理),如果都不满足则递归进行子属性的响应式处理。
这里就使整个响应式的核心:Object.defineProperty
,在这里Vue为其定义了enumerable
,configurable
,get
,set
get():
获取原始的属性值(通过原始getting或者直接获取)
判断Dep.target的值是否存在(这个值后面介绍Dep对象时介绍,代表的是当前的依赖),如果存在的话,就调用dep对象的depend()
方法进行依赖收集。
接下来判断是否有子ob对下给你,如果有的话,也调用子的dep的depend方法进行依赖收集。同时判断对象属性原来的值是否是个数组,如果是的话,调用dependArray
方法进行数组的依赖收集。
set()
获取对象属性原本的值,调用原来的getter,如果没有,就接受传入的值。
判断有没有必要更新。
判断是否有customSetter
,如果有的话,就调用
如果只有getter,没有setter,则直接返回。这里是为了修复#7981的BUG,问题大概是如果一个对象属性如果被其他插件修改后只有getter,但没有setter,也就是说整个插件的原意是将其变为一个不可写入的属性,但是如果不加这一句进行判断,那么Vue会直接调用val = newVal
,对其进行赋值。这不符合预期,所以加了这一句判断,直接返回不进行处理。
判断如果原来有setter的话,就调用其setter。否则就直接赋值给val。
然后同样是对子属性的处理。
这一步进行依赖派发。
至此,defineReactive
方法流程介绍完毕。但是我们还留下了两个坑
依赖收集dep.depend()
具体如何完成的
依赖通知dep.notify()
具体如何完成的
接下来我们在进入这两个函数进行分析。
首先我们看一下Dep对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import type Watcher from './watcher' import { remove } from '../util/index' import config from '../config' let uid = 0 export default class Dep { static target : ?Watcher ; id : number; subs : Array <Watcher >; constructor ( ) { this .id = uid++ this .subs = [] } addSub (sub : Watcher ) { this .subs .push (sub) } removeSub (sub : Watcher ) { remove (this .subs , sub) } depend () { if (Dep .target ) { Dep .target .addDep (this ) } } notify () { const subs = this .subs .slice () if (process.env .NODE_ENV !== 'production' && !config.async ) { subs.sort ((a, b ) => a.id - b.id ) } for (let i = 0 , l = subs.length ; i < l; i++) { subs[i].update () } } } Dep .target = null const targetStack = []export function pushTarget (target: ?Watcher ) { targetStack.push (target) Dep .target = target } export function popTarget ( ) { targetStack.pop () Dep .target = targetStack[targetStack.length - 1 ] }
可以看到Dep对象有三个属性,
target:一个静态属性,类型是Watcher
,即当前执行的依赖
id:Dep的标识符
subs:整个dep中所有的依赖,是一个Watcherd数组
然后我们直接看depend()
方法:
这个函数判断dep的target是否存在,如果存在的话,则调用当前依赖的addDep方法,我们知道这个target是一个Watcher。所以我们看一下Watcher:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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; } 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 ) } } }
可以看到在addDep中,判断这个Watcher
对应的新dep中是否含有这个传进来的Dep,如果没有就其push到新dep与新depIds中,然后判断原来的dep中是否含有这个watcher,如果没有,就push进来。
这里的newDep与dep是为了灵活的动态更新视图,思考以下场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <div> <span v-if="nameShow">{{name}}</span> <span v-if="ageShow">{{age}}</span> <input :value="name"/> <input :value="age"/> <button @click="nameShow = !nameShow">切换name状态</button> <button @click="ageShow = !ageShow">age</button> </div> </template> <script> export default { name:"Register", data () { return { nameShow: true, ageShow: true, name:"123", age:"111" } } } </script>
当nameShow与ageShow都是true时,我们对表单机进行修改以修改name与age的值时,肯定会涉及到到依赖的分发。
但是当我们点击button将nameShow或ageShow的值切换为false时,视图上已经不显示对应信息,则讲道理应该不会在对这个依赖进行通知。这个newDep的存在就是为了这里。
至此,依赖收集的过程基本完成。下面看看如何进行依赖派发的:
当一个对象属性被改变时,其set方法就会被调用,由此调用dep.notify()
,进行依赖派发。我们看一下dep.notify()
这个函数内部:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 notify () { const subs = this .subs .slice () if (process.env .NODE_ENV !== 'production' && !config.async ) { subs.sort ((a, b ) => a.id - b.id ) } for (let i = 0 , l = subs.length ; i < l; i++) { subs[i].update () } } }
可以看到首先是对subs
所有的依赖进行了排序,根据官方的注释是应为:如果subs不是异步运行的话,那么他们没有在调度者中进行排序,所以我们需要对其进行排序以保证其正确按序派发。
然后这里调用每一个watcher的update方法,进行DOM的更新。看一下update()
方法
1 2 3 4 5 6 7 8 9 10 update () { if (this .lazy ) { this .dirty = true } else if (this .sync ) { this .run () } else { queueWatcher (this ) } }
可以看到首先判断了这个watcher是不是懒加载的,如果是的话,将其dirty属性变为true,Vue会在调用到它时进行加载,否则看他是不是同步的,如果是的话,立即调用run()
进行DOM更新操作,否则就将其推入到queueWatcher
队列中,等待调度者,进行调度。(这里就不再讲调度算法,后续再讲)
再进入run()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 run () { if (this .active ) { const value = this .get () if ( value !== this .value || isObject (value) || this .deep ) { 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) } } } }
由官方的注释也可以看出来,这个run()
是由调度者进行执行的(除非他是一个同步的watcher)
run()
函数的运行流程如下:
判断这个wacher是否的活动的,如果是才操作。
通过get()
获取这个watcher对应的值,判断获取到的值与watcher中保存的值是否相同
如果不等,则把当前watcher中保存的值作为oldValue保存下来,将当前watcher中的value设为获取到的value,然后判断这个watcher是不是用户定义的(this.user)(根据调度者中注释,watcher分为user watcher与render watcher),如果使用定义的watcher则用try--catch
预防错误,否则直接调用这个watcher的回调函数。这个回调函数就会进行真正操作,比如调用rrnder更新DOM。
数组的响应式如何实现 在上面我们介绍Dep
对象时,在其构造方法中,我们只看了this.walk()
对对象的操作,现在我们看一下对数组的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export class Observer { value : any; dep : Dep ; vmCount : number; constructor (value: any ) { this .value = value this .dep = new Dep () this .vmCount = 0 def (value, '__ob__' , this ) if (Array .isArray (value)) { if (hasProto) { protoAugment (value, arrayMethods) } else { copyAugment (value, arrayMethods, arrayKeys) } this .observeArray (value) } else { this .walk (value) } }
如果value是一个对象,那么会判断value有没有__ptoto__
对象,因为部分浏览器不支持这个属性,如果有的话,则调用protoAugment(value, arrayMethods)
把arrayMethods
挂载到value的__proto__
上,我们再看一下arrayMethods
,它放在
observer/array.js中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import { def } from '../util/index' const arrayProto = Array .prototype export const arrayMethods = Object .create (arrayProto)const methodsToPatch = [ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'sort' , 'reverse' ] methodsToPatch.forEach (function (method ) { const original = arrayProto[method] def (arrayMethods, method, function mutator (...args ) { const result = original.apply (this , args) const ob = this .__ob__ let inserted switch (method) { case 'push' : case 'unshift' : inserted = args break case 'splice' : inserted = args.slice (2 ) break } if (inserted) ob.observeArray (inserted) ob.dep .notify () return result }) })
可以看到
首先是以原生Array的prototype作为原型创建了一个新的对象arrayMethods
。
列举出需要被修改的数组方法methodsToPatch
对methodsToPatch
进行forEach循环,并给arrayMethods定义每一个列举出的方法(不可枚举),如果这些方法中要为这个数组插值,我们必须也要探测这个值是否是一个引用类型(Araay或者Object),并也要将其变为响应式,所以后面判断了如果是push
,unshift
,splice
则拿到要插入的值inserted
判断inserted
是否存在,如果存在,也使用observeArray
将其变为响应式。
进行依赖收集ob.dep.notify()
返回原始方法调用后返回的结果。
然后我们再看protoAugment
方法:
1 2 3 4 5 function protoAugment (target, src: Object ) { target.__proto__ = src }
可以看到很简单,就只是把第一个参数的__proto__
修改为第二个参数,结合我们刚刚传入的参数,即:将这个数组的_proto_
修改为arrayMethods
,即上面我们分析的这个对象。
再看copyAugment
方法,这个方法也很简单,是针对不支持__proto__
属性的浏览器:
1 2 3 4 5 6 7 8 9 10 11 function copyAugment (target: Object , src: Object , keys: Array <string> ) { for (let i = 0 , l = keys.length ; i < l; i++) { const key = keys[i] def (target, key, src[key]) } }
即遍历所有的方法名字,并将其设置为到目标数组上的不可枚举属性。
最后我们看一下observeArray
方法:
1 2 3 4 5 6 7 8 observeArray (items : Array <any>) { for (let i = 0 , l = items.length ; i < l; i++) { observe (items[i]) } }
这里实际上就是将数组的每一个值变为响应式。
但是还有一点是:我们还可以通过下标的方式为数组赋值,但是JS中找不到方法检测整个操作,所以Vue也无法检测到,所以Vue提供了Vue.set和Vue.del这两个api,用来弥补这一点。
至此,响应式的整个流程就差不多完成了。
最后梳理以下整个流程(对象):
这个流程主要是我通过分析源码,借助一定的网上资料整理出来的,可能其中会有错误。希望大家指出来,谢谢。