Vue原理解析——依赖收集
上篇文章中我们谈到了数据的响应化的工作流程,其中很重要的一部分就是依赖收集的过程。实际上依赖收集不只在数据响应化时用到,这篇文章就集中来谈谈。
我们之前说过数据的响应化是从initState这个函数中的initData函数开始的,实际上initData只是该函数的一部分,该函数内部实现如下:
export function initState (vm: Component) {
// 首先在vm上初始化一个_watchers数组,缓存这个vm上的所有watcher
vm._watchers = []
// 获取options,包括在new Vue传入的,同时还包括了Vue所继承的options
const opts = vm.$options
// 初始化props属性
if (opts.props) initProps(vm, opts.props)
// 初始化methods属性
if (opts.methods) initMethods(vm, opts.methods)
// 初始化data属性
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed属性
if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch属性
if (opts.watch) initWatch(vm, opts.watch)
}
可以看到除了data属性的数据初始化,还存在着props,computed,watch属性的初始化,这些属性也正是同样需要依赖收集的地方。
initProps
实例化Vue对象时,会通过props属性表示从父组件传递的值。对该值的处理都集中在initProp函数中,具体如下:
function initProps (vm: Component, propsOptions: Object) {
// propsData主要是为了方便测试使用
const propsData = vm.$options.propsData || {}
// 新建vm._props对象,可以通过app实例去访问
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
// 缓存的prop key
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
observerState.shouldConvert = isRoot
for (const key in propsOptions) {
// this._init传入的options中的props属性
keys.push(key)
// 注意这个validateProp方法,不仅完成了prop属性类型验证的,同时将prop的值都转化为了getter/setter,并返回一个observer
const value = validateProp(key, propsOptions, propsData, vm)
// 将这个key对应的值转化为getter/setter
defineReactive(props, key, value)
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
// 如果在vm这个实例上没有key属性,那么就通过proxy转化为proxyGetter/proxySetter, 并挂载到vm实例上,可以通过app._props[key]这种形式去访问
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
observerState.shouldConvert = true
}
可以看到该函数中主要做的事情有两个,一是验证props,二是我们之前提到的数据响应式处理。
先看验证props的validateProp函数,该函数实现如下:
function validateProp (
key: string,
propOptions: Object, // $options.props属性
propsData: Object, // $options.propsData属性
vm?: Component
): any {
const prop = propOptions[key]
// 如果在propsData测试props上没有缓存的key
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// 处理boolean类型的数据
// handle boolean props
if (isType(Boolean, prop.type)) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
value = true
}
}
// check default value
if (value === undefined) {
// default属性值,是基本类型还是function
// getPropsDefaultValue见下面第一段代码
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldConvert = observerState.shouldConvert
observerState.shouldConvert = true
// 将value的所有属性转化为getter/setter形式
// 并添加value的依赖
observe(value)
observerState.shouldConvert = prevShouldConvert
}
if (process.env.NODE_ENV !== 'production') {
assertProp(prop, key, value, vm, absent)
}
return value
}
这部分主要任务在于props的默认值获取,然后通过observe函数将其响应化。之后的实现就和initData中的依赖收集相同了。大致流程还是触发订阅者函数 => 订阅者放入Dep.target => 订阅者触发prop的getter => getter将订阅者放入这个prop的订阅者数组中 => 依赖收集完成 => prop更新时触发订阅者的更新。
initComputed
计算属性初始化的过程中,会为每个属性先实例化一个订阅者,代码如下:
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// 新建_computedWatchers属性
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
// 如果computed为funtion,即取这个function为getter函数
// 如果computed为非function.则可以单独为这个属性定义getter/setter属性
let getter = typeof userDef === 'function' ? userDef : userDef.get
// create internal watcher for the computed property.
// lazy属性为true
// 注意这个地方传入的getter参数
// 实例化的过程当中不去完成依赖的收集工作
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
由于传入了{lazy: true}
的配置选项,组件初始化时不会立即对计算属性进行求值和依赖收集,而是实例化为订阅者后同时将其响应化。当访问该计算属性时再触发相同的依赖收集流程。需要的是计算属性响应化后的getter会新增一条对其订阅者的dirty属性的判断。代码如下:
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
}
}
}
该属性主要用于判断该计算属性是否需要根据其依赖重新求值,否则就使用缓存值。
initWatch
这个函数就比较简单了,watch选项或是$watch方法都能触发,就是实例化一个订阅者将其放入观测属性的订阅者数组中即可。走的流程仍然是依赖收集,区别在于这里的依赖是我们手动定义的key。
-- EOF --