vue深入学习--vue2响应式原理

104 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

前提: 大概了解vue2的响应式的了解,对原理有个概念。

目的: 想要以点带面的走通vue2的响应式原理。

new Vue()初始化之后,对options进行初始化。其中initstate()是对props,data,methods,computed和watcher。

export function initState(vm:component){
if (opts.props) initProps(vm, opts.props) 
vm[key] = methods[key] if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) }
if (opts.computed) initComputed(vm, opts.computed) 
if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }
}

第一步:initProps

首先会对props进行初始化,循环遍历props对象,将每个props的值设置为响应式的,并且将每个props都挂在到vm上,便于使用this.xxx进行访问。

for (const key in propsOptions) {
const value = validateProp(key, propsOptions, propsData, vm) 
defineReactive(props, key, value)//将props设置为响应式
if (!(key in vm)) { 
proxy(vm, `_props`, key) //将props挂载到vm上去
} 
}

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

对methods进行判重,命名的优先级props高于methods,将methods挂载到vm上,便于使用this.xxx进行使用。

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)) { //进行判断是否和props重复,如果重复发出警告
            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) }//将methods挂载到vm上边

第三步:initData

对data进行判重,命名的优先级props>methods>data,然后将data挂载到vm上,最后将数据设置为响应式(observer)

while (i--) { 
const key = keys[i] if (process.env.NODE_ENV !== 'production') { 
if (methods && hasOwn(methods, key)) { //和methods进行比对判重
warn( `Method "${key}" has already been defined as a data property.`, vm ) } } 
if (props && hasOwn(props, key)) { //和props进行比对判重
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) } } // 为 data 对象上的数据设置响应式 
observe(data, true /* asRootData */)

第四步:initComputed

首先创建computed[key]创建watcher实例,默认是懒执行。代理computed[key]到vm实例,判重,computed不能和props,data重复了。

for (const key in computed) { 
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) 
} 
} 
}

首先会为每个computed属性创建watcher,并将取出每个computed的get,将getter作为watcher的参数。

const computedWatcherOptions = { lazy: true }
const watchers = vm._computedWatchers = Object.create(null)
for(let key in computed){
    if (!isSSR) { // 如果不是服务端渲染,为 computed 属性创建 watcher 实例
    watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) }

}

接下来将computed的每个属性挂载vm上

for(let key in computed){
    defineComputed(vm, key, userDef)//将computed挂载到vm上,并且对computed做数据劫持,当访问的时候会触发getter
}
function computedGetter () { computed的getter
const watcher = this._computedWatchers && this._computedWatchers[key] 
if (watcher) { 
if (watcher.dirty) {
watcher.evaluate() 
} if (Dep.target) { 
watcher.depend()
} 
return watcher.value 
}

当通过this.xxx访问computed的时候,就会返回对应的watcher的value,在返回之前,判断dirty是否为true,如果为true第一次加载就会进行计算computed中对应的watcher的value,接下来就不会重复计算了。

constructor(){//watcher的构造函数,
if (typeof expOrFn === 'function') { this.getter = expOrFn }//将computed的getter(exporFn)赋值给this.getter中
this.value = this.lazy ? undefined : this.get()当new一个watcher的时候就会调用get函数
}
get () { 
Dep.target = this pushTarget(this) 
let value const vm = this.vm 
try { 
value = this.getter.call(vm, vm) //调用computed的getter函数,在函数中会访问data中的数据,这就触发了data的getter函数
} 
catch (e) { 
if (this.user) { 
handleError(e, vm, `getter for watcher "${this.expression}"`) 
} 
else {
throw e
} 
} 
finally { 
if (this.deep) {
traverse(value) 
} // 关闭 Dep.target,
Dep.target = null
popTarget() 
this.cleanupDeps() 
} 
return value 
}

第五步,初始化watch 首先遍历watch,找到和watch 所监听的数据(名字相同的),这个数据在之前的初始化过程已经在vm上挂载了。直接从vm中获取。

handler = vm[handler]

根据获取的这个属性,创建一个对应的watcher

const watcher = new Watcher(vm, expOrFn, cb, options)

data中对应的数据会收集这个watcher,当修改data中的数据会触发数据劫持的set,从而通知watcher进行更新,从而对watcher中设置的watch进行回调

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)
} 
watcher 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() } 
}

总结

网上单个分析代码的文章已经很多了,但是我看了很多片文章都会了只是讲解了知识,而没有把知识串联在一起。现在我将总结串联vue2的响应式的实现原理。

首先,在初始化data的时候,我们会对data中的数据通过object.defineProperty重写get和set进行数据劫持。对于层级比较深的数据会进行递归来实现数据劫持,每一个属性都会new一个Dep(称之为依赖收集器),在数据的getter函数触发的时候,dep会调用depend方法,会进行收集Dep.target的watcher(监听器,我更愿意称之为依赖,虽然不标准)到自己的subs队列,当触发数据的setter函数的时候,会触发dep的notify方法,notify方法会将遍历dep自身的subs队列,循环调用watcher的update方法,watcher的update方法则是 将watcher放入watcher队列中,进行异步更新。到这里大致流程已经清楚了。

现在提出几个问题: 问题一:dep只收集Dep.target上的watcher,那么watcher是什么时候放到Dep.target? 问题二:dep要收集所有使用到data的依赖,那么多个watcher都要放到Dep.target上边去,那么怎么安排多个watcher放到Dep.target? 问题三:computed和watch(没错!就是options里边的那个watch)是怎么实现的?有什么区别? 当初自己学响应式原理的时候就被问题一和问题二锁死了,今天经过了学习,有了一种初窥门径的感觉,这三个问题实际上都是一个问题watcher的执行原理,把watcher源码看懂,就会理解了。

首先拿computed举例,在初始化computed的时候,我们会遍历computed对象,将computed对象中的每一项进行代理到vm上,可以用this.xxx访问,同时对computed的访问进行拦截,也就是数据劫持,当访问computed的属性的时候,就会触发重写的getter(数据拦截)。

在遍历computed的时候,vue会为computed中的每一项创建对应的watcher,并且从computed获取当前的getter(options中的computed的getter或者是简略写法,直接把computed写成个函数就是个getter)作为参数来new一个watcher。在实例化一个watcher的时候,watcher的构造函数会调用自身的get方法,在get方法中,会调用我们传入的getter方法,进行计算watcher的value值。这个watcher的value值,也正是我们访问computed的时候返回的值(之前vue对computed做了数据劫持)。

问题一的奥妙就在这个watcher自身的get方法中!

在get方法中,首先会将当前的watcher,也就是我们正在实例化的watcher,放入Dep.target中,然后会调用我们创建watcher传入的getter函数,在这个函数中,我们会使用到data中的数据,然后就触发了data的数据劫持了!在触发data的get的时候当前这个被computed的watcher访问到的data数据的dep,就会触发depend方法,进行收集Dep.target(也就是当前的watcher),然后计算出computed值后,再将Dep.target上边的值置空。这就完美的实现了一次dep收集相关watcher的过程。

问题二的答案也就是不言而喻了,随着不断遍历和初始化data中的dep们,就会收集到了所有的依赖,也就是使用到data中数据的地方。

computed有一个重要的特性就是缓存机制这个缓存机制就是在遍历computed的时候,创建watcher传入一个参数computedWatcherOptions(不懂可以翻看initComputed的第二个代码段),computedWatcherOptions对象有一个lazy属性,这个属性设置为true,computed的watcher初始化的时候会将lazy的值保存起来,并将自身的dirty设置为lazy,也就是设置为true。当访问computed的时候会判断对应的watcher的dirty属性是否是true,如果是true则证明是第一次访问,就会进行计算,并将watcher的dirty设置为false,下次在访问的时候,因为dirty已经被设置为false,那么就会直接返回watch.value。当computed所依赖的data发生修改的时候会触发computed对应的watch的update方法,在update方法中判断watcher的lazy是否为true(watcher的lazy属性就是判断是否是计算属性的,lazy为true就是计算属性),如果是当前watcher的lazy为true,那么就会将watcher的dirty设置为true,再次访问computed的时候就会重新进行计算。watch与computed不同的地方就是在它们的watcher在update方法的不同,computed的watcher会将dirty置为true,而watch的watcher则会将watcher加入watcher队列(这个就是watch可以进行异步操作,而computed不能异步操作的关键的关键)。

哈哈哈哈哈,经典正文没有总结篇幅长.....