前言 不知道写什么,所以没有前言 我们从入口开始讲起
从入口开始 1 2 3 4 5 6 7 8 9 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) }
可以看到,vue调用了一个_init
方法,并传入了options
,那我们转到_init方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export function initMixin (Vue: Class<Component> ) { Vue .prototype ._init = function (options?: Object ) { vm._self = vm; initLifecycle (vm); initEvents (vm); initRender (vm); callHook (vm, 'beforeCreate' ); initInjections (vm); initState (vm); initProvide (vm); callHook (vm, 'created' ); if (vm.$options .el ) { vm.$mount(vm.$options .el ) } } }
然后对数据进行处理的是initState
函数,那我们转到initState
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ) } }
我们可以从执行的函数名看出,这里完成了props,methods,data,computed,watch的初始化,我们来看看data的初始化,我们转到initData
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 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 ) }
我们可以看到,在经过前面的一堆处理和检测后,observe
函数被调用了,并且把data作为参数传了进去,从函数名我们也可以看出,这就是实现数据响应式的函数。 然后我们看看observe函数的实现
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 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; }
在经过了一系列判断后,如果这个对象没有被观测过,就会通过new Observer
的方式进行观测,那我们来看看Observe
做了什么。
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 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)) { const augment = hasProto ? protoAugment : copyAugment; augment (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]) } } } export function def (obj: Object , key: string, val: any, enumerable?: boolean ) { Object .defineProperty (obj, key, { value : val, enumerable : !!enumerable, writable : true , configurable : true }) }
在上面我已经把源码放了出来,可以看出,vue会把数组和对象作为两种情况讨论,所以下面我们把情况分为对对象和对数组来进行讨论。 不过我们要先花一点时间了解对一些流程做一个整体的了解。首先我们要知道,在vue里,一个组件对应一个渲染watcher
,这个watcher
上的update
方法执行时,就会重新渲染这个组件,那么这个组件怎么知道什么时候要重新渲染呢,其实是这样的,在这个组件渲染时,这个组件会把它对应的渲染watcher
推到Dep.target
上,而在访问渲染需要的数据时,会触发定义的getter
方法,这个时候,数据会从Dep.target
上拿到这个watcher
,然后把它保存到一个地方,这个地方就是dep
实例,dep.subs
会缓存对应的watcher
对象,在这个数据的值刷新时,定义的setter
方法就会被触发,setter
方法会调用dep.notify
,这个方法会遍历保存在dep
里的watcher
,调用watcher
的update
方法,对渲染watcher
来说,调用update
方法就相当于调用了重新渲染组件的方法。这就是数据响应式的一个大致流程,接下来我们来看看源码的实现。
对象的观测 第一层 假设我们有以下的代码
1 2 3 4 5 6 7 8 9 let vm = new Vue ({ el : "#app" , data ( ) { return { name : "sena" , age : "16" } } })
那么在执行的时候发生了什么呢,我们来看看,首先是调用了new Observer()
,然后调用了walk
方法,需要关注的代码就只有这些
1 2 3 4 constructor (value : any ) { this .value = value; this .walk (value) }
然后在walk方法里,遍历所有属性,用defineReactive
定义响应式
1 2 3 4 5 6 walk (obj: Object ) { const keys = Object .keys (obj); for (let i = 0 ; i < keys.length ; i++) { defineReactive (obj, keys[i]) } }
然后我们看看defineReactive
(PS:删除了一部分这种情况不会执行/不重要的代码,之后也会逐步根据不同情况添加代码)
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 export function defineReactive ( obj: Object , key: string, val: any ) { const dep = new Dep (); Object .defineProperty (obj, key, { enumerable : true , configurable : true , get : function reactiveGetter ( ) { const value = val; if (Dep .target ) { dep.depend (); } return value }, set : function reactiveSetter (newVal ) { const value = val; if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal; dep.notify () } }) }
通过执行defineReactive
,vue的数据得以和watcher关联起来,由此完成了响应式。 显然,如果只完成第一层的观测,代码是很简单的,但是正常情况下要观测的数据比这复杂的多,那我们来看看多层的实现吧
多层实现 实际上,多层实现也没有太复杂,依旧是先调用walk
方法,执行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 export function defineReactive( obj: Object, key: string, val: any ) { const dep = new Dep(); + // 新增代码1 : 递归调用进行观测 + let childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // getter收集依赖 // 获取属性对应的值 const value = val; // Dep.target会存放当前组件的渲染Watcher if (Dep.target) { // 这句话会把Dep.target添加到dep的中 // 也就是保存了watcher dep.depend(); } return value }, set: function reactiveSetter(newVal) { // setter派发更新 const value = val; // 判断新旧值是否相等, 相等就不触发派发更新 if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal; + // 新增代码2 : 如果新的值也是对象,也会进行观测 + childOb = observe(newVal); // 这里通知渲染watcher执行update方法,进行组件更新 dep.notify() } }) }
要完成观测多重嵌套,只要添加两行代码就可以了,我接下来会用一个组件,然后去源码中分别注释这两行,然后观察效果
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 <template > <div class ="home" > <p > name : {{name}}</p > <p > age : {{age}}</p > <p > school.name : {{school.name}}</p > <button @click ="click" > click me</button > </div > </template > <script > export default { name : 'home' , data ( ) { return { name : "sena" , age : "16" , school : { name : "school name" } } }, methods : { click ( ) { this .school .name = "another school name" ; console .log ("click" ); console .log (this ); } } } </script >
首先我们注释的是新增代码1,也就是let childOb = observe(val)
这一句,注释后组件可以正常渲染
然后我们点击一下按钮,可以看到,尽管school.name已经更新,但是组件没有刷新,这是因为school.name没有被观测
我们把注释去掉,然后点击按钮
这次school.name在改变的同时也触发了更新,我们也可以见到,school.name变成了一个访问器属性,这证明school.name已经通过defineReactive获得了收集依赖和派发更新的能力。 所以我们可以知道,这一行代码的用处是递归处理深层的数据,给深层数据定义响应式。
现在我们去源码里把第二行注释掉,也就是childOb = observe(newVal)
这一句,刷新页面后点击按钮,修改了school.name后依旧可以正常更新组件
那是不是意味着没有问题了呢,当然不是,我们改一下代码
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 <template > <div class ="home" > <p > {{school.name}}</p > <button @click ="school = { name : 'another school name' }" > button1</button > <button @click ="click" > button2</button > </div > </template > <script > export default { name : 'home' , data ( ) { return { name : "sena" , age : "16" , school : { name : "school name" } } }, methods : { click ( ) { this .school .name = "school name" ; console .log ("click" ); console .log (this ); }, } } </script >
页面刷新后分别点击button1和button2,可以看到这样的现象
诶,为什么点击button2时没有更新页面呢,其实很简单,我们直接修改了school的值,那么school就是一个全新的对象,这个新的对象自然是没有被观测过的,我们从图中可以看到school.name已经不是一个访问器属性了,也就是说修改它时setter不会被触发,也就不能触发组件更新的逻辑。顺带一提,这个时候school仍然是一个访问器属性,因为getter和setter是对应在对象的键上的。
然后我们把注释去掉,就能正常更新了,school.name也在school被重新赋值后被观测
数据的观测 第一层 在new Observer(value
)时,如果value
是一个数组,要处理的流程就和是对象的情况不一样了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 constructor(value: any) { this.value = value; + // 这个dep是数组专用的,具体使用后面会说 + this.dep = new Dep(); + // 定义__ob__, 这个属性会在之后用上 + // 这里把定义了访问器属性value.__ob__,值是this,也就是observer + def(value, '__ob__', this); + if (Array.isArray(value)) { + // 修改原型 + value.__proto__ = arrayMethods; + this.observeArray(value) } else { this.walk(value) } }
可以看到,如果要观测的对象是数组类型,会调用observeArray
方法,那我们来看看这个方法干了什么。
1 2 3 4 5 observeArray (items: Array <any> ) { for (let i = 0 , l = items.length ; i < l; i++) { observe (items[i]) } }
observeArray
遍历了数组的每一项,并且调用了observe
方法去观测他们,我们来看看observe
方法
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 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; }
可以看到,observe
只会对没有观测过的数据和对象类型的数据进行观测,所以observeArray
实际上是观测数组上的对象,并不观测下标,我们可以验证一下。
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 <template > <div class ="home" > <p v-for ="(game, index) in gameList" :key ="index" > {{game.gameName}}</p > <button @click ="click" > button</button > </div > </template > <script > export default { name : 'home' , data ( ) { return { name : "sena" , age : "16" , school : { name : "school name" }, gameList : [{ gameName : "GTA6" }, { gameName : "MONSTER HUNTER : WORLD" }] } }, methods : { click ( ) { console .log (this ); } }, mounted ( ) { window .vm = this ; }, beforeDestroy ( ) { window .vm = null ; } } </script >
很明显,数组里的[0], [1]都不是访问器属性的,但是数组里的对象被观测了,这就是为什么我们平时通过下标修改数组是不会触发更新的。
那么数组的变化侦测是怎么实现的呢,虽然你可能现在不清楚,但你一定知道我们平时想要在修改数组时触发更新,一般都是通过调用push,shift这样的函数,这是怎么实现的呢? 在new Observer时有这样几句话
1 2 3 4 5 if (Array .isArray (value)) {+ + value.__proto__ = arrayMethods; + this .observeArray (value) }
可以看到,数组的原型被修改成了arrayMethods
,那我们来看看arrayMethods
的实现
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 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 }) });
其实原理也很简单,vue对数组的方法做了拦截,如果调用这些会修改数组本身的方法,就会走到被拦截的方法里,然后在观测新添加的值后调用ob.dep.notify()
, 通知组件watcher
更新,顺带讲点细节问题,还记得Observr
的构造函数吗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 constructor (value: any ) { this .value = value; this .dep = new Dep (); def (value, '__ob__' , this ); if (Array .isArray (value)) { value.__proto__ = arrayMethods; this .observeArray (value) } else { this .walk (value) } }
this.dep
和value.__ob__
都是有目的的,前者是为了能在数组的拦截方法中访问到dep
,用于触发组件更新,后者是为了能在数组的拦截方法中访问到Observer
,当然也可以标识这个对象是否已经被观测。
除了上面的地方,我们还要知道,针对value
是数组的情况,vue也在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 export function defineReactive( obj: Object, key: string, val: any ) { const dep = new Dep(); // 递归调用进行观测 let childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // getter收集依赖 // 获取属性对应的值 const value = val; // Dep.target会存放当前组件的渲染Watcher if (Dep.target) { // 这句话会把Dep.target添加到dep的中 // 也就是保存了watcher dep.depend(); + if (childOb) { + childOb.dep.depend(); + } } return value }, set: function reactiveSetter(newVal) { // setter派发更新 const value = val; // 判断新旧值是否相等, 相等就不触发派发更新 if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal; // 果新的值也是对象,也会进行观测 childOb = observe(newVal); // 这里通知渲染watcher执行update方法,进行组件更新 dep.notify() } }) }
可以看到在get里加了一个调用childOb.dep.depend
的逻辑,那么为什么要这么做呢,我们可以在数组的拦截方法里调用dep.notify
,但是问题是,怎么把渲染watche
r添加到dep
中,也许你会说,在get中使用dep.depend()
,然而并不是这样的,在get
中调用的dep
是依赖这个属性的,在数组拦截的方法中能获得的dep,是observer.dep这个属性,那怎么办呢,vue的做法也很简单,首先递归调用observe
,把返回值保存在childOb
上,然后在get中添加上面的几行,childOb
如果存在,value
一定是数组或者对象类型,那么就会调用childOb.dep.depend
把渲染watcher
保存起来,childOb.dep
就是在数组拦截的函数中能获取到的dep
,这些方法也会用这个dep
触发渲染watcher
重新渲染组件。
我们来做个对比,如果,在源码中注释掉新增的这几行 然后就会变成这样 可以从图中看到,就算通过push方法修改了数组也没有触发组件更新,这就是因为observe.dep这个位置上没有保存渲染watcher
然后把注释去掉 这次在push后,组件成功刷新,原因就是我上面说的dep里保存了渲染watcher
你以为到这里就结束了?不不不,我们还有最后一个问题要解决,那就是数组套数组的问题
多层数组 我们修改下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template > <div class ="home" > <p > {{arr[0]}}</p > </div > </template > <script > export default { name : 'home' , data ( ) { return { arr : [[1 ,2 ,3 ],4 ] } }, mounted ( ) { window .vm = this ; }, beforeDestroy ( ) { window .vm = null ; } } </script >
然后我们测试一下 很明显,生产的数组没有保存渲染watcher,那要怎么做呢,我们还要对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 export function defineReactive( obj: Object, key: string, val: any ) { const dep = new Dep(); // 递归调用进行观测 let childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // getter收集依赖 // 获取属性对应的值 const value = val; // Dep.target会存放当前组件的渲染Watcher if (Dep.target) { // 这句话会把Dep.target添加到dep的中 // 也就是保存了watcher dep.depend(); if (childOb) { childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } } } return value }, set: function reactiveSetter(newVal) { // setter派发更新 const value = val; // 判断新旧值是否相等, 相等就不触发派发更新 if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal; // 果新的值也是对象,也会进行观测 childOb = observe(newVal); // 这里通知渲染watcher执行update方法,进行组件更新 dep.notify() } }) }
可以看到,新增的代码在判断value
是否为Array类型后,调用了一个叫做dependArray
的方法,那么这个方法做了什么呢
1 2 3 4 5 6 7 8 9 10 11 12 13 function dependArray (value ) { for (var e = (void 0 ), i = 0 , l = value.length ; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__ .dep .depend (); if (Array .isArray (e)) { dependArray (e); } } }
可以看到,vue的处理是,触发深层所有数组保存的observer.dep.depend()
方法,从而让所有的子数组都能保存渲染watcher。 我们去掉源码中的注释,然后再测试一下 现在就好了,深层的数组也能被观测了
后记 好啦这次的分享就到这里了,如果有错误的地方,欢迎各位大佬指正,那么下次见(咕咕咕)。顺带祝各位大佬们520快乐。