vue 在初始化时会调用 initState(vm) 使当前 vue 实例具有响应式
initState /src/core/instance/state.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 47 48 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 ) } }
initProps 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 function initProps (vm : Component , propsOptions : Object ) { const propsData = vm.$options .propsData || {} const props = vm._props = {} const keys = vm.$options ._propKeys = [] const isRoot = !vm.$parent if (!isRoot) { toggleObserving (false ) } for (const key in propsOptions) { keys.push (key) const value = validateProp (key, propsOptions, propsData, vm) defineReactive (props, key, value) if (!(key in vm)) { proxy (vm, `_props` , key) } } toggleObserving (true ) }
proxy 1 2 3 4 5 6 7 8 9 10 export function proxy (target : Object , sourceKey : string, key : string) { sharedPropertyDefinition.get = function proxyGetter () { return this [sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this [sourceKey][key] = val } Object .defineProperty (target, key, sharedPropertyDefinition) }
initMethods 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 function initMethods (vm : Component , methods : Object ) { const props = vm.$options .props for (const key in methods) { if (process.env .NODE_ENV !== 'production' ) { if (typeof methods[key] !== 'function' ) { warn ( `Method "${key} " has type "${typeof methods[key]} " in the component definition. ` + `Did you reference the function correctly?` , vm ) } if (props && hasOwn (props, key)) { warn ( `Method "${key} " has already been defined as a prop.` , vm ) } if ((key in vm) && isReserved (key)) { warn ( `Method "${key} " conflicts with an existing Vue instance method. ` + `Avoid defining component methods that start with _ or $.` ) } } vm[key] = typeof methods[key] !== 'function' ? noop : bind (methods[key], vm) } }
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 55 56 57 58 59 60 61 62 63 64 65 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 ) }export function getData (data : Function , vm : Component ): any { pushTarget () try { return data.call (vm, vm) } catch (e) { handleError (e, vm, `data()` ) return {} } finally { popTarget () } }
initComputed 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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 const computedWatcherOptions = { lazy : true }function initComputed (vm : Component , computed : Object ) { const watchers = vm._computedWatchers = Object .create (null ) 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) { watchers[key] = new Watcher ( vm, getter || noop, noop, computedWatcherOptions ) } 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) : createGetterInvoker (userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter (key) : createGetterInvoker (userDef.get ) : noop sharedPropertyDefinition.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) { if (watcher.dirty ) { watcher.evaluate() } if (Dep .target ) { watcher.depend () } return watcher.value } } }function createGetterInvoker (fn ) { return function computedGetter () { return fn.call (this , this ) } }
initWatch 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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 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) }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 ) { try { cb.call (vm, watcher.value ) } catch (error) { handleError (error, vm, `callback for immediate watcher "${watcher.expression} "` ) } } return function unwatchFn () { watcher.teardown () } }
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 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 进行响应式处理
Observer 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 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]) } } }
因为 vue2 使用的是 Object.defineProperty 无法监听数组的改变,所以数组和对象分开处理。
对象遍历调用 defineReactive() 函数
数组遍历调用 observe() 函数
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 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.depend(),将 dep 添加到 watcher 中,也将 watcher 添加到 dep 中
修改数据时,判断新老值是否相同,判断属性是否只读,对新值做响应式处理,依赖通知更新 dep.notify(),通知收集的所有 watcher,让 watcher 执行 update 方法。
initProps
为 props 对象的每个属性设置响应式,调用 defineReactive()
代理到 vue 实例上,可以通过 this.propKey 访问,调用 proxy()
initMethods
检验 methods[key] 是不是函数
判重
methods 中的 key 不能和 props 中的 key 相同
methods 中的 key 与 Vue 实例上已有的方法重叠,一般是一些内置方法,比如以 $ 和 _ 开头的方法
代理到 vue 实例上,可以通过 this.methodKey 访问
initData
initComputed
computed 是通过 watcher 实现的,对每个 computedKey 实例化一个 watcher,默认懒执行
代理到 vue 实例上,可以通过 this.computedKey 访问,调用 defineComputed()
computed 缓存的实现原理(dirty 控制)
initWatch
遍历 watch对象,对每一项调用 createWatcher,在其中对 handler 做处理(因为 watch 的书写有很多种形式),处理完毕后调用 vm.$watch,实例化一个 watcher,如果 immediate 为 true 的话,立即执行一次 callback。返回一个 unwatch 函数,用于解绑。
面试题 Vue 响应式原理是怎么实现的? 响应式的核心是通过 Object.defineProperty
拦截对数据的访问和设置
响应式的数据分为两类:
对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter
访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher
设置数据时由 dep 通知相关的 watcher 去更新
数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作
添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
删除数据时,也要由 dep 通知 watcher 去更新
对象属性经过深度遍历后,最终就是以一个基本类型的数据为单位收集依赖,但是数组仍然是一个引用类型 。
methods、computed 和 watch 有什么区别? 使用场景
methods 一般用于封装一些较为复杂的处理逻辑(同步、异步)
computed 一般用于封装一些简单的同步逻辑,将经过处理的数据返回,然后显示在模版中,以减轻模版的重量
watch 一般用于当需要在数据变化时执行异步或开销较大的操作
区别
methods VS computed
如果在一次渲染中,有多个地方使用了同一个 methods 或 computed 属性,methods 会被执行多次,而 computed 的回调函数则只会被执行一次。
通过阅读源码我们知道,在一次渲染中,多次访问 computedProperty,只会在第一次执行 computed 属性的回调函数,后续的其它访问,则直接使用第一次的执行结果(watcher.value),而这一切的实现原理则是通过对 watcher.dirty 属性的控制实现的。在第一次执行后 dirty 变为 false,等到数据变化之后,重新渲染页面时,才会重新变为 true。当 dirty 为 false 时,直接返回 watcher.value,而不执行 watcher.evaluate 重新计算。而 methods,每一次的访问则是简单的方法调用(this.xxMethods)。
computed VS watch
通过阅读源码我们知道,computed 和 watch 的本质是一样的,内部都是通过 Watcher 来实现的,其实没什么区别,非要说区别的化就两点:1、使用场景上的区别,2、computed 默认是懒执行的,切不可更改。
methods VS watch
methods 和 watch 之间其实没什么可比的,完全是两个东西,不过在使用上可以把 watch 中一些逻辑抽到 methods 中,提高代码的可读性。
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 73 74 75 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 ] }
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 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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 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; constructor ( vm : Component , expOrFn : string | Function , cb : Function , options?: ?Object , isRenderWatcher?: boolean ) { this .vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers .push (this ) if (options) { this .deep = !!options.deep this .user = !!options.user this .lazy = !!options.lazy this .sync = !!options.sync this .before = options.before } else { this .deep = this .user = this .lazy = this .sync = false } this .cb = cb this .id = ++uid this .active = true this .dirty = this .lazy this .deps = [] this .newDeps = [] this .depIds = new Set () this .newDepIds = new Set () this .expression = process.env .NODE_ENV !== 'production' ? expOrFn.toString () : '' if (typeof expOrFn === 'function' ) { this .getter = expOrFn } else { this .getter = parsePath (expOrFn) if (!this .getter ) { this .getter = noop 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 .value = this .lazy ? undefined : this .get () } 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 { if (this .deep ) { traverse (value) } popTarget () this .cleanupDeps () } return value } 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 ) } } } 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 } update () { if (this .lazy ) { this .dirty = true } else if (this .sync ) { this .run () } else { queueWatcher (this ) } } 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) } } } } evaluate () { this .value = this .get () this .dirty = false } depend () { let i = this .deps .length while (i--) { this .deps [i].depend () } } teardown () { if (this .active ) { if (!this .vm ._isBeingDestroyed ) { remove (this .vm ._watchers , this ) } let i = this .deps .length while (i--) { this .deps [i].removeSub (this ) } this .active = false } } }
总结 在 new Vue 时,vue 初始化的时候会调用 initState,在 initState 中会依次调用,initProps、initMethods、initData、initComputed、initWatch,这个顺序是固定的,因为存在优先级,一些属性是不能重复的,后面的函数会做判断,来避免重复的情况。
在具体的 init 函数中,我主要说一下 data 吧,首先 data 会做一个判重,避免和 props 以及 methods 的属性重复。然后会调用定义好的 proxy 函数将 data 中的数据代理到 vue 实例上,以便于通过 this.key 去访问。最后调用 observe 函数,传入data,给数据设置响应式。observe 函数就是数据响应式的入口,在 observe 函数里面会 new 一个 Observer,Observer 是一个观察者类,在构造函数中,会判断传进来的是数组还是对象。
如果是对象的话,会调用 walk 函数,遍历所有键,调用 defineReactive,在 defineReactive 中,首先会做一个递归处理,因为响应式是深层的嘛,不过不是递归调用自身 (defineReactive)而是调用 observe,传入值,再 new Observer,再调用 defineReactive,可以说是三个函数的循环递归。defineReactive 递归后,通过 object.defineProperty 设置 getter 和 setter,这也是真正响应式的核心,getter 中 会通过 dep.depend 做依赖收集,dep 添加到 watcher 中,watcher 也添加到 dep 中,dep 实际上就是 object.key。setter 中首先做一些处理,判断新值和老值是不是一样,判断属性是不是只读,处理完毕才会设置新值,然后对新值调用 observe,让新值也是响应式的,然后调用 dep.notify 通知依赖更新。
如果是数组的话,首先会改写数组原型上的方法,因为这些方法都可以改变数组。然后会调用 observeArray 函数,对数组的每一项调用 observe,总归是又回到了 observe,后面的流程其实和对象差不多。对于数组原型方法的改写其实也比较简单,再执行原生的方法之后,需要判断该方法,是不是 push/unshift/splice,如果是的话,就代表插入了新元素,需要调用 observeArray 对新元素做响应式处理,然后notify 通知更新。
然后像 initProps 的话,就是比较简单的遍历 props,defineReactive 设置响应式,再 proxy 代理到 vue 实例上。
initMethods,首先判断是否为函数,然后判重,最后绑定到 vue 实例上。
initComputed,也有判重和绑定到 vue 实例的步骤,主要是调用 createWatcher 创建一个懒执行的 watcher,调用 $watch,$watch里会对配置对象做一些处理,比如说 immediate 为 true,$watch 里就会理解调用一次函数,最后会返回一个 unwatch 函数用来解除监听。
initWatch 其实是差不多的。所以可以说 computed 和 watch 的本质是一样的,都是通过 Watcher 实现的,只是使用场景上有些区别,然后 computed 是懒执行的。computed 的缓存性是通过 watcher 的 dirty 属性 以及 evaluate 和 update 方法实现的,dirty 为 true 时,才会 watcher.evaluate 来计算新的结果并把 dirty 置为 false,否则就用缓存的结果。直到页面更新后,watcher.update 调用后才会将 dirty 置为 true。
参考 https://juejin.cn/post/6950826293923414047
https://juejin.cn/post/7074422512318152718